5cae59bc
Add text coverage comparison mode
a73x 2026-04-09 08:32
Commit message
src/main.zig
| Old | New | ||
|---|---|---|---|
| @@ -5,6 +5,7 @@ const wayland_client = @import("wayland-client"); | |||
| 5 | const renderer = @import("renderer"); | 5 | const renderer = @import("renderer"); |
| 6 | const font = @import("font"); | 6 | const font = @import("font"); |
| 7 | const config = @import("config"); | 7 | const config = @import("config"); |
| 8 | const vk = @field(renderer, "vk"); | ||
| 8 | 9 | ||
| 9 | const c = @cImport({ | 10 | const c = @cImport({ |
| 10 | @cInclude("xkbcommon/xkbcommon-keysyms.h"); | 11 | @cInclude("xkbcommon/xkbcommon-keysyms.h"); |
| @@ -56,6 +57,10 @@ pub fn main() !void { | |||
| 56 | return runDrawSmokeTest(alloc); | 57 | return runDrawSmokeTest(alloc); |
| 57 | } | 58 | } |
| 58 | 59 | ||
| 60 | if (args.len >= 2 and std.mem.eql(u8, args[1], "--text-compare")) { | ||
| 61 | return runTextCoverageCompare(alloc); | ||
| 62 | } | ||
| 63 | |||
| 59 | return runTerminal(alloc); | 64 | return runTerminal(alloc); |
| 60 | } | 65 | } |
| 61 | 66 | ||
| @@ -494,6 +499,255 @@ fn comparisonPanelOrigins(panel_cols: u32, top_margin_rows: u32) [4][2]f32 { | |||
| 494 | return origins; | 499 | return origins; |
| 495 | } | 500 | } |
| 496 | 501 | ||
| 502 | const ComparisonPanelDraw = struct { | ||
| 503 | instance_offset_instances: u32, | ||
| 504 | instance_count: u32, | ||
| 505 | coverage: [2]f32, | ||
| 506 | origin_col: u32, | ||
| 507 | }; | ||
| 508 | |||
| 509 | const TextCoverageCompareScene = struct { | ||
| 510 | instances: std.ArrayListUnmanaged(renderer.Instance) = .empty, | ||
| 511 | panel_draws: [4]ComparisonPanelDraw, | ||
| 512 | panel_cols: u32, | ||
| 513 | window_cols: u32, | ||
| 514 | window_rows: u32, | ||
| 515 | |||
| 516 | fn deinit(self: *TextCoverageCompareScene, alloc: std.mem.Allocator) void { | ||
| 517 | self.instances.deinit(alloc); | ||
| 518 | } | ||
| 519 | }; | ||
| 520 | |||
| 521 | fn comparisonPanelCols(lines: []const []const u8) u32 { | ||
| 522 | var max_cols: u32 = 0; | ||
| 523 | for (lines) |line| { | ||
| 524 | max_cols = @max(max_cols, @as(u32, @intCast(line.len))); | ||
| 525 | } | ||
| 526 | return max_cols + 4; | ||
| 527 | } | ||
| 528 | |||
| 529 | fn comparisonVisibleGlyphCount(lines: []const []const u8) u32 { | ||
| 530 | var count: u32 = 0; | ||
| 531 | for (lines) |line| { | ||
| 532 | for (line) |char| { | ||
| 533 | if (char != ' ') count += 1; | ||
| 534 | } | ||
| 535 | } | ||
| 536 | return count; | ||
| 537 | } | ||
| 538 | |||
| 539 | fn buildTextCoverageCompareScene( | ||
| 540 | alloc: std.mem.Allocator, | ||
| 541 | face: *font.Face, | ||
| 542 | atlas: *font.Atlas, | ||
| 543 | ) !TextCoverageCompareScene { | ||
| 544 | const specimen_lines = comparisonSpecimenLines(); | ||
| 545 | const variants = comparisonVariants(); | ||
| 546 | const top_margin_rows: u32 = 2; | ||
| 547 | const panel_cols = comparisonPanelCols(specimen_lines); | ||
| 548 | const panel_origins = comparisonPanelOrigins(panel_cols, top_margin_rows); | ||
| 549 | const panel_glyph_count = comparisonVisibleGlyphCount(specimen_lines); | ||
| 550 | |||
| 551 | var instances: std.ArrayListUnmanaged(renderer.Instance) = .empty; | ||
| 552 | errdefer instances.deinit(alloc); | ||
| 553 | try instances.ensureTotalCapacity(alloc, @as(usize, panel_glyph_count) * variants.len); | ||
| 554 | |||
| 555 | const baseline = face.baseline(); | ||
| 556 | const fg = [4]f32{ 1.0, 1.0, 1.0, 1.0 }; | ||
| 557 | const bg = [4]f32{ 0.0, 0.0, 0.0, 1.0 }; | ||
| 558 | |||
| 559 | var panel_draws: [4]ComparisonPanelDraw = undefined; | ||
| 560 | for (variants, 0..) |variant, panel_idx| { | ||
| 561 | const origin = panel_origins[panel_idx]; | ||
| 562 | const instance_offset_instances: u32 = @intCast(instances.items.len); | ||
| 563 | |||
| 564 | for (specimen_lines, 0..) |line, row_idx| { | ||
| 565 | for (line, 0..) |char, col_idx| { | ||
| 566 | if (char == ' ') continue; | ||
| 567 | |||
| 568 | const glyph_uv = try atlas.getOrInsert(face, char); | ||
| 569 | try instances.append(alloc, .{ | ||
| 570 | .cell_pos = .{ | ||
| 571 | origin[0] + @as(f32, @floatFromInt(col_idx)), | ||
| 572 | origin[1] + @as(f32, @floatFromInt(row_idx)), | ||
| 573 | }, | ||
| 574 | .glyph_size = .{ | ||
| 575 | @floatFromInt(glyph_uv.width), | ||
| 576 | @floatFromInt(glyph_uv.height), | ||
| 577 | }, | ||
| 578 | .glyph_bearing = .{ | ||
| 579 | @floatFromInt(glyph_uv.bearing_x), | ||
| 580 | glyphTopOffset(baseline, glyph_uv.bearing_y), | ||
| 581 | }, | ||
| 582 | .uv_rect = .{ | ||
| 583 | glyph_uv.u0, | ||
| 584 | glyph_uv.v0, | ||
| 585 | glyph_uv.u1, | ||
| 586 | glyph_uv.v1, | ||
| 587 | }, | ||
| 588 | .fg = fg, | ||
| 589 | .bg = bg, | ||
| 590 | }); | ||
| 591 | } | ||
| 592 | } | ||
| 593 | |||
| 594 | panel_draws[panel_idx] = .{ | ||
| 595 | .instance_offset_instances = instance_offset_instances, | ||
| 596 | .instance_count = @as(u32, @intCast(instances.items.len)) - instance_offset_instances, | ||
| 597 | .coverage = variant.coverage, | ||
| 598 | .origin_col = @as(u32, @intCast(panel_idx)) * panel_cols, | ||
| 599 | }; | ||
| 600 | } | ||
| 601 | |||
| 602 | return .{ | ||
| 603 | .instances = instances, | ||
| 604 | .panel_draws = panel_draws, | ||
| 605 | .panel_cols = panel_cols, | ||
| 606 | .window_cols = panel_cols * @as(u32, @intCast(variants.len)), | ||
| 607 | .window_rows = top_margin_rows + @as(u32, @intCast(specimen_lines.len)) + 2, | ||
| 608 | }; | ||
| 609 | } | ||
| 610 | |||
| 611 | fn drawTextCoverageCompareFrame( | ||
| 612 | ctx: *renderer.Context, | ||
| 613 | scene: *const TextCoverageCompareScene, | ||
| 614 | cell_w_px: u32, | ||
| 615 | cell_h_px: u32, | ||
| 616 | clear_color: [4]f32, | ||
| 617 | ) !void { | ||
| 618 | _ = try ctx.vkd.waitForFences( | ||
| 619 | ctx.device, | ||
| 620 | 1, | ||
| 621 | @ptrCast(&ctx.in_flight_fence), | ||
| 622 | .true, | ||
| 623 | std.math.maxInt(u64), | ||
| 624 | ); | ||
| 625 | try ctx.vkd.resetFences(ctx.device, 1, @ptrCast(&ctx.in_flight_fence)); | ||
| 626 | |||
| 627 | const acquire = ctx.vkd.acquireNextImageKHR( | ||
| 628 | ctx.device, | ||
| 629 | ctx.swapchain, | ||
| 630 | std.math.maxInt(u64), | ||
| 631 | ctx.image_available, | ||
| 632 | .null_handle, | ||
| 633 | ) catch |err| switch (err) { | ||
| 634 | error.OutOfDateKHR => return error.OutOfDateKHR, | ||
| 635 | else => return err, | ||
| 636 | }; | ||
| 637 | if (acquire.result == .suboptimal_khr) return error.OutOfDateKHR; | ||
| 638 | const image_index = acquire.image_index; | ||
| 639 | |||
| 640 | try ctx.vkd.resetCommandBuffer(ctx.command_buffer, .{}); | ||
| 641 | try ctx.vkd.beginCommandBuffer(ctx.command_buffer, &vk.CommandBufferBeginInfo{ | ||
| 642 | .flags = .{ .one_time_submit_bit = true }, | ||
| 643 | }); | ||
| 644 | |||
| 645 | const clear_value = vk.ClearValue{ .color = .{ .float_32 = clear_color } }; | ||
| 646 | ctx.vkd.cmdBeginRenderPass(ctx.command_buffer, &vk.RenderPassBeginInfo{ | ||
| 647 | .render_pass = ctx.render_pass, | ||
| 648 | .framebuffer = ctx.framebuffers[image_index], | ||
| 649 | .render_area = .{ | ||
| 650 | .offset = .{ .x = 0, .y = 0 }, | ||
| 651 | .extent = ctx.swapchain_extent, | ||
| 652 | }, | ||
| 653 | .clear_value_count = 1, | ||
| 654 | .p_clear_values = @ptrCast(&clear_value), | ||
| 655 | }, .@"inline"); | ||
| 656 | |||
| 657 | ctx.vkd.cmdBindPipeline(ctx.command_buffer, .graphics, ctx.pipeline); | ||
| 658 | |||
| 659 | const viewport = vk.Viewport{ | ||
| 660 | .x = 0.0, | ||
| 661 | .y = 0.0, | ||
| 662 | .width = @floatFromInt(ctx.swapchain_extent.width), | ||
| 663 | .height = @floatFromInt(ctx.swapchain_extent.height), | ||
| 664 | .min_depth = 0.0, | ||
| 665 | .max_depth = 1.0, | ||
| 666 | }; | ||
| 667 | ctx.vkd.cmdSetViewport(ctx.command_buffer, 0, 1, @ptrCast(&viewport)); | ||
| 668 | |||
| 669 | ctx.vkd.cmdBindDescriptorSets( | ||
| 670 | ctx.command_buffer, | ||
| 671 | .graphics, | ||
| 672 | ctx.pipeline_layout, | ||
| 673 | 0, | ||
| 674 | 1, | ||
| 675 | @ptrCast(&ctx.descriptor_set), | ||
| 676 | 0, | ||
| 677 | null, | ||
| 678 | ); | ||
| 679 | |||
| 680 | const buffers = [_]vk.Buffer{ ctx.quad_vertex_buffer, ctx.instance_buffer }; | ||
| 681 | const panel_width_px = scene.panel_cols * cell_w_px; | ||
| 682 | |||
| 683 | for (scene.panel_draws) |panel_draw| { | ||
| 684 | const scissor_x_px = panel_draw.origin_col * cell_w_px; | ||
| 685 | if (scissor_x_px >= ctx.swapchain_extent.width) continue; | ||
| 686 | |||
| 687 | const scissor = vk.Rect2D{ | ||
| 688 | .offset = .{ .x = @intCast(scissor_x_px), .y = 0 }, | ||
| 689 | .extent = .{ | ||
| 690 | .width = @min(panel_width_px, ctx.swapchain_extent.width - scissor_x_px), | ||
| 691 | .height = ctx.swapchain_extent.height, | ||
| 692 | }, | ||
| 693 | }; | ||
| 694 | ctx.vkd.cmdSetScissor(ctx.command_buffer, 0, 1, @ptrCast(&scissor)); | ||
| 695 | |||
| 696 | const push_constants = renderer.PushConstants{ | ||
| 697 | .viewport_size = .{ | ||
| 698 | @floatFromInt(ctx.swapchain_extent.width), | ||
| 699 | @floatFromInt(ctx.swapchain_extent.height), | ||
| 700 | }, | ||
| 701 | .cell_size = .{ | ||
| 702 | @floatFromInt(cell_w_px), | ||
| 703 | @floatFromInt(cell_h_px), | ||
| 704 | }, | ||
| 705 | .coverage_params = panel_draw.coverage, | ||
| 706 | }; | ||
| 707 | ctx.vkd.cmdPushConstants( | ||
| 708 | ctx.command_buffer, | ||
| 709 | ctx.pipeline_layout, | ||
| 710 | .{ .vertex_bit = true, .fragment_bit = true }, | ||
| 711 | 0, | ||
| 712 | @sizeOf(renderer.PushConstants), | ||
| 713 | @ptrCast(&push_constants), | ||
| 714 | ); | ||
| 715 | |||
| 716 | const offsets = [_]vk.DeviceSize{ | ||
| 717 | 0, | ||
| 718 | @as(vk.DeviceSize, panel_draw.instance_offset_instances) * @sizeOf(renderer.Instance), | ||
| 719 | }; | ||
| 720 | ctx.vkd.cmdBindVertexBuffers(ctx.command_buffer, 0, 2, &buffers, &offsets); | ||
| 721 | ctx.vkd.cmdDraw(ctx.command_buffer, 6, panel_draw.instance_count, 0, 0); | ||
| 722 | } | ||
| 723 | |||
| 724 | ctx.vkd.cmdEndRenderPass(ctx.command_buffer); | ||
| 725 | try ctx.vkd.endCommandBuffer(ctx.command_buffer); | ||
| 726 | |||
| 727 | const wait_stage = vk.PipelineStageFlags{ .color_attachment_output_bit = true }; | ||
| 728 | try ctx.vkd.queueSubmit(ctx.graphics_queue, 1, @ptrCast(&vk.SubmitInfo{ | ||
| 729 | .wait_semaphore_count = 1, | ||
| 730 | .p_wait_semaphores = @ptrCast(&ctx.image_available), | ||
| 731 | .p_wait_dst_stage_mask = @ptrCast(&wait_stage), | ||
| 732 | .command_buffer_count = 1, | ||
| 733 | .p_command_buffers = @ptrCast(&ctx.command_buffer), | ||
| 734 | .signal_semaphore_count = 1, | ||
| 735 | .p_signal_semaphores = @ptrCast(&ctx.render_finished), | ||
| 736 | }), ctx.in_flight_fence); | ||
| 737 | |||
| 738 | const present_result = ctx.vkd.queuePresentKHR(ctx.present_queue, &vk.PresentInfoKHR{ | ||
| 739 | .wait_semaphore_count = 1, | ||
| 740 | .p_wait_semaphores = @ptrCast(&ctx.render_finished), | ||
| 741 | .swapchain_count = 1, | ||
| 742 | .p_swapchains = @ptrCast(&ctx.swapchain), | ||
| 743 | .p_image_indices = @ptrCast(&image_index), | ||
| 744 | }) catch |err| switch (err) { | ||
| 745 | error.OutOfDateKHR => return error.OutOfDateKHR, | ||
| 746 | else => return err, | ||
| 747 | }; | ||
| 748 | if (present_result == .suboptimal_khr) return error.OutOfDateKHR; | ||
| 749 | } | ||
| 750 | |||
| 497 | const RowRefreshState = enum { | 751 | const RowRefreshState = enum { |
| 498 | full, | 752 | full, |
| 499 | partial, | 753 | partial, |
| @@ -1469,6 +1723,86 @@ fn makeTestInstances( | |||
| 1469 | return instances; | 1723 | return instances; |
| 1470 | } | 1724 | } |
| 1471 | 1725 | ||
| 1726 | fn runTextCoverageCompare(alloc: std.mem.Allocator) !void { | ||
| 1727 | var conn = try wayland_client.Connection.init(); | ||
| 1728 | defer conn.deinit(); | ||
| 1729 | |||
| 1730 | const window = try conn.createWindow(alloc, "waystty-text-compare"); | ||
| 1731 | defer window.deinit(); | ||
| 1732 | |||
| 1733 | _ = conn.display.roundtrip(); | ||
| 1734 | |||
| 1735 | var font_lookup = try font.lookupConfiguredFont(alloc); | ||
| 1736 | defer font_lookup.deinit(alloc); | ||
| 1737 | |||
| 1738 | var face = try font.Face.init(alloc, font_lookup.path, font_lookup.index, config.font_size_px); | ||
| 1739 | defer face.deinit(); | ||
| 1740 | |||
| 1741 | var atlas = try font.Atlas.init(alloc, 1024, 1024); | ||
| 1742 | defer atlas.deinit(); | ||
| 1743 | |||
| 1744 | var scene = try buildTextCoverageCompareScene(alloc, &face, &atlas); | ||
| 1745 | defer scene.deinit(alloc); | ||
| 1746 | |||
| 1747 | const cell_w = face.cellWidth(); | ||
| 1748 | const cell_h = face.cellHeight(); | ||
| 1749 | window.width = scene.window_cols * cell_w; | ||
| 1750 | window.height = scene.window_rows * cell_h; | ||
| 1751 | |||
| 1752 | var ctx = try renderer.Context.init( | ||
| 1753 | alloc, | ||
| 1754 | @ptrCast(conn.display), | ||
| 1755 | @ptrCast(window.surface), | ||
| 1756 | window.width, | ||
| 1757 | window.height, | ||
| 1758 | ); | ||
| 1759 | defer ctx.deinit(); | ||
| 1760 | |||
| 1761 | try ctx.uploadAtlas(atlas.pixels); | ||
| 1762 | atlas.dirty = false; | ||
| 1763 | try ctx.uploadInstances(scene.instances.items); | ||
| 1764 | |||
| 1765 | var last_window_w = window.width; | ||
| 1766 | var last_window_h = window.height; | ||
| 1767 | |||
| 1768 | while (!window.should_close) { | ||
| 1769 | _ = conn.display.flush(); | ||
| 1770 | if (conn.display.prepareRead()) { | ||
| 1771 | _ = conn.display.readEvents(); | ||
| 1772 | } | ||
| 1773 | _ = conn.display.dispatchPending(); | ||
| 1774 | |||
| 1775 | if (window.width != last_window_w or window.height != last_window_h) { | ||
| 1776 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 1777 | try ctx.recreateSwapchain(window.width, window.height); | ||
| 1778 | last_window_w = window.width; | ||
| 1779 | last_window_h = window.height; | ||
| 1780 | } | ||
| 1781 | |||
| 1782 | drawTextCoverageCompareFrame( | ||
| 1783 | &ctx, | ||
| 1784 | &scene, | ||
| 1785 | cell_w, | ||
| 1786 | cell_h, | ||
| 1787 | .{ 0.0, 0.0, 0.0, 1.0 }, | ||
| 1788 | ) catch |err| switch (err) { | ||
| 1789 | error.OutOfDateKHR => { | ||
| 1790 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 1791 | try ctx.recreateSwapchain(window.width, window.height); | ||
| 1792 | last_window_w = window.width; | ||
| 1793 | last_window_h = window.height; | ||
| 1794 | continue; | ||
| 1795 | }, | ||
| 1796 | else => return err, | ||
| 1797 | }; | ||
| 1798 | |||
| 1799 | _ = conn.display.flush(); | ||
| 1800 | std.Thread.sleep(16 * std.time.ns_per_ms); | ||
| 1801 | } | ||
| 1802 | |||
| 1803 | _ = try ctx.vkd.deviceWaitIdle(ctx.device); | ||
| 1804 | } | ||
| 1805 | |||
| 1472 | fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { | 1806 | fn runDrawSmokeTest(alloc: std.mem.Allocator) !void { |
| 1473 | var conn = try wayland_client.Connection.init(); | 1807 | var conn = try wayland_client.Connection.init(); |
| 1474 | defer conn.deinit(); | 1808 | defer conn.deinit(); |
| @@ -1769,6 +2103,38 @@ test "comparisonSpecimenLines remains fixed and non-empty" { | |||
| 1769 | } | 2103 | } |
| 1770 | } | 2104 | } |
| 1771 | 2105 | ||
| 2106 | test "buildTextCoverageCompareScene repeats the same specimen in four panels" { | ||
| 2107 | var lookup = try font.lookupConfiguredFont(std.testing.allocator); | ||
| 2108 | defer lookup.deinit(std.testing.allocator); | ||
| 2109 | |||
| 2110 | var face = try font.Face.init(std.testing.allocator, lookup.path, lookup.index, config.font_size_px); | ||
| 2111 | defer face.deinit(); | ||
| 2112 | |||
| 2113 | var atlas = try font.Atlas.init(std.testing.allocator, 256, 256); | ||
| 2114 | defer atlas.deinit(); | ||
| 2115 | |||
| 2116 | var scene = try buildTextCoverageCompareScene(std.testing.allocator, &face, &atlas); | ||
| 2117 | defer scene.deinit(std.testing.allocator); | ||
| 2118 | |||
| 2119 | const variants = comparisonVariants(); | ||
| 2120 | const first_draw = scene.panel_draws[0]; | ||
| 2121 | try std.testing.expect(first_draw.instance_count > 0); | ||
| 2122 | |||
| 2123 | var idx: usize = 0; | ||
| 2124 | while (idx < scene.panel_draws.len) : (idx += 1) { | ||
| 2125 | try std.testing.expectEqual(first_draw.instance_count, scene.panel_draws[idx].instance_count); | ||
| 2126 | try std.testing.expectEqualDeep(variants[idx].coverage, scene.panel_draws[idx].coverage); | ||
| 2127 | } | ||
| 2128 | |||
| 2129 | const panel0_first = scene.instances.items[first_draw.instance_offset_instances]; | ||
| 2130 | const panel1_first = scene.instances.items[scene.panel_draws[1].instance_offset_instances]; | ||
| 2131 | try std.testing.expectEqual(panel0_first.cell_pos[1], panel1_first.cell_pos[1]); | ||
| 2132 | try std.testing.expectEqual( | ||
| 2133 | @as(f32, @floatFromInt(scene.panel_cols)), | ||
| 2134 | panel1_first.cell_pos[0] - panel0_first.cell_pos[0], | ||
| 2135 | ); | ||
| 2136 | } | ||
| 2137 | |||
| 1772 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { | 2138 | fn runRenderSmokeTest(alloc: std.mem.Allocator) !void { |
| 1773 | var conn = try wayland_client.Connection.init(); | 2139 | var conn = try wayland_client.Connection.init(); |
| 1774 | defer conn.deinit(); | 2140 | defer conn.deinit(); |