diff --git a/build.zig b/build.zig index dab6d5e..d7a93c1 100644 --- a/build.zig +++ b/build.zig @@ -161,6 +161,13 @@ pub fn build_exe( options.addOption(bool, "use_tree_sitter", use_tree_sitter); options.addOption(bool, "strip", strip); options.addOption(bool, "gui", gui); + if (gui) { + options.addOption(bool, "d2d", b.option( + bool, + "d2d", + "use the Direct2D backend (instead of Direct3D11)", + ) orelse false); + } const options_mod = options.createModule(); @@ -323,6 +330,7 @@ pub fn build_exe( const gui_mod = b.createModule(.{ .root_source_file = b.path("src/win32/gui.zig"), .imports = &.{ + .{ .name = "build_options", .module = options_mod }, .{ .name = "win32", .module = win32_mod }, .{ .name = "ddui", .module = direct2d_dep.module("ddui") }, .{ .name = "cbor", .module = cbor_mod }, diff --git a/src/renderer/win32/renderer.zig b/src/renderer/win32/renderer.zig index fe8f24a..7d0f066 100644 --- a/src/renderer/win32/renderer.zig +++ b/src/renderer/win32/renderer.zig @@ -126,8 +126,8 @@ pub fn fmtmsg(buf: []u8, value: anytype) []const u8 { } pub fn render(self: *Self) error{}!void { - _ = gui.updateScreen(&self.vx.screen); - if (self.hwnd) |hwnd| win32.invalidateHwnd(hwnd); + const hwnd = self.hwnd orelse return; + _ = gui.updateScreen(hwnd, &self.vx.screen); } pub fn stop(self: *Self) void { // this is guaranteed because stop won't be called until after diff --git a/src/win32/DwriteRenderer.zig b/src/win32/DwriteRenderer.zig new file mode 100644 index 0000000..f3e2dea --- /dev/null +++ b/src/win32/DwriteRenderer.zig @@ -0,0 +1,205 @@ +const DwriteRenderer = @This(); + +const std = @import("std"); +const win32 = @import("win32").everything; +const win32ext = @import("win32ext.zig"); + +const dwrite = @import("dwrite.zig"); +const XY = @import("xy.zig").XY; + +staging_texture: StagingTexture = .{}, + +const StagingTexture = struct { + const Cached = struct { + size: XY(u16), + texture: *win32.ID3D11Texture2D, + render_target: *win32.ID2D1RenderTarget, + white_brush: *win32.ID2D1SolidColorBrush, + }; + cached: ?Cached = null, + pub fn update( + self: *StagingTexture, + d3d_device: *win32.ID3D11Device, + d2d_factory: *win32.ID2D1Factory, + size: XY(u16), + ) struct { + texture: *win32.ID3D11Texture2D, + render_target: *win32.ID2D1RenderTarget, + white_brush: *win32.ID2D1SolidColorBrush, + } { + if (self.cached) |cached| { + if (cached.size.eql(size)) return .{ + .texture = cached.texture, + .render_target = cached.render_target, + .white_brush = cached.white_brush, + }; + std.log.debug( + "resizing staging texture from {}x{} to {}x{}", + .{ cached.size.x, cached.size.y, size.x, size.y }, + ); + _ = cached.white_brush.IUnknown.Release(); + _ = cached.render_target.IUnknown.Release(); + _ = cached.texture.IUnknown.Release(); + self.cached = null; + } + + var texture: *win32.ID3D11Texture2D = undefined; + const desc: win32.D3D11_TEXTURE2D_DESC = .{ + .Width = size.x, + .Height = size.y, + .MipLevels = 1, + .ArraySize = 1, + .Format = .B8G8R8A8_UNORM, + .SampleDesc = .{ .Count = 1, .Quality = 0 }, + .Usage = .DEFAULT, + .BindFlags = .{ .RENDER_TARGET = 1 }, + .CPUAccessFlags = .{}, + .MiscFlags = .{}, + }; + { + const hr = d3d_device.CreateTexture2D(&desc, null, &texture); + if (hr < 0) fatalHr("CreateStagingTexture", hr); + } + errdefer _ = texture.IUnknown.Release(); + + const dxgi_surface = win32ext.queryInterface(texture, win32.IDXGISurface); + defer _ = dxgi_surface.IUnknown.Release(); + + var render_target: *win32.ID2D1RenderTarget = undefined; + { + const props = win32.D2D1_RENDER_TARGET_PROPERTIES{ + .type = .DEFAULT, + .pixelFormat = .{ + .format = .B8G8R8A8_UNORM, + .alphaMode = .PREMULTIPLIED, + }, + .dpiX = 0, + .dpiY = 0, + .usage = .{}, + .minLevel = .DEFAULT, + }; + const hr = d2d_factory.CreateDxgiSurfaceRenderTarget( + dxgi_surface, + &props, + &render_target, + ); + if (hr < 0) fatalHr("CreateDxgiSurfaceRenderTarget", hr); + } + errdefer _ = render_target.IUnknown.Release(); + + { + const dc = win32ext.queryInterface(render_target, win32.ID2D1DeviceContext); + defer _ = dc.IUnknown.Release(); + dc.SetUnitMode(win32.D2D1_UNIT_MODE_PIXELS); + } + + var white_brush: *win32.ID2D1SolidColorBrush = undefined; + { + const hr = render_target.CreateSolidColorBrush( + &.{ .r = 1.0, .g = 1.0, .b = 1.0, .a = 1.0 }, + null, + &white_brush, + ); + if (hr < 0) fatalHr("CreateSolidColorBrush", hr); + } + errdefer _ = white_brush.IUnknown.Release(); + + self.cached = .{ + .size = size, + .texture = texture, + .render_target = render_target, + .white_brush = white_brush, + }; + return .{ + .texture = self.cached.?.texture, + .render_target = self.cached.?.render_target, + .white_brush = self.cached.?.white_brush, + }; + } +}; + +pub fn render( + self: *DwriteRenderer, + d3d_device: *win32.ID3D11Device, + d3d_context: *win32.ID3D11DeviceContext, + d2d_factory: *win32.ID2D1Factory, + font: dwrite.Font, + texture: *win32.ID3D11Texture2D, + codepoint: u21, + coord: XY(u16), +) void { + const staging = self.staging_texture.update( + d3d_device, + d2d_factory, + font.cell_size, + ); + + var utf16_buf: [10]u16 = undefined; + + const utf16_len = blk: { + var utf8_buf: [7]u8 = undefined; + const utf8_len: u3 = std.unicode.utf8Encode(codepoint, &utf8_buf) catch |e| std.debug.panic( + "todo: handle invalid codepoint {} (0x{0x}) ({s})", + .{ codepoint, @errorName(e) }, + ); + const utf8 = utf8_buf[0..utf8_len]; + break :blk std.unicode.utf8ToUtf16Le(&utf16_buf, utf8) catch unreachable; + }; + + const utf16 = utf16_buf[0..utf16_len]; + std.debug.assert(utf16.len <= 2); + + { + const rect: win32.D2D_RECT_F = .{ + .left = 0, + .top = 0, + .right = @floatFromInt(font.cell_size.x), + .bottom = @floatFromInt(font.cell_size.y), + }; + staging.render_target.BeginDraw(); + { + const color: win32.D2D_COLOR_F = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; + staging.render_target.Clear(&color); + } + staging.render_target.DrawText( + @ptrCast(utf16.ptr), + @intCast(utf16.len), + font.text_format, + &rect, + &staging.white_brush.ID2D1Brush, + .{}, + .NATURAL, + ); + var tag1: u64 = undefined; + var tag2: u64 = undefined; + const hr = staging.render_target.EndDraw(&tag1, &tag2); + if (hr < 0) std.debug.panic( + "D2D DrawText failed, hresult=0x{x}, tag1={}, tag2={}", + .{ @as(u32, @bitCast(hr)), tag1, tag2 }, + ); + } + + const box: win32.D3D11_BOX = .{ + .left = 0, + .top = 0, + .front = 0, + .right = font.cell_size.x, + .bottom = font.cell_size.y, + .back = 1, + }; + + d3d_context.CopySubresourceRegion( + &texture.ID3D11Resource, + 0, // subresource + coord.x, + coord.y, + 0, // z + &staging.texture.ID3D11Resource, + 0, // subresource + &box, + ); +} + +fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { + std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); +} diff --git a/src/win32/FontFace.zig b/src/win32/FontFace.zig new file mode 100644 index 0000000..6bf2e81 --- /dev/null +++ b/src/win32/FontFace.zig @@ -0,0 +1,31 @@ +const FontFace = @This(); + +const std = @import("std"); + +// it seems that Windows only supports font faces with up to 31 characters +pub const max = 31; + +buf: [max + 1]u16, +len: u5, + +pub fn initUtf8(utf8: []const u8) error{ TooLong, InvalidUtf8 }!FontFace { + const utf16_len = std.unicode.calcUtf16LeLen(utf8) catch return error.InvalidUtf8; + if (utf16_len > max) + return error.TooLong; + var result: FontFace = .{ .buf = undefined, .len = @intCast(utf16_len) }; + result.buf[utf16_len] = 0; + const actual_len = try std.unicode.utf8ToUtf16Le(&result.buf, utf8); + std.debug.assert(actual_len == utf16_len); + return result; +} + +pub fn ptr(self: *const FontFace) [*:0]const u16 { + std.debug.assert(self.buf[@as(usize, self.len)] == 0); + return @ptrCast(&self.buf); +} +pub fn slice(self: *const FontFace) [:0]const u16 { + return self.ptr()[0..self.len :0]; +} +pub fn eql(self: *const FontFace, other: *const FontFace) bool { + return std.mem.eql(u16, self.slice(), other.slice()); +} diff --git a/src/win32/GlyphIndexCache.zig b/src/win32/GlyphIndexCache.zig new file mode 100644 index 0000000..02bc281 --- /dev/null +++ b/src/win32/GlyphIndexCache.zig @@ -0,0 +1,224 @@ +const GlyphIndexCache = @This(); +const std = @import("std"); + +const Node = struct { + prev: ?u32, + next: ?u32, + codepoint: ?u21, +}; + +map: std.AutoHashMapUnmanaged(u21, u32) = .{}, +nodes: []Node, +front: u32, +back: u32, + +pub fn init(allocator: std.mem.Allocator, capacity: u32) error{OutOfMemory}!GlyphIndexCache { + var result: GlyphIndexCache = .{ + .map = .{}, + .nodes = try allocator.alloc(Node, capacity), + .front = undefined, + .back = undefined, + }; + result.clearRetainingCapacity(); + return result; +} + +pub fn clearRetainingCapacity(self: *GlyphIndexCache) void { + self.map.clearRetainingCapacity(); + self.nodes[0] = .{ .prev = null, .next = 1, .codepoint = null }; + self.nodes[self.nodes.len - 1] = .{ .prev = @intCast(self.nodes.len - 2), .next = null, .codepoint = null }; + for (self.nodes[1 .. self.nodes.len - 1], 1..) |*node, index| { + node.* = .{ + .prev = @intCast(index - 1), + .next = @intCast(index + 1), + .codepoint = null, + }; + } + self.front = 0; + self.back = @intCast(self.nodes.len - 1); +} + +pub fn deinit(self: *GlyphIndexCache, allocator: std.mem.Allocator) void { + allocator.free(self.nodes); + self.map.deinit(allocator); +} + +pub fn isFull(self: *const GlyphIndexCache) bool { + return self.map.count() == self.nodes.len; +} + +const Reserved = struct { + index: u32, + replaced: ?u21, +}; +pub fn reserve(self: *GlyphIndexCache, allocator: std.mem.Allocator, codepoint: u21) error{OutOfMemory}!union(enum) { + newly_reserved: Reserved, + already_reserved: u32, +} { + { + const entry = try self.map.getOrPut(allocator, codepoint); + if (entry.found_existing) { + self.moveToBack(entry.value_ptr.*); + return .{ .already_reserved = entry.value_ptr.* }; + } + entry.value_ptr.* = self.front; + } + + std.debug.assert(self.nodes[self.front].prev == null); + std.debug.assert(self.nodes[self.front].next != null); + const replaced = self.nodes[self.front].codepoint; + self.nodes[self.front].codepoint = codepoint; + if (replaced) |r| { + const removed = self.map.remove(r); + std.debug.assert(removed); + } + const save_front = self.front; + self.moveToBack(self.front); + return .{ .newly_reserved = .{ .index = save_front, .replaced = replaced } }; +} + +fn moveToBack(self: *GlyphIndexCache, index: u32) void { + if (index == self.back) return; + + const node = &self.nodes[index]; + if (node.prev) |prev| { + self.nodes[prev].next = node.next; + } else { + self.front = node.next.?; + } + + if (node.next) |next| { + self.nodes[next].prev = node.prev; + } + + self.nodes[self.back].next = index; + node.prev = self.back; + node.next = null; + self.back = index; +} + +fn testValidate(self: *const GlyphIndexCache, seen: []bool) !void { + for (seen) |*s| { + s.* = false; + } + try std.testing.expectEqual(null, self.nodes[self.front].prev); + try std.testing.expect(self.nodes[self.front].next != null); + seen[self.front] = true; + try std.testing.expectEqual(null, self.nodes[self.back].next); + try std.testing.expect(self.nodes[self.back].prev != null); + seen[self.back] = true; + + var index = self.nodes[self.front].next.?; + var count: u32 = 1; + while (index != self.back) : ({ + count += 1; + index = self.nodes[index].next orelse break; + }) { + try std.testing.expect(!seen[index]); + seen[index] = true; + const node = &self.nodes[index]; + try std.testing.expect(node.prev != null); + try std.testing.expect(node.next != null); + try std.testing.expectEqual(index, self.nodes[node.prev.?].next.?); + try std.testing.expectEqual(index, self.nodes[node.next.?].prev.?); + } + try std.testing.expectEqual(self.nodes.len - 1, count); + for (seen) |s| { + try std.testing.expect(s); + } +} + +test "GlyphIndexCache" { + const testing = std.testing; + const allocator = testing.allocator; + + var validation_buf: [3]bool = undefined; + var cache = try GlyphIndexCache.init(allocator, 3); + defer cache.deinit(allocator); + + try cache.testValidate(&validation_buf); + + switch (try cache.reserve(allocator, 'A')) { + .newly_reserved => |reserved| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(0, reserved.index); + try testing.expectEqual(null, reserved.replaced); + try testing.expectEqual(reserved.index, cache.back); + try testing.expect(!cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + switch (try cache.reserve(allocator, 'A')) { + .already_reserved => |index| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(0, index); + try testing.expectEqual(index, cache.back); + try testing.expect(!cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + + switch (try cache.reserve(allocator, 'B')) { + .newly_reserved => |reserved| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(1, reserved.index); + try testing.expectEqual(null, reserved.replaced); + try testing.expectEqual(reserved.index, cache.back); + try testing.expect(!cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + switch (try cache.reserve(allocator, 'A')) { + .already_reserved => |index| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(0, index); + try testing.expectEqual(index, cache.back); + try testing.expectEqual(2, cache.front); + try testing.expect(!cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + + for (0..6) |run| { + const index: u32 = @intCast(run % 2); + cache.moveToBack(index); + try cache.testValidate(&validation_buf); + try testing.expectEqual(index, cache.back); + try testing.expectEqual(2, cache.front); + try testing.expect(!cache.isFull()); + } + + switch (try cache.reserve(allocator, 'C')) { + .newly_reserved => |reserved| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(2, reserved.index); + try testing.expectEqual(null, reserved.replaced); + try testing.expectEqual(reserved.index, cache.back); + try testing.expect(cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + + for (0..10) |run| { + const index: u32 = @intCast(run % 3); + cache.moveToBack(index); + try cache.testValidate(&validation_buf); + try testing.expectEqual(index, cache.back); + try testing.expect(cache.isFull()); + } + + { + const expected_index = cache.front; + const expected_replaced = cache.nodes[cache.front].codepoint.?; + switch (try cache.reserve(allocator, 'D')) { + .newly_reserved => |reserved| { + try cache.testValidate(&validation_buf); + try testing.expectEqual(expected_index, reserved.index); + try testing.expectEqual(expected_replaced, reserved.replaced.?); + try testing.expectEqual(reserved.index, cache.back); + try testing.expect(cache.isFull()); + }, + else => return error.TestUnexpectedResult, + } + } +} diff --git a/src/win32/d2d.zig b/src/win32/d2d.zig new file mode 100644 index 0000000..e0b49b1 --- /dev/null +++ b/src/win32/d2d.zig @@ -0,0 +1,250 @@ +const std = @import("std"); +const win32 = @import("win32").everything; +const win32ext = @import("win32ext.zig"); +const ddui = @import("ddui"); +const vaxis = @import("vaxis"); +const dwrite = @import("dwrite.zig"); + +const RGB = @import("color").RGB; +const xterm = @import("xterm.zig"); +const XY = @import("xy.zig").XY; + +pub const Font = dwrite.Font; + +pub const NOREDIRECTIONBITMAP = 0; + +const global = struct { + var init_called: bool = false; + var d2d_factory: *win32.ID2D1Factory = undefined; + var background: win32.D2D_COLOR_F = .{ .r = 0.075, .g = 0.075, .b = 0.075, .a = 1.0 }; +}; + +pub fn init() void { + std.debug.assert(!global.init_called); + global.init_called = true; + dwrite.init(); + { + const hr = win32.D2D1CreateFactory( + .SINGLE_THREADED, + win32.IID_ID2D1Factory, + null, + @ptrCast(&global.d2d_factory), + ); + if (hr < 0) fatalHr("D2D1CreateFactory", hr); + } +} + +pub fn setBackground(state: *const WindowState, rgb: RGB) void { + _ = state; + global.background = ddui.rgb8(rgb.r, rgb.g, rgb.b); +} + +pub const WindowState = struct { + maybe_d2d: ?D2d = null, + pub fn init(hwnd: win32.HWND) WindowState { + _ = hwnd; + return .{}; + } +}; + +const D2d = struct { + target: *win32.ID2D1HwndRenderTarget, + brush: *win32.ID2D1SolidColorBrush, + pub fn init(hwnd: win32.HWND, err: *ddui.HResultError) error{HResult}!D2d { + var target: *win32.ID2D1HwndRenderTarget = undefined; + const target_props = win32.D2D1_RENDER_TARGET_PROPERTIES{ + .type = .DEFAULT, + .pixelFormat = .{ + .format = .B8G8R8A8_UNORM, + .alphaMode = .PREMULTIPLIED, + }, + .dpiX = 0, + .dpiY = 0, + .usage = .{}, + .minLevel = .DEFAULT, + }; + const hwnd_target_props = win32.D2D1_HWND_RENDER_TARGET_PROPERTIES{ + .hwnd = hwnd, + .pixelSize = .{ .width = 0, .height = 0 }, + .presentOptions = .{}, + }; + + { + const hr = global.d2d_factory.CreateHwndRenderTarget( + &target_props, + &hwnd_target_props, + &target, + ); + if (hr < 0) return err.set(hr, "CreateHwndRenderTarget"); + } + errdefer _ = target.IUnknown.Release(); + + { + const dc = win32ext.queryInterface(target, win32.ID2D1DeviceContext); + defer _ = dc.IUnknown.Release(); + // just make everything DPI aware, all applications should just do this + dc.SetUnitMode(win32.D2D1_UNIT_MODE_PIXELS); + } + + var brush: *win32.ID2D1SolidColorBrush = undefined; + { + const color: win32.D2D_COLOR_F = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; + const hr = target.ID2D1RenderTarget.CreateSolidColorBrush(&color, null, &brush); + if (hr < 0) return err.set(hr, "CreateSolidBrush"); + } + errdefer _ = brush.IUnknown.Release(); + + return .{ + .target = target, + .brush = brush, + }; + } + pub fn deinit(self: *D2d) void { + _ = self.brush.IUnknown.Release(); + _ = self.target.IUnknown.Release(); + } + pub fn solid(self: *const D2d, color: win32.D2D_COLOR_F) *win32.ID2D1Brush { + self.brush.SetColor(&color); + return &self.brush.ID2D1Brush; + } +}; + +pub fn paint( + hwnd: win32.HWND, + state: *WindowState, + font: Font, + screen: *const vaxis.Screen, +) void { + const client_size = getClientSize(hwnd); + + const err: ddui.HResultError = blk: { + var ps: win32.PAINTSTRUCT = undefined; + _ = win32.BeginPaint(hwnd, &ps) orelse return fatalWin32( + "BeginPaint", + win32.GetLastError(), + ); + defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32( + "EndPaint", + win32.GetLastError(), + ); + + if (state.maybe_d2d == null) { + var err: ddui.HResultError = undefined; + state.maybe_d2d = D2d.init(hwnd, &err) catch break :blk err; + } + + { + const size: win32.D2D_SIZE_U = .{ + .width = @intCast(client_size.x), + .height = @intCast(client_size.y), + }; + const hr = state.maybe_d2d.?.target.Resize(&size); + if (hr < 0) break :blk ddui.HResultError{ .context = "D2dResize", .hr = hr }; + } + + state.maybe_d2d.?.target.ID2D1RenderTarget.BeginDraw(); + paintD2d(&state.maybe_d2d.?, screen, font); + break :blk ddui.HResultError{ + .context = "D2dEndDraw", + .hr = state.maybe_d2d.?.target.ID2D1RenderTarget.EndDraw(null, null), + }; + }; + + if (err.hr == win32.D2DERR_RECREATE_TARGET) { + std.log.debug("D2DERR_RECREATE_TARGET", .{}); + state.maybe_d2d.?.deinit(); + state.maybe_d2d = null; + win32.invalidateHwnd(hwnd); + } else if (err.hr < 0) std.debug.panic("paint error: {}", .{err}); +} + +fn paintD2d( + d2d: *const D2d, + screen: *const vaxis.Screen, + font: Font, +) void { + d2d.target.ID2D1RenderTarget.Clear(&global.background); + for (0..screen.height) |y| { + const row_y: i32 = font.cell_size.y * @as(i32, @intCast(y)); + for (0..screen.width) |x| { + const column_x: i32 = font.cell_size.x * @as(i32, @intCast(x)); + const cell_index = screen.width * y + x; + const cell = &screen.buf[cell_index]; + + const cell_rect: win32.RECT = .{ + .left = column_x, + .top = row_y, + .right = column_x + font.cell_size.x, + .bottom = row_y + font.cell_size.y, + }; + ddui.FillRectangle( + &d2d.target.ID2D1RenderTarget, + cell_rect, + d2d.solid(d2dColorFromVAxis(cell.style.bg)), + ); + + // TODO: pre-caclulate the buffer size needed, for now this should just + // cause out-of-bounds access + var buf_wtf16: [100]u16 = undefined; + const grapheme_len = blk: { + break :blk std.unicode.wtf8ToWtf16Le(&buf_wtf16, cell.char.grapheme) catch |err| switch (err) { + error.InvalidWtf8 => { + buf_wtf16[0] = std.unicode.replacement_character; + break :blk 1; + }, + }; + }; + const grapheme = buf_wtf16[0..grapheme_len]; + if (std.mem.eql(u16, grapheme, &[_]u16{' '})) + continue; + ddui.DrawText( + &d2d.target.ID2D1RenderTarget, + grapheme, + font.text_format, + ddui.rectFloatFromInt(cell_rect), + d2d.solid(d2dColorFromVAxis(cell.style.fg)), + .{ + .CLIP = 1, + .ENABLE_COLOR_FONT = 1, + }, + .NATURAL, + ); + } + } +} + +fn d2dColorFromVAxis(color: vaxis.Cell.Color) win32.D2D_COLOR_F { + return switch (color) { + .default => .{ .r = 0, .g = 0, .b = 0, .a = 0 }, + .index => |idx| blk: { + const rgb = RGB.from_u24(xterm.colors[idx]); + break :blk .{ + .r = @as(f32, @floatFromInt(rgb.r)) / 255.0, + .g = @as(f32, @floatFromInt(rgb.g)) / 255.0, + .b = @as(f32, @floatFromInt(rgb.b)) / 255.0, + .a = 1, + }; + }, + .rgb => |rgb| .{ + .r = @as(f32, @floatFromInt(rgb[0])) / 255.0, + .g = @as(f32, @floatFromInt(rgb[1])) / 255.0, + .b = @as(f32, @floatFromInt(rgb[2])) / 255.0, + .a = 1, + }, + }; +} + +fn getClientSize(hwnd: win32.HWND) XY(i32) { + var rect: win32.RECT = undefined; + if (0 == win32.GetClientRect(hwnd, &rect)) + fatalWin32("GetClientRect", win32.GetLastError()); + std.debug.assert(rect.left == 0); + std.debug.assert(rect.top == 0); + return .{ .x = rect.right, .y = rect.bottom }; +} +fn fatalWin32(what: []const u8, err: win32.WIN32_ERROR) noreturn { + std.debug.panic("{s} failed with {}", .{ what, err.fmt() }); +} +fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { + std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); +} diff --git a/src/win32/d3d11.zig b/src/win32/d3d11.zig new file mode 100644 index 0000000..ddc791c --- /dev/null +++ b/src/win32/d3d11.zig @@ -0,0 +1,801 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const win32 = @import("win32").everything; +const win32ext = @import("win32ext.zig"); +const vaxis = @import("vaxis"); + +const dwrite = @import("dwrite.zig"); +const GlyphIndexCache = @import("GlyphIndexCache.zig"); +const TextRenderer = @import("DwriteRenderer.zig"); + +const RGB = @import("color").RGB; +const xterm = @import("xterm.zig"); +const XY = @import("xy.zig").XY; + +pub const Font = dwrite.Font; + +const log = std.log.scoped(.d3d); + +// the redirection bitmap is unnecessary for a d3d window and causes +// bad artifacts when the window is resized +pub const NOREDIRECTIONBITMAP = 1; + +const global = struct { + var init_called: bool = false; + var d3d: D3d = undefined; + var shaders: Shaders = undefined; + var const_buf: *win32.ID3D11Buffer = undefined; + var d2d_factory: *win32.ID2D1Factory = undefined; + var glyph_cache_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var text_renderer: TextRenderer = undefined; + var background: Rgba8 = .{ .r = 19, .g = 19, .b = 19, .a = 255 }; +}; + +pub const Color = Rgba8; +const Rgba8 = packed struct(u32) { + a: u8, + b: u8, + g: u8, + r: u8, + pub fn initRgb(r: u8, g: u8, b: u8) Color { + return .{ .r = r, .g = g, .b = b, .a = 255 }; + } +}; + +// types shared with the shader +const shader = struct { + const GridConfig = extern struct { + cell_size: [2]u32, + col_count: u32, + row_count: u32, + }; + const Cell = extern struct { + glyph_index: u32, + background: Rgba8, + foreground: Rgba8, + }; +}; + +const swap_chain_flags: u32 = @intFromEnum(win32.DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT); + +pub fn init() void { + std.debug.assert(!global.init_called); + global.init_called = true; + dwrite.init(); + + const debug_d3d = switch (builtin.mode) { + .Debug => true, + else => false, + }; + global.d3d = D3d.init(.{ .debug = debug_d3d }); + + if (debug_d3d) { + const info = win32ext.queryInterface(global.d3d.device, win32.ID3D11InfoQueue); + defer _ = info.IUnknown.Release(); + { + const hr = info.SetBreakOnSeverity(.CORRUPTION, 1); + if (hr < 0) fatalHr("SetBreakOnCorruption", hr); + } + { + const hr = info.SetBreakOnSeverity(.ERROR, 1); + if (hr < 0) fatalHr("SetBreakOnError", hr); + } + { + const hr = info.SetBreakOnSeverity(.WARNING, 1); + if (hr < 0) fatalHr("SetBreakOnWarning", hr); + } + } + + global.shaders = Shaders.init(); + + { + const desc: win32.D3D11_BUFFER_DESC = .{ + // d3d requires constants be sized in multiples of 16 + .ByteWidth = std.mem.alignForward(u32, @sizeOf(shader.GridConfig), 16), + .Usage = .DYNAMIC, + .BindFlags = .{ .CONSTANT_BUFFER = 1 }, + .CPUAccessFlags = .{ .WRITE = 1 }, + .MiscFlags = .{}, + .StructureByteStride = 0, + }; + const hr = global.d3d.device.CreateBuffer(&desc, null, &global.const_buf); + if (hr < 0) fatalHr("CreateBuffer for grid config", hr); + } + + { + const hr = win32.D2D1CreateFactory( + .SINGLE_THREADED, + win32.IID_ID2D1Factory, + null, + @ptrCast(&global.d2d_factory), + ); + if (hr < 0) fatalHr("D2D1CreateFactory", hr); + } +} + +pub fn setBackground(state: *const WindowState, rgb: RGB) void { + global.background = .{ .r = rgb.r, .g = rgb.b, .b = rgb.b, .a = 255 }; + const color: win32.DXGI_RGBA = .{ + .r = @as(f32, @floatFromInt(rgb.r)) / 255, + .g = @as(f32, @floatFromInt(rgb.g)) / 255, + .b = @as(f32, @floatFromInt(rgb.b)) / 255, + .a = 1.0, + }; + const hr = state.swap_chain.IDXGISwapChain1.SetBackgroundColor(&color); + if (hr < 0) fatalHr("SetBackgroundColor", hr); +} + +pub const WindowState = struct { + swap_chain: *win32.IDXGISwapChain2, + maybe_target_view: ?*win32.ID3D11RenderTargetView = null, + shader_cells: ShaderCells = .{}, + + glyph_texture: GlyphTexture = .{}, + glyph_cache_cell_size: ?XY(u16) = null, + glyph_index_cache: ?GlyphIndexCache = null, + + pub fn init(hwnd: win32.HWND) WindowState { + std.debug.assert(global.init_called); + const swap_chain = initSwapChain(global.d3d.device, hwnd); + return .{ .swap_chain = swap_chain }; + } +}; + +pub fn paint( + hwnd: win32.HWND, + state: *WindowState, + font: Font, + screen: *const vaxis.Screen, +) void { + var ps: win32.PAINTSTRUCT = undefined; + _ = win32.BeginPaint(hwnd, &ps) orelse fatalWin32("BeginPaint", win32.GetLastError()); + defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32("EndPaint", win32.GetLastError()); + + const client_size = getClientSize(u32, hwnd); + + { + const swap_chain_size = getSwapChainSize(state.swap_chain); + if (swap_chain_size.x != client_size.x or swap_chain_size.y != client_size.y) { + log.debug( + "SwapChain Buffer Resize from {}x{} to {}x{}", + .{ swap_chain_size.x, swap_chain_size.y, client_size.x, client_size.y }, + ); + global.d3d.context.ClearState(); + if (state.maybe_target_view) |target_view| { + _ = target_view.IUnknown.Release(); + state.maybe_target_view = null; + } + global.d3d.context.Flush(); + if (swap_chain_size.x == 0) @panic("possible? no need to resize?"); + if (swap_chain_size.y == 0) @panic("possible? no need to resize?"); + + { + const hr = state.swap_chain.IDXGISwapChain.ResizeBuffers( + 0, + @intCast(client_size.x), + @intCast(client_size.y), + .UNKNOWN, + swap_chain_flags, + ); + if (hr < 0) fatalHr("ResizeBuffers", hr); + } + } + } + + // for now we'll just use 1 texture and leverage the entire thing + const texture_cell_count: XY(u16) = getD3d11TextureMaxCellCount(font.cell_size); + const texture_cell_count_total: u32 = + @as(u32, texture_cell_count.x) * @as(u32, texture_cell_count.y); + + const texture_pixel_size: XY(u16) = .{ + .x = texture_cell_count.x * font.cell_size.x, + .y = texture_cell_count.y * font.cell_size.y, + }; + const texture_retained = switch (state.glyph_texture.updateSize(texture_pixel_size)) { + .retained => true, + .newly_created => false, + }; + + const cache_cell_size_valid = if (state.glyph_cache_cell_size) |size| size.eql(font.cell_size) else false; + state.glyph_cache_cell_size = font.cell_size; + + if (!texture_retained or !cache_cell_size_valid) { + if (state.glyph_index_cache) |*c| { + c.deinit(global.glyph_cache_arena.allocator()); + _ = global.glyph_cache_arena.reset(.retain_capacity); + state.glyph_index_cache = null; + } + } + + const glyph_index_cache = blk: { + if (state.glyph_index_cache) |*c| break :blk c; + state.glyph_index_cache = GlyphIndexCache.init( + global.glyph_cache_arena.allocator(), + texture_cell_count_total, + ) catch |e| oom(e); + break :blk &(state.glyph_index_cache.?); + }; + + const shader_col_count: u16 = @intCast(@divTrunc(client_size.x + font.cell_size.x - 1, font.cell_size.x)); + const shader_row_count: u16 = @intCast(@divTrunc(client_size.y + font.cell_size.y - 1, font.cell_size.y)); + + { + var mapped: win32.D3D11_MAPPED_SUBRESOURCE = undefined; + const hr = global.d3d.context.Map( + &global.const_buf.ID3D11Resource, + 0, + .WRITE_DISCARD, + 0, + &mapped, + ); + if (hr < 0) fatalHr("MapConstBuffer", hr); + defer global.d3d.context.Unmap(&global.const_buf.ID3D11Resource, 0); + const config: *shader.GridConfig = @ptrCast(@alignCast(mapped.pData)); + config.cell_size[0] = font.cell_size.x; + config.cell_size[1] = font.cell_size.y; + config.col_count = shader_col_count; + config.row_count = shader_row_count; + } + + const space_glyph = generateGlyph( + font, + glyph_index_cache, + texture_cell_count.x, + " ", + state.glyph_texture.obj, + ); + const populate_col_count: u16 = @min(screen.width, shader_col_count); + const populate_row_count: u16 = @min(screen.height, shader_row_count); + // we loop through and cache all the glyphs before mapping the cell buffer and potentially + // blocking the gpu while we're doing expensive text rendering + for (0..populate_row_count) |row| { + const row_offset = row * screen.width; + for (0..populate_col_count) |col| { + const screen_cell = &screen.buf[row_offset + col]; + _ = generateGlyph( + font, + glyph_index_cache, + texture_cell_count.x, + screen_cell.char.grapheme, + state.glyph_texture.obj, + ); + } + } + + const cell_count: u32 = @as(u32, shader_col_count) * @as(u32, shader_row_count); + state.shader_cells.updateCount(cell_count); + if (state.shader_cells.count > 0) { + var mapped: win32.D3D11_MAPPED_SUBRESOURCE = undefined; + const hr = global.d3d.context.Map( + &state.shader_cells.cell_buf.ID3D11Resource, + 0, + .WRITE_DISCARD, + 0, + &mapped, + ); + if (hr < 0) fatalHr("MapCellBuffer", hr); + defer global.d3d.context.Unmap(&state.shader_cells.cell_buf.ID3D11Resource, 0); + + const cells_shader: [*]shader.Cell = @ptrCast(@alignCast(mapped.pData)); + for (0..shader_row_count) |row| { + const src_row_offset = row * screen.width; + const dst_row_offset = row * @as(usize, shader_col_count); + const src_col_count = if (row < screen.height) populate_col_count else 0; + for (0..src_col_count) |col| { + const screen_cell = &screen.buf[src_row_offset + col]; + const codepoint = std.unicode.wtf8Decode(screen_cell.char.grapheme) catch std.unicode.replacement_character; + const glyph_index = blk: { + switch (glyph_index_cache.reserve(global.glyph_cache_arena.allocator(), codepoint) catch |e| oom(e)) { + .newly_reserved => |reserved| { + // should never happen unless there' more characters than the cache can hold + // var render_success = false; + // defer if (!render_success) state.glyph_index_cache.remove(reserved.index); + const pos: XY(u16) = cellPosFromIndex(reserved.index, texture_cell_count.x); + const coord = coordFromCellPos(font.cell_size, pos); + global.text_renderer.render( + global.d3d.device, + global.d3d.context, + global.d2d_factory, + font, + state.glyph_texture.obj, + codepoint, + coord, + ); + break :blk reserved.index; + }, + .already_reserved => |i| break :blk i, + } + }; + cells_shader[dst_row_offset + col] = .{ + .glyph_index = glyph_index, + .background = shaderColorFromVaxis(screen_cell.style.bg), + .foreground = shaderColorFromVaxis(screen_cell.style.fg), + }; + } + for (src_col_count..shader_col_count) |col| { + cells_shader[dst_row_offset + col] = .{ + .glyph_index = space_glyph, + .background = global.background, + .foreground = global.background, + }; + } + } + } + + if (state.maybe_target_view == null) { + state.maybe_target_view = createRenderTargetView( + global.d3d.device, + &state.swap_chain.IDXGISwapChain, + client_size, + ); + } + { + var target_views = [_]?*win32.ID3D11RenderTargetView{state.maybe_target_view.?}; + global.d3d.context.OMSetRenderTargets(target_views.len, &target_views, null); + } + + global.d3d.context.PSSetConstantBuffers(0, 1, @constCast(@ptrCast(&global.const_buf))); + var resources = [_]?*win32.ID3D11ShaderResourceView{ + if (state.shader_cells.count > 0) state.shader_cells.cell_view else null, + state.glyph_texture.view, + }; + global.d3d.context.PSSetShaderResources(0, resources.len, &resources); + global.d3d.context.VSSetShader(global.shaders.vertex, null, 0); + global.d3d.context.PSSetShader(global.shaders.pixel, null, 0); + global.d3d.context.Draw(4, 0); + + // NOTE: don't enable vsync, it causes the gpu to lag behind horribly + // if we flood it with resize events + { + const hr = state.swap_chain.IDXGISwapChain.Present(0, 0); + if (hr < 0) fatalHr("SwapChainPresent", hr); + } +} + +fn generateGlyph( + font: Font, + glyph_index_cache: *GlyphIndexCache, + texture_column_count: u16, + grapheme_utf8: []const u8, + texture: *win32.ID3D11Texture2D, +) u32 { + const codepoint = std.unicode.wtf8Decode(grapheme_utf8) catch std.unicode.replacement_character; + switch (glyph_index_cache.reserve( + global.glyph_cache_arena.allocator(), + codepoint, + ) catch |e| oom(e)) { + .newly_reserved => |reserved| { + // var render_success = false; + // defer if (!render_success) state.glyph_index_cache.remove(reserved.index); + const pos: XY(u16) = cellPosFromIndex(reserved.index, texture_column_count); + const coord = coordFromCellPos(font.cell_size, pos); + global.text_renderer.render( + global.d3d.device, + global.d3d.context, + global.d2d_factory, + font, + texture, + codepoint, + coord, + ); + return reserved.index; + }, + .already_reserved => |index| return index, + } +} + +fn shaderColorFromVaxis(color: vaxis.Color) Rgba8 { + return switch (color) { + .default => .{ .r = 0, .g = 0, .b = 0, .a = 255 }, + .index => |idx| return @bitCast(@as(u32, xterm.colors[idx]) << 8 | 0xff), + .rgb => |rgb| .{ .r = rgb[0], .g = rgb[1], .b = rgb[2], .a = 255 }, + }; +} + +const D3d = struct { + device: *win32.ID3D11Device, + context: *win32.ID3D11DeviceContext, + context1: *win32.ID3D11DeviceContext1, + + pub fn init(opt: struct { debug: bool }) D3d { + const levels = [_]win32.D3D_FEATURE_LEVEL{ + .@"11_0", + }; + var last_hr: i32 = undefined; + for (&[_]win32.D3D_DRIVER_TYPE{ .HARDWARE, .WARP }) |driver| { + var device: *win32.ID3D11Device = undefined; + var context: *win32.ID3D11DeviceContext = undefined; + last_hr = win32.D3D11CreateDevice( + null, + driver, + null, + .{ + .BGRA_SUPPORT = 1, + .SINGLETHREADED = 1, + .DEBUG = if (opt.debug) 1 else 0, + }, + &levels, + levels.len, + win32.D3D11_SDK_VERSION, + &device, + null, + &context, + ); + if (last_hr >= 0) return .{ + .device = device, + .context = context, + .context1 = win32ext.queryInterface(context, win32.ID3D11DeviceContext1), + }; + std.log.info( + "D3D11 {s} Driver error, hresult=0x{x}", + .{ @tagName(driver), @as(u32, @bitCast(last_hr)) }, + ); + } + std.debug.panic("failed to initialize Direct3D11, hresult=0x{x}", .{last_hr}); + } +}; + +fn getDxgiFactory(device: *win32.ID3D11Device) *win32.IDXGIFactory2 { + const dxgi_device = win32ext.queryInterface(device, win32.IDXGIDevice); + defer _ = dxgi_device.IUnknown.Release(); + + var adapter: *win32.IDXGIAdapter = undefined; + { + const hr = dxgi_device.GetAdapter(&adapter); + if (hr < 0) fatalHr("GetDxgiAdapter", hr); + } + defer _ = adapter.IUnknown.Release(); + + var factory: *win32.IDXGIFactory2 = undefined; + { + const hr = adapter.IDXGIObject.GetParent(win32.IID_IDXGIFactory2, @ptrCast(&factory)); + if (hr < 0) fatalHr("GetDxgiFactory", hr); + } + return factory; +} + +fn getSwapChainSize(swap_chain: *win32.IDXGISwapChain2) XY(u32) { + var size: XY(u32) = undefined; + { + const hr = swap_chain.GetSourceSize(&size.x, &size.y); + if (hr < 0) fatalHr("GetSwapChainSourceSize", hr); + } + return size; +} + +fn initSwapChain( + device: *win32.ID3D11Device, + hwnd: win32.HWND, +) *win32.IDXGISwapChain2 { + const factory = getDxgiFactory(device); + defer _ = factory.IUnknown.Release(); + + const swap_chain1: *win32.IDXGISwapChain1 = blk: { + var swap_chain1: *win32.IDXGISwapChain1 = undefined; + const desc = win32.DXGI_SWAP_CHAIN_DESC1{ + .Width = 0, + .Height = 0, + .Format = .B8G8R8A8_UNORM, + .Stereo = 0, + .SampleDesc = .{ .Count = 1, .Quality = 0 }, + .BufferUsage = win32.DXGI_USAGE_RENDER_TARGET_OUTPUT, + .BufferCount = 2, + .Scaling = .NONE, + .SwapEffect = .FLIP_DISCARD, + .AlphaMode = .IGNORE, + .Flags = swap_chain_flags, + }; + { + const hr = factory.CreateSwapChainForHwnd( + &device.IUnknown, + hwnd, + &desc, + null, + null, + &swap_chain1, + ); + if (hr < 0) fatalHr("CreateD3dSwapChain", hr); + } + break :blk swap_chain1; + }; + defer _ = swap_chain1.IUnknown.Release(); + + { + const color: win32.DXGI_RGBA = .{ .r = 0.075, .g = 0.075, .b = 0.075, .a = 1.0 }; + const hr = swap_chain1.SetBackgroundColor(&color); + if (hr < 0) fatalHr("SetBackgroundColor", hr); + } + + var swap_chain2: *win32.IDXGISwapChain2 = undefined; + { + const hr = swap_chain1.IUnknown.QueryInterface(win32.IID_IDXGISwapChain2, @ptrCast(&swap_chain2)); + if (hr < 0) fatalHr("QuerySwapChain2", hr); + } + + // refterm is doing this but I don't know why + if (false) { + const hr = factory.IDXGIFactory.MakeWindowAssociation(hwnd, 0); //DXGI_MWA_NO_ALT_ENTER | DXGI_MWA_NO_WINDOW_CHANGES); + if (hr < 0) fatalHr("MakeWindowAssoc", hr); + } + + return swap_chain2; +} + +const Shaders = struct { + vertex: *win32.ID3D11VertexShader, + pixel: *win32.ID3D11PixelShader, + pub fn init() Shaders { + const shader_source = @embedFile("terminal.hlsl"); + + var vs_blob: *win32.ID3DBlob = undefined; + var error_blob: ?*win32.ID3DBlob = null; + { + const hr = win32.D3DCompile( + shader_source.ptr, + shader_source.len, + null, + null, + null, + "VertexMain", + "vs_5_0", + 0, + 0, + @ptrCast(&vs_blob), + @ptrCast(&error_blob), + ); + reportShaderError(.vertex, error_blob); + error_blob = null; + if (hr < 0) { + fatalHr("D3DCompileVertexShader", hr); + } + } + defer _ = vs_blob.IUnknown.Release(); + var ps_blob: *win32.ID3DBlob = undefined; + { + const hr = win32.D3DCompile( + shader_source.ptr, + shader_source.len, + null, + null, + null, + "PixelMain", + "ps_5_0", + 0, + 0, + @ptrCast(&ps_blob), + @ptrCast(&error_blob), + ); + reportShaderError(.pixel, error_blob); + error_blob = null; + if (hr < 0) { + fatalHr("D3DCompilePixelShader", hr); + } + } + defer _ = ps_blob.IUnknown.Release(); + + var vertex_shader: *win32.ID3D11VertexShader = undefined; + { + const hr = global.d3d.device.CreateVertexShader( + @ptrCast(vs_blob.GetBufferPointer()), + vs_blob.GetBufferSize(), + null, + &vertex_shader, + ); + if (hr < 0) fatalHr("CreateVertexShader", hr); + } + errdefer vertex_shader.IUnknown.Release(); + + var pixel_shader: *win32.ID3D11PixelShader = undefined; + { + const hr = global.d3d.device.CreatePixelShader( + @ptrCast(ps_blob.GetBufferPointer()), + ps_blob.GetBufferSize(), + null, + &pixel_shader, + ); + if (hr < 0) fatalHr("CreatePixelShader", hr); + } + errdefer pixel_shader.IUnknown.Release(); + + return .{ + .vertex = vertex_shader, + .pixel = pixel_shader, + }; + } +}; +fn reportShaderError(kind: enum { vertex, pixel }, maybe_error_blob: ?*win32.ID3DBlob) void { + const err = maybe_error_blob orelse return; + defer _ = err.IUnknown.Release(); + const ptr: [*]const u8 = @ptrCast(err.GetBufferPointer() orelse return); + const str = ptr[0..err.GetBufferSize()]; + log.err("{s} shader error:\n{s}\n", .{ @tagName(kind), str }); + std.debug.panic("{s} shader error:\n{s}\n", .{ @tagName(kind), str }); +} + +const ShaderCells = struct { + count: u32 = 0, + cell_buf: *win32.ID3D11Buffer = undefined, + cell_view: *win32.ID3D11ShaderResourceView = undefined, + pub fn updateCount(self: *ShaderCells, count: u32) void { + if (count == self.count) return; + + log.debug("CellCount {} > {}", .{ self.count, count }); + if (self.count != 0) { + _ = self.cell_view.IUnknown.Release(); + _ = self.cell_buf.IUnknown.Release(); + self.count = 0; + } + + if (count > 0) { + self.cell_buf = createCellBuffer(global.d3d.device, count); + errdefer { + self.cell_buf.IUnknown.Release(); + self.cell_buf = undefined; + } + + { + const desc: win32.D3D11_SHADER_RESOURCE_VIEW_DESC = .{ + .Format = .UNKNOWN, + .ViewDimension = ._SRV_DIMENSION_BUFFER, + .Anonymous = .{ + .Buffer = .{ + .Anonymous1 = .{ .FirstElement = 0 }, + .Anonymous2 = .{ .NumElements = count }, + }, + }, + }; + const hr = global.d3d.device.CreateShaderResourceView( + &self.cell_buf.ID3D11Resource, + &desc, + &self.cell_view, + ); + if (hr < 0) fatalHr("CreateShaderResourceView for cells", hr); + } + } + self.count = count; + } +}; + +const GlyphTexture = struct { + size: ?XY(u16) = null, + obj: *win32.ID3D11Texture2D = undefined, + view: *win32.ID3D11ShaderResourceView = undefined, + pub fn updateSize(self: *GlyphTexture, size: XY(u16)) enum { retained, newly_created } { + if (self.size) |existing_size| { + if (existing_size.eql(size)) return .retained; + + _ = self.view.IUnknown.Release(); + self.view = undefined; + _ = self.obj.IUnknown.Release(); + self.obj = undefined; + self.size = null; + } + log.debug("GlyphTexture: init {}x{}", .{ size.x, size.y }); + + { + const desc: win32.D3D11_TEXTURE2D_DESC = .{ + .Width = size.x, + .Height = size.y, + .MipLevels = 1, + .ArraySize = 1, + .Format = .B8G8R8A8_UNORM, + .SampleDesc = .{ .Count = 1, .Quality = 0 }, + .Usage = .DEFAULT, + .BindFlags = .{ .SHADER_RESOURCE = 1 }, + .CPUAccessFlags = .{}, + .MiscFlags = .{}, + }; + const hr = global.d3d.device.CreateTexture2D(&desc, null, &self.obj); + if (hr < 0) fatalHr("CreateGlyphTexture", hr); + } + errdefer { + self.obj.IUnknown.Release(); + self.obj = undefined; + } + + { + const hr = global.d3d.device.CreateShaderResourceView( + &self.obj.ID3D11Resource, + null, + &self.view, + ); + if (hr < 0) fatalHr("CreateGlyphView", hr); + } + self.size = size; + return .newly_created; + } +}; + +fn createRenderTargetView( + device: *win32.ID3D11Device, + swap_chain: *win32.IDXGISwapChain, + size: XY(u32), +) *win32.ID3D11RenderTargetView { + var back_buffer: *win32.ID3D11Texture2D = undefined; + + { + const hr = swap_chain.GetBuffer(0, win32.IID_ID3D11Texture2D, @ptrCast(&back_buffer)); + if (hr < 0) fatalHr("SwapChainGetBuffer", hr); + } + defer _ = back_buffer.IUnknown.Release(); + + var target_view: *win32.ID3D11RenderTargetView = undefined; + { + const hr = device.CreateRenderTargetView(&back_buffer.ID3D11Resource, null, &target_view); + if (hr < 0) fatalHr("CreateRenderTargetView", hr); + } + + { + var viewport = win32.D3D11_VIEWPORT{ + .TopLeftX = 0, + .TopLeftY = 0, + .Width = @floatFromInt(size.x), + .Height = @floatFromInt(size.y), + .MinDepth = 0.0, + .MaxDepth = 0.0, + }; + global.d3d.context.RSSetViewports(1, @ptrCast(&viewport)); + } + // TODO: is this the right place to put this? + global.d3d.context.IASetPrimitiveTopology(._PRIMITIVE_TOPOLOGY_TRIANGLESTRIP); + + return target_view; +} + +fn createCellBuffer(device: *win32.ID3D11Device, count: u32) *win32.ID3D11Buffer { + var cell_buffer: *win32.ID3D11Buffer = undefined; + const buffer_desc: win32.D3D11_BUFFER_DESC = .{ + .ByteWidth = count * @sizeOf(shader.Cell), + .Usage = .DYNAMIC, + .BindFlags = .{ .SHADER_RESOURCE = 1 }, + .CPUAccessFlags = .{ .WRITE = 1 }, + .MiscFlags = .{ .BUFFER_STRUCTURED = 1 }, + .StructureByteStride = @sizeOf(shader.Cell), + }; + const hr = device.CreateBuffer(&buffer_desc, null, &cell_buffer); + if (hr < 0) fatalHr("CreateCellBuffer", hr); + return cell_buffer; +} + +fn getD3d11TextureMaxCellCount(cell_size: XY(u16)) XY(u16) { + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // small size so we can just render the whole texture for development + //if (true) return .{ .x = 80, .y = 500 }; + comptime std.debug.assert(win32.D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION == 16384); + return .{ + .x = @intCast(@divTrunc(win32.D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION, cell_size.x)), + .y = @intCast(@divTrunc(win32.D3D11_REQ_TEXTURE2D_U_OR_V_DIMENSION, cell_size.y)), + }; +} + +fn cellPosFromIndex(index: u32, column_count: u16) XY(u16) { + return .{ + .x = @intCast(index % column_count), + .y = @intCast(@divTrunc(index, column_count)), + }; +} +fn coordFromCellPos(cell_size: XY(u16), cell_pos: XY(u16)) XY(u16) { + return .{ + .x = cell_size.x * cell_pos.x, + .y = cell_size.y * cell_pos.y, + }; +} + +fn getClientSize(comptime T: type, hwnd: win32.HWND) XY(T) { + var rect: win32.RECT = undefined; + if (0 == win32.GetClientRect(hwnd, &rect)) + fatalWin32("GetClientRect", win32.GetLastError()); + std.debug.assert(rect.left == 0); + std.debug.assert(rect.top == 0); + return .{ .x = @intCast(rect.right), .y = @intCast(rect.bottom) }; +} + +fn fatalWin32(what: []const u8, err: win32.WIN32_ERROR) noreturn { + std.debug.panic("{s} failed with {}", .{ what, err.fmt() }); +} +fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { + std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); +} +fn oom(e: error{OutOfMemory}) noreturn { + @panic(@errorName(e)); +} diff --git a/src/win32/dwrite.zig b/src/win32/dwrite.zig new file mode 100644 index 0000000..ee9ad38 --- /dev/null +++ b/src/win32/dwrite.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const win32 = @import("win32").everything; + +const FontFace = @import("FontFace.zig"); +const XY = @import("xy.zig").XY; + +const global = struct { + var init_called: bool = false; + var dwrite_factory: *win32.IDWriteFactory = undefined; +}; + +pub fn init() void { + std.debug.assert(!global.init_called); + global.init_called = true; + { + const hr = win32.DWriteCreateFactory( + win32.DWRITE_FACTORY_TYPE_SHARED, + win32.IID_IDWriteFactory, + @ptrCast(&global.dwrite_factory), + ); + if (hr < 0) fatalHr("DWriteCreateFactory", hr); + } +} + +pub const Font = struct { + text_format: *win32.IDWriteTextFormat, + cell_size: XY(u16), + + pub fn init(dpi: u32, size: f32, face: *const FontFace) Font { + var text_format: *win32.IDWriteTextFormat = undefined; + { + const hr = global.dwrite_factory.CreateTextFormat( + face.ptr(), + null, + .NORMAL, //weight + .NORMAL, // style + .NORMAL, // stretch + win32.scaleDpi(f32, size, dpi), + win32.L(""), // locale + &text_format, + ); + if (hr < 0) std.debug.panic( + "CreateTextFormat '{}' height {d} failed, hresult=0x{x}", + .{ std.unicode.fmtUtf16le(face.slice()), size, @as(u32, @bitCast(hr)) }, + ); + } + errdefer _ = text_format.IUnknown.Release(); + + const cell_size = blk: { + var text_layout: *win32.IDWriteTextLayout = undefined; + { + const hr = global.dwrite_factory.CreateTextLayout( + win32.L("█"), + 1, + text_format, + std.math.floatMax(f32), + std.math.floatMax(f32), + &text_layout, + ); + if (hr < 0) fatalHr("CreateTextLayout", hr); + } + defer _ = text_layout.IUnknown.Release(); + + var metrics: win32.DWRITE_TEXT_METRICS = undefined; + { + const hr = text_layout.GetMetrics(&metrics); + if (hr < 0) fatalHr("GetMetrics", hr); + } + break :blk .{ + .x = @as(u16, @intFromFloat(@floor(metrics.width))), + .y = @as(u16, @intFromFloat(@floor(metrics.height))), + }; + }; + + return .{ + .text_format = text_format, + .cell_size = cell_size, + }; + } + + pub fn deinit(self: *Font) void { + _ = self.text_format.IUnknown.Release(); + self.* = undefined; + } + + pub fn getCellSize(self: Font, comptime T: type) XY(T) { + return .{ + .x = @intCast(self.cell_size.x), + .y = @intCast(self.cell_size.y), + }; + } +}; + +fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { + std.debug.panic("{s} failed, hresult=0x{x}", .{ what, @as(u32, @bitCast(hresult)) }); +} diff --git a/src/win32/gui.zig b/src/win32/gui.zig index e6c0102..82255ed 100644 --- a/src/win32/gui.zig +++ b/src/win32/gui.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const build_options = @import("build_options"); const root = @import("root"); const c = @cImport({ @@ -6,7 +7,7 @@ const c = @cImport({ }); const win32 = @import("win32").everything; -const ddui = @import("ddui"); +const win32ext = @import("win32ext.zig"); const cbor = @import("cbor"); const thespian = @import("thespian"); @@ -17,19 +18,24 @@ const RGB = @import("color").RGB; const input = @import("input"); const windowmsg = @import("windowmsg.zig"); -const HResultError = ddui.HResultError; +const render = if (build_options.d2d) @import("d2d.zig") else @import("d3d11.zig"); + +const FontFace = @import("FontFace.zig"); +const XY = @import("xy.zig").XY; const WM_APP_EXIT = win32.WM_APP + 1; const WM_APP_SET_BACKGROUND = win32.WM_APP + 2; const WM_APP_ADJUST_FONTSIZE = win32.WM_APP + 3; const WM_APP_SET_FONTSIZE = win32.WM_APP + 4; const WM_APP_SET_FONTFACE = win32.WM_APP + 5; +const WM_APP_UPDATE_SCREEN = win32.WM_APP + 6; const WM_APP_EXIT_RESULT = 0x45feaa11; const WM_APP_SET_BACKGROUND_RESULT = 0x369a26cd; const WM_APP_ADJUST_FONTSIZE_RESULT = 0x79aba9ef; const WM_APP_SET_FONTSIZE_RESULT = 0x72fa44bc; const WM_APP_SET_FONTFACE_RESULT = 0x1a49ffa8; +const WM_APP_UPDATE_SCREEN_RESULT = 0x3add213b; pub const DropWriter = struct { pub const WriteError = error{}; @@ -58,48 +64,27 @@ const global = struct { var init_called: bool = false; var start_called: bool = false; var icons: Icons = undefined; - var dwrite_factory: *win32.IDWriteFactory = undefined; + var state: ?State = null; - var d2d_factory: *win32.ID2D1Factory = undefined; var conf: ?gui_config = null; - var fontface: ?[:0]const u16 = null; + var fontface: ?FontFace = null; var fontsize: ?f32 = null; + var font: ?Font = null; - var text_format_editor: ddui.TextFormatCache(FontCacheParams, createTextFormatEditor) = .{}; - - const shared_screen = struct { - var mutex: std.Thread.Mutex = .{}; - // only access arena/obj while the mutex is locked - var arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); - var obj: vaxis.Screen = .{}; - }; + var screen_arena: std.heap.ArenaAllocator = std.heap.ArenaAllocator.init(std.heap.page_allocator); + var screen: vaxis.Screen = .{}; }; const window_style_ex = win32.WINDOW_EX_STYLE{ .APPWINDOW = 1, //.ACCEPTFILES = 1, + .NOREDIRECTIONBITMAP = render.NOREDIRECTIONBITMAP, }; const window_style = win32.WS_OVERLAPPEDWINDOW; pub fn init() void { std.debug.assert(!global.init_called); global.init_called = true; - - { - const hr = win32.DWriteCreateFactory( - win32.DWRITE_FACTORY_TYPE_SHARED, - win32.IID_IDWriteFactory, - @ptrCast(&global.dwrite_factory), - ); - if (hr < 0) fatalHr("DWriteCreateFactory", hr); - } - { - var err: HResultError = undefined; - global.d2d_factory = ddui.createFactory( - .SINGLE_THREADED, - .{}, - &err, - ) catch std.debug.panic("{}", .{err}); - } + render.init(); } const Icons = struct { @@ -135,35 +120,6 @@ fn getIcons(dpi: XY(u32)) Icons { return .{ .small = @ptrCast(small), .large = @ptrCast(large) }; } -fn d2dColorFromVAxis(color: vaxis.Cell.Color) win32.D2D_COLOR_F { - return switch (color) { - .default => .{ .r = 0, .g = 0, .b = 0, .a = 0 }, - .index => |idx| blk: { - const rgb = RGB.from_u24(xterm_colors[idx]); - break :blk .{ - .r = @as(f32, @floatFromInt(rgb.r)) / 255.0, - .g = @as(f32, @floatFromInt(rgb.g)) / 255.0, - .b = @as(f32, @floatFromInt(rgb.b)) / 255.0, - .a = 1, - }; - }, - .rgb => |rgb| .{ - .r = @as(f32, @floatFromInt(rgb[0])) / 255.0, - .g = @as(f32, @floatFromInt(rgb[1])) / 255.0, - .b = @as(f32, @floatFromInt(rgb[2])) / 255.0, - .a = 1, - }, - }; -} - -const FontCacheParams = struct { - dpi: u32, - fontsize: f32, - pub fn eql(self: FontCacheParams, other: FontCacheParams) bool { - return self.dpi == other.dpi and self.fontsize == other.fontsize; - } -}; - fn getConfig() *const gui_config { if (global.conf == null) { global.conf, _ = root.read_config(gui_config, global.arena); @@ -177,20 +133,34 @@ fn getFieldDefault(field: std.builtin.Type.StructField) ?*const field.type { return @alignCast(@ptrCast(field.default_value orelse return null)); } -fn getFontFace() [:0]const u16 { +fn getDefaultFontFace() FontFace { + const default = comptime getFieldDefault( + std.meta.fieldInfo(gui_config, .fontface), + ) orelse @compileError("gui_config fontface is missing default"); + const default_wide = win32.L(default.*); + var result: FontFace = .{ .buf = undefined, .len = default_wide.len }; + @memcpy(result.buf[0..default_wide.len], default_wide); + result.buf[default_wide.len] = 0; + return result; +} + +fn getFontFace() *const FontFace { if (global.fontface == null) { const conf = getConfig(); global.fontface = blk: { - break :blk std.unicode.utf8ToUtf16LeAllocZ(std.heap.c_allocator, conf.fontface) catch |e| { - std.log.err("failed to convert fontface name with {s}", .{@errorName(e)}); - const default = comptime getFieldDefault( - std.meta.fieldInfo(gui_config, .fontface), - ) orelse @compileError("fontface is missing default"); - break :blk win32.L(default.*); + break :blk FontFace.initUtf8(conf.fontface) catch |e| switch (e) { + error.TooLong => { + std.log.err("fontface '{s}' is too long", .{conf.fontface}); + break :blk getDefaultFontFace(); + }, + error.InvalidUtf8 => { + std.log.err("fontface '{s}' is invalid utf8", .{conf.fontface}); + break :blk getDefaultFontFace(); + }, }; }; } - return global.fontface.?; + return &(global.fontface.?); } fn getFontSize() f32 { @@ -200,88 +170,34 @@ fn getFontSize() f32 { return global.fontsize.?; } -fn createTextFormatEditor(params: FontCacheParams) *win32.IDWriteTextFormat { - var err: HResultError = undefined; - return ddui.createTextFormat(global.dwrite_factory, &err, .{ - .size = win32.scaleDpi(f32, params.fontsize, params.dpi), - .family_name = getFontFace(), - }) catch std.debug.panic("{s} failed, hresult=0x{x}", .{ err.context, err.hr }); +fn getFont(dpi: u32, size: f32, face: *const FontFace) render.Font { + if (global.font) |*font| { + if (font.dpi == dpi and font.size == size and font.face.eql(face)) + return font.render_object; + font.render_object.deinit(); + global.font = null; + } + global.font = .{ + .dpi = dpi, + .size = size, + .face = face.*, + .render_object = render.Font.init(dpi, size, face), + }; + return global.font.?.render_object; } -const D2d = struct { - target: *win32.ID2D1HwndRenderTarget, - brush: *win32.ID2D1SolidColorBrush, - pub fn init(hwnd: win32.HWND, err: *HResultError) error{HResult}!D2d { - var target: *win32.ID2D1HwndRenderTarget = undefined; - const target_props = win32.D2D1_RENDER_TARGET_PROPERTIES{ - .type = .DEFAULT, - .pixelFormat = .{ - .format = .B8G8R8A8_UNORM, - .alphaMode = .PREMULTIPLIED, - }, - .dpiX = 0, - .dpiY = 0, - .usage = .{}, - .minLevel = .DEFAULT, - }; - const hwnd_target_props = win32.D2D1_HWND_RENDER_TARGET_PROPERTIES{ - .hwnd = hwnd, - .pixelSize = .{ .width = 0, .height = 0 }, - .presentOptions = .{}, - }; - - { - const hr = global.d2d_factory.CreateHwndRenderTarget( - &target_props, - &hwnd_target_props, - &target, - ); - if (hr < 0) return err.set(hr, "CreateHwndRenderTarget"); - } - errdefer _ = target.IUnknown.Release(); - - { - var dc: *win32.ID2D1DeviceContext = undefined; - { - const hr = target.IUnknown.QueryInterface(win32.IID_ID2D1DeviceContext, @ptrCast(&dc)); - if (hr < 0) return err.set(hr, "GetDeviceContext"); - } - defer _ = dc.IUnknown.Release(); - // just make everything DPI aware, all applications should just do this - dc.SetUnitMode(win32.D2D1_UNIT_MODE_PIXELS); - } - - var brush: *win32.ID2D1SolidColorBrush = undefined; - { - const color: win32.D2D_COLOR_F = .{ .r = 0, .g = 0, .b = 0, .a = 0 }; - const hr = target.ID2D1RenderTarget.CreateSolidColorBrush(&color, null, &brush); - if (hr < 0) return err.set(hr, "CreateSolidBrush"); - } - errdefer _ = brush.IUnknown.Release(); - - return .{ - .target = target, - .brush = brush, - }; - } - pub fn deinit(self: *D2d) void { - _ = self.brush.IUnknown.Release(); - _ = self.target.IUnknown.Release(); - } - pub fn solid(self: *const D2d, color: win32.D2D_COLOR_F) *win32.ID2D1Brush { - self.brush.SetColor(&color); - return &self.brush.ID2D1Brush; - } +const Font = struct { + dpi: u32, + size: f32, + face: FontFace, + render_object: render.Font, }; const State = struct { hwnd: win32.HWND, pid: thespian.pid, - maybe_d2d: ?D2d = null, - erase_bg_done: bool = false, + render_state: render.WindowState, scroll_delta: isize = 0, - currently_rendered_cell_size: ?XY(i32) = null, - background: ?u32 = null, last_sizing_edge: ?win32.WPARAM = null, bounds: ?WindowBounds = null, }; @@ -290,67 +206,6 @@ fn stateFromHwnd(hwnd: win32.HWND) *State { return &global.state.?; } -fn paint( - d2d: *const D2d, - background: RGB, - screen: *const vaxis.Screen, - text_format_editor: *win32.IDWriteTextFormat, - cell_size: XY(i32), -) void { - { - const color = ddui.rgb8(background.r, background.g, background.b); - d2d.target.ID2D1RenderTarget.Clear(&color); - } - - for (0..screen.height) |y| { - const row_y: i32 = cell_size.y * @as(i32, @intCast(y)); - for (0..screen.width) |x| { - const column_x: i32 = cell_size.x * @as(i32, @intCast(x)); - const cell_index = screen.width * y + x; - const cell = &screen.buf[cell_index]; - - const cell_rect: win32.RECT = .{ - .left = column_x, - .top = row_y, - .right = column_x + cell_size.x, - .bottom = row_y + cell_size.y, - }; - ddui.FillRectangle( - &d2d.target.ID2D1RenderTarget, - cell_rect, - d2d.solid(d2dColorFromVAxis(cell.style.bg)), - ); - - // TODO: pre-caclulate the buffer size needed, for now this should just - // cause out-of-bounds access - var buf_wtf16: [100]u16 = undefined; - const grapheme_len = blk: { - break :blk std.unicode.wtf8ToWtf16Le(&buf_wtf16, cell.char.grapheme) catch |err| switch (err) { - error.InvalidWtf8 => { - buf_wtf16[0] = std.unicode.replacement_character; - break :blk 1; - }, - }; - }; - const grapheme = buf_wtf16[0..grapheme_len]; - if (std.mem.eql(u16, grapheme, &[_]u16{' '})) - continue; - ddui.DrawText( - &d2d.target.ID2D1RenderTarget, - grapheme, - text_format_editor, - ddui.rectFloatFromInt(cell_rect), - d2d.solid(d2dColorFromVAxis(cell.style.fg)), - .{ - .CLIP = 1, - .ENABLE_COLOR_FONT = 1, - }, - .NATURAL, - ); - } - } -} - const WindowPlacement = struct { dpi: XY(u32), size: XY(i32), @@ -409,7 +264,7 @@ fn calcWindowPlacement( .x = @min(wanted_size.x, work_size.x), .y = @min(wanted_size.y, work_size.y), }; - const bouding_rect: win32.RECT = ddui.rectIntFromSize(.{ + const bouding_rect: win32.RECT = rectIntFromSize(.{ .left = work_rect.left + @divTrunc(work_size.x - bounding_size.x, 2), .top = work_rect.top + @divTrunc(work_size.y - bounding_size.y, 2), .width = bounding_size.x, @@ -440,6 +295,9 @@ pub fn start() !std.Thread { return try std.Thread.spawn(.{}, entry, .{pid}); } fn entry(pid: thespian.pid) !void { + std.debug.assert(global.init_called); + std.debug.assert(global.start_called); + const conf = getConfig(); const maybe_monitor: ?win32.HMONITOR = blk: { @@ -472,8 +330,7 @@ fn entry(pid: thespian.pid) !void { break :blk dpi; }; - const text_format_editor = global.text_format_editor.getOrCreate(FontCacheParams{ .dpi = @max(dpi.x, dpi.y), .fontsize = @floatFromInt(conf.fontsize) }); - const cell_size = getCellSize(text_format_editor); + const cell_size = getFont(@max(dpi.x, dpi.y), getFontSize(), getFontFace()).getCellSize(i32); const initial_placement = calcWindowPlacement( maybe_monitor, @max(dpi.x, dpi.y), @@ -598,45 +455,25 @@ pub fn set_fontsize(hwnd: win32.HWND, fontsize: f32) void { } pub fn set_fontface(hwnd: win32.HWND, fontface_utf8: []const u8) void { - const fontface = std.unicode.utf8ToUtf16LeAllocZ(std.heap.c_allocator, fontface_utf8) catch |e| { - std.log.err("failed to convert fontface name '{s}' with {s}", .{ fontface_utf8, @errorName(e) }); + const fontface = FontFace.initUtf8(fontface_utf8) catch |e| { + std.log.err("failed to set fontface '{s}' with {s}", .{ fontface_utf8, @errorName(e) }); return; }; std.debug.assert(WM_APP_SET_FONTFACE_RESULT == win32.SendMessageW( hwnd, WM_APP_SET_FONTFACE, - @intFromPtr(fontface.ptr), - @intCast(fontface.len), + @intFromPtr(&fontface), + 0, )); } -pub fn updateScreen(screen: *const vaxis.Screen) void { - global.shared_screen.mutex.lock(); - defer global.shared_screen.mutex.unlock(); - _ = global.shared_screen.arena.reset(.retain_capacity); - - const buf = global.shared_screen.arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); - @memcpy(buf, screen.buf); - for (buf) |*cell| { - cell.char.grapheme = global.shared_screen.arena.allocator().dupe( - u8, - cell.char.grapheme, - ) catch |e| oom(e); - } - global.shared_screen.obj = .{ - .width = screen.width, - .height = screen.height, - .width_pix = screen.width_pix, - .height_pix = screen.height_pix, - .buf = buf, - .cursor_row = screen.cursor_row, - .cursor_col = screen.cursor_col, - .cursor_vis = screen.cursor_vis, - .unicode = undefined, - .width_method = undefined, - .mouse_shape = screen.mouse_shape, - .cursor_shape = undefined, - }; +pub fn updateScreen(hwnd: win32.HWND, screen: *const vaxis.Screen) void { + std.debug.assert(WM_APP_UPDATE_SCREEN_RESULT == win32.SendMessageW( + hwnd, + WM_APP_UPDATE_SCREEN, + @intFromPtr(screen), + 0, + )); } const WindowBounds = struct { @@ -650,8 +487,8 @@ fn updateWindowSize( bounds_ref: *?WindowBounds, ) void { const dpi = win32.dpiFromHwnd(hwnd); - const text_format_editor = global.text_format_editor.getOrCreate(FontCacheParams{ .dpi = dpi, .fontsize = getFontSize() }); - const cell_size = getCellSize(text_format_editor); + const font = getFont(dpi, getFontSize(), getFontFace()); + const cell_size = font.getCellSize(i32); var window_rect: win32.RECT = undefined; if (0 == win32.GetWindowRect(hwnd, &window_rect)) fatalWin32( @@ -681,35 +518,6 @@ fn updateWindowSize( setWindowPosRect(hwnd, new_rect); } -// NOTE: we round the text metric up to the nearest integer which -// means our background rectangles will be aligned. We accomodate -// for any gap added by doing this by centering the text. -fn getCellSize(text_format: *win32.IDWriteTextFormat) XY(i32) { - var text_layout: *win32.IDWriteTextLayout = undefined; - { - const hr = global.dwrite_factory.CreateTextLayout( - win32.L("█"), - 1, - text_format, - std.math.floatMax(f32), - std.math.floatMax(f32), - &text_layout, - ); - if (hr < 0) fatalHr("CreateTextLayout", hr); - } - defer _ = text_layout.IUnknown.Release(); - - var metrics: win32.DWRITE_TEXT_METRICS = undefined; - { - const hr = text_layout.GetMetrics(&metrics); - if (hr < 0) fatalHr("GetMetrics", hr); - } - return .{ - .x = @max(1, @as(i32, @intFromFloat(@floor(metrics.width)))), - .y = @max(1, @as(i32, @intFromFloat(@floor(metrics.height)))), - }; -} - const CellPos = struct { cell: XY(i32), offset: XY(i32), @@ -757,9 +565,10 @@ fn sendMouse( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { - const point = ddui.pointFromLparam(lparam); + const point = win32ext.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); - const cell_size = state.currently_rendered_cell_size orelse return; + const dpi = win32.dpiFromHwnd(hwnd); + const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); const cell = CellPos.init(cell_size, point.x, point.y); switch (kind) { .move => { @@ -807,9 +616,10 @@ fn sendMouseWheel( wparam: win32.WPARAM, lparam: win32.LPARAM, ) void { - const point = ddui.pointFromLparam(lparam); + const point = win32ext.pointFromLparam(lparam); const state = stateFromHwnd(hwnd); - const cell_size = state.currently_rendered_cell_size orelse return; + const dpi = win32.dpiFromHwnd(hwnd); + const cell_size = getFont(dpi, getFontSize(), getFontFace()).getCellSize(i32); const cell = CellPos.init(cell_size, point.x, point.y); // const fwKeys = win32.loword(wparam); state.scroll_delta += @as(i16, @bitCast(win32.hiword(wparam))); @@ -1159,70 +969,30 @@ fn WndProc( return 0; }, win32.WM_PAINT => { - const dpi = win32.dpiFromHwnd(hwnd); - const client_size = getClientSize(hwnd); const state = stateFromHwnd(hwnd); - - const err: HResultError = blk: { - var ps: win32.PAINTSTRUCT = undefined; - _ = win32.BeginPaint(hwnd, &ps) orelse return fatalWin32( - "BeginPaint", - win32.GetLastError(), - ); - defer if (0 == win32.EndPaint(hwnd, &ps)) fatalWin32( - "EndPaint", - win32.GetLastError(), - ); - - if (state.maybe_d2d == null) { - var err: HResultError = undefined; - state.maybe_d2d = D2d.init(hwnd, &err) catch break :blk err; - } - - { - const size: win32.D2D_SIZE_U = .{ - .width = @intCast(client_size.x), - .height = @intCast(client_size.y), - }; - const hr = state.maybe_d2d.?.target.Resize(&size); - if (hr < 0) break :blk HResultError{ .context = "D2dResize", .hr = hr }; - } - state.maybe_d2d.?.target.ID2D1RenderTarget.BeginDraw(); - - const text_format_editor = global.text_format_editor.getOrCreate(FontCacheParams{ .dpi = dpi, .fontsize = getFontSize() }); - state.currently_rendered_cell_size = getCellSize(text_format_editor); - - { - global.shared_screen.mutex.lock(); - defer global.shared_screen.mutex.unlock(); - paint( - &state.maybe_d2d.?, - RGB.from_u24(if (state.background) |b| @intCast(0xffffff & b) else 0), - &global.shared_screen.obj, - text_format_editor, - state.currently_rendered_cell_size.?, - ); - } - - break :blk HResultError{ - .context = "D2dEndDraw", - .hr = state.maybe_d2d.?.target.ID2D1RenderTarget.EndDraw(null, null), - }; - }; - - if (err.hr == win32.D2DERR_RECREATE_TARGET) { - std.log.debug("D2DERR_RECREATE_TARGET", .{}); - state.maybe_d2d.?.deinit(); - state.maybe_d2d = null; - win32.invalidateHwnd(hwnd); - } else if (err.hr < 0) std.debug.panic("paint error: {}", .{err}); + const dpi = win32.dpiFromHwnd(hwnd); + const font = getFont(dpi, getFontSize(), getFontFace()); + render.paint(hwnd, &state.render_state, font, &global.screen); return 0; }, win32.WM_GETDPISCALEDSIZE => { const inout_size: *win32.SIZE = @ptrFromInt(@as(usize, @bitCast(lparam))); const new_dpi: u32 = @intCast(0xffffffff & wparam); - const text_format_editor = global.text_format_editor.getOrCreate(FontCacheParams{ .dpi = new_dpi, .fontsize = getFontSize() }); - const cell_size = getCellSize(text_format_editor); + // we don't want to update the font with the new dpi until after + // the dpi change is effective, so, we get the cell size from the current font/dpi + // and re-scale it based on the new dpi ourselves + const current_dpi = win32.dpiFromHwnd(hwnd); + const font = getFont(current_dpi, getFontSize(), getFontFace()); + const current_cell_size_i32 = font.getCellSize(i32); + const current_cell_size: XY(f32) = .{ + .x = @floatFromInt(current_cell_size_i32.x), + .y = @floatFromInt(current_cell_size_i32.y), + }; + const scale: f32 = @as(f32, @floatFromInt(new_dpi)) / @as(f32, @floatFromInt(current_dpi)); + const rescaled_cell_size: XY(i32) = .{ + .x = @intFromFloat(@round(current_cell_size.x * scale)), + .y = @intFromFloat(@round(current_cell_size.y * scale)), + }; const new_rect = calcWindowRect( new_dpi, .{ @@ -1232,7 +1002,7 @@ fn WndProc( .bottom = inout_size.cy, }, win32.WMSZ_BOTTOMRIGHT, - cell_size, + rescaled_cell_size, ); inout_size.* = .{ .cx = new_rect.right, @@ -1254,13 +1024,13 @@ fn WndProc( => { const do_sanity_check = true; if (do_sanity_check) { - const client_pixel_size: XY(u16) = .{ + const lparam_size: XY(u16) = .{ .x = win32.loword(lparam), .y = win32.hiword(lparam), }; - const client_size = getClientSize(hwnd); - std.debug.assert(client_pixel_size.x == client_size.x); - std.debug.assert(client_pixel_size.y == client_size.y); + const client_size = getClientSize(u16, hwnd); + std.debug.assert(lparam_size.x == client_size.x); + std.debug.assert(lparam_size.y == client_size.y); } sendResize(hwnd); return 0; @@ -1281,20 +1051,6 @@ fn WndProc( win32.invalidateHwnd(hwnd); return 0; }, - win32.WM_ERASEBKGND => { - const state = stateFromHwnd(hwnd); - if (!state.erase_bg_done) { - state.erase_bg_done = true; - const brush = win32.CreateSolidBrush(toColorRef(.{ .r = 29, .g = 29, .b = 29 })) orelse - fatalWin32("CreateSolidBrush", win32.GetLastError()); - defer deleteObject(brush); - const hdc: win32.HDC = @ptrFromInt(wparam); - var rect: win32.RECT = undefined; - if (0 == win32.GetClientRect(hwnd, &rect)) @panic(""); - if (0 == win32.FillRect(hdc, &rect, brush)) @panic(""); - } - return 1; // background erased - }, win32.WM_CLOSE => { const state = stateFromHwnd(hwnd); state.pid.send(.{ "cmd", "quit" }) catch |e| onexit(e); @@ -1305,8 +1061,10 @@ fn WndProc( return WM_APP_EXIT_RESULT; }, WM_APP_SET_BACKGROUND => { - const state = stateFromHwnd(hwnd); - state.background = @intCast(wparam); + render.setBackground( + &stateFromHwnd(hwnd).render_state, + RGB.from_u24(@intCast(0xffffff & wparam)), + ); win32.invalidateHwnd(hwnd); return WM_APP_SET_BACKGROUND_RESULT; }, @@ -1328,15 +1086,40 @@ fn WndProc( }, WM_APP_SET_FONTFACE => { const state = stateFromHwnd(hwnd); - var fontface: [:0]const u16 = undefined; - fontface.ptr = @ptrFromInt(wparam); - fontface.len = @intCast(lparam); - if (global.fontface) |old_fontface| std.heap.c_allocator.free(old_fontface); - global.fontface = fontface; + const fontface: *FontFace = @ptrFromInt(wparam); + global.fontface = fontface.*; updateWindowSize(hwnd, win32.WMSZ_BOTTOMRIGHT, &state.bounds); win32.invalidateHwnd(hwnd); return WM_APP_SET_FONTFACE_RESULT; }, + WM_APP_UPDATE_SCREEN => { + const screen: *const vaxis.Screen = @ptrFromInt(wparam); + _ = global.screen_arena.reset(.retain_capacity); + const buf = global.screen_arena.allocator().alloc(vaxis.Cell, screen.buf.len) catch |e| oom(e); + @memcpy(buf, screen.buf); + for (buf) |*cell| { + cell.char.grapheme = global.screen_arena.allocator().dupe( + u8, + cell.char.grapheme, + ) catch |e| oom(e); + } + global.screen = .{ + .width = screen.width, + .height = screen.height, + .width_pix = screen.width_pix, + .height_pix = screen.height_pix, + .buf = buf, + .cursor_row = screen.cursor_row, + .cursor_col = screen.cursor_col, + .cursor_vis = screen.cursor_vis, + .unicode = undefined, + .width_method = undefined, + .mouse_shape = screen.mouse_shape, + .cursor_shape = undefined, + }; + win32.invalidateHwnd(hwnd); + return WM_APP_UPDATE_SCREEN_RESULT; + }, win32.WM_CREATE => { std.debug.assert(global.state == null); const create_struct: *win32.CREATESTRUCTW = @ptrFromInt(@as(usize, @bitCast(lparam))); @@ -1344,6 +1127,7 @@ fn WndProc( global.state = .{ .hwnd = hwnd, .pid = create_args.pid, + .render_state = render.WindowState.init(hwnd), }; std.debug.assert(&(global.state.?) == stateFromHwnd(hwnd)); sendResize(hwnd); @@ -1363,32 +1147,25 @@ fn sendResize( ) void { const dpi = win32.dpiFromHwnd(hwnd); const state = stateFromHwnd(hwnd); - if (state.maybe_d2d == null) { - var err: HResultError = undefined; - state.maybe_d2d = D2d.init(hwnd, &err) catch std.debug.panic( - "D2d.init failed with {}", - .{err}, - ); - } - const single_cell_size = getCellSize( - global.text_format_editor.getOrCreate(FontCacheParams{ .dpi = @intCast(dpi), .fontsize = getFontSize() }), - ); - const client_pixel_size = getClientSize(hwnd); - const client_cell_size: XY(u16) = .{ - .x = @intCast(@divTrunc(client_pixel_size.x, single_cell_size.x)), - .y = @intCast(@divTrunc(client_pixel_size.y, single_cell_size.y)), + + const font = getFont(dpi, getFontSize(), getFontFace()); + const cell_size = font.getCellSize(u16); + const client_size = getClientSize(u16, hwnd); + const client_cell_count: XY(u16) = .{ + .x = @intCast(@divTrunc(client_size.x, cell_size.x)), + .y = @intCast(@divTrunc(client_size.y, cell_size.y)), }; std.log.debug( "Resize Px={}x{} Cells={}x{}", - .{ client_pixel_size.x, client_pixel_size.y, client_cell_size.x, client_cell_size.y }, + .{ client_size.x, client_size.y, client_cell_count.x, client_cell_count.y }, ); state.pid.send(.{ "RDR", "Resize", - client_cell_size.x, - client_cell_size.y, - client_pixel_size.x, - client_pixel_size.y, + client_cell_count.x, + client_cell_count.y, + client_size.x, + client_size.y, }) catch @panic("pid send failed"); } @@ -1405,13 +1182,13 @@ fn fatalHr(what: []const u8, hresult: win32.HRESULT) noreturn { fn deleteObject(obj: ?win32.HGDIOBJ) void { if (0 == win32.DeleteObject(obj)) fatalWin32("DeleteObject", win32.GetLastError()); } -fn getClientSize(hwnd: win32.HWND) XY(i32) { +fn getClientSize(comptime T: type, hwnd: win32.HWND) XY(T) { var rect: win32.RECT = undefined; if (0 == win32.GetClientRect(hwnd, &rect)) fatalWin32("GetClientRect", win32.GetLastError()); std.debug.assert(rect.left == 0); std.debug.assert(rect.top == 0); - return .{ .x = rect.right, .y = rect.bottom }; + return .{ .x = @intCast(rect.right), .y = @intCast(rect.bottom) }; } fn calcWindowRect( @@ -1495,6 +1272,15 @@ fn getClientInset(dpi: u32) XY(i32) { }; } +fn rectIntFromSize(args: struct { left: i32, top: i32, width: i32, height: i32 }) win32.RECT { + return .{ + .left = args.left, + .top = args.top, + .right = args.left + args.width, + .bottom = args.top + args.height, + }; +} + fn setWindowPosRect(hwnd: win32.HWND, rect: win32.RECT) void { if (0 == win32.SetWindowPos( hwnd, @@ -1506,50 +1292,3 @@ fn setWindowPosRect(hwnd: win32.HWND, rect: win32.RECT) void { .{ .NOZORDER = 1 }, )) fatalWin32("SetWindowPos", win32.GetLastError()); } - -pub fn XY(comptime T: type) type { - return struct { - x: T, - y: T, - pub fn init(x: T, y: T) @This() { - return .{ .x = x, .y = y }; - } - }; -} - -const xterm_colors: [256]u24 = .{ - 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, - 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, - - 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, - 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, - 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, - 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, - 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, - 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, - 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, - 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, - 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, - 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, - 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, - 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, - 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, - 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, - 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, - 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, - 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, - 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, - 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, - 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, - 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, - 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, - 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, - 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, - 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, - 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, - 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, - - 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, - 0x585858, 0x606060, 0x666666, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, - 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, -}; diff --git a/src/win32/terminal.hlsl b/src/win32/terminal.hlsl new file mode 100644 index 0000000..ae120df --- /dev/null +++ b/src/win32/terminal.hlsl @@ -0,0 +1,84 @@ +cbuffer GridConfig : register(b0) +{ + uint2 cell_size; + uint col_count; + uint row_count; +} + +struct Cell +{ + uint glyph_index; + uint background; + uint foreground; + // todo: underline flags, single/double/curly/dotted/dashed + // todo: underline color +}; +StructuredBuffer cells : register(t0); +Texture2D glyph_texture : register(t1); + +float4 VertexMain(uint id : SV_VERTEXID) : SV_POSITION +{ + return float4( + 2.0 * (float(id & 1) - 0.5), + -(float(id >> 1) - 0.5) * 2.0, + 0, 1 + ); +} + +float4 UnpackRgba(uint packed) +{ + float4 unpacked; + unpacked.r = (float)((packed >> 24) & 0xFF) / 255.0f; + unpacked.g = (float)((packed >> 16) & 0xFF) / 255.0f; + unpacked.b = (float)((packed >> 8) & 0xFF) / 255.0f; + unpacked.a = (float)(packed & 0xFF) / 255.0f; + return unpacked; +} + +float4 PixelMain(float4 sv_pos : SV_POSITION) : SV_TARGET { + uint2 grid_pos = sv_pos.xy / cell_size; + uint index = grid_pos.y * col_count + grid_pos.x; + + const uint DEBUG_MODE_NONE = 0; + const uint DEBUG_MODE_CHECKERBOARD = 1; + const uint DEBUG_MODE_GLYPH_TEXTURE = 2; + + const uint DEBUG_MODE = DEBUG_MODE_NONE; + //const uint DEBUG_MODE = DEBUG_MODE_CHECKERBOARD; + //const uint DEBUG_MODE = DEBUG_MODE_GLYPH_TEXTURE; + + if (DEBUG_MODE == DEBUG_MODE_CHECKERBOARD) { + uint cell_count = col_count * row_count; + float strength = float(index) / float(cell_count); + uint checker = (grid_pos.x + grid_pos.y) % 2; + if (checker == 0) { + float shade = 1.0 - strength; + return float4(shade,shade,shade,1); + } + return float4(0,0,0,1); + } + + Cell cell = cells[index]; + float4 bg_color = UnpackRgba(cell.background); + float4 fg_color = UnpackRgba(cell.foreground); + + if (DEBUG_MODE == DEBUG_MODE_GLYPH_TEXTURE) { + float4 glyph_texel = glyph_texture.Load(int3(sv_pos.xy, 0)); + return lerp(bg_color, fg_color, glyph_texel); + } + + uint2 cell_pixel = uint2(sv_pos.xy) % cell_size; + + uint texture_width, texture_height; + glyph_texture.GetDimensions(texture_width, texture_height); + uint2 texture_size = uint2(texture_width, texture_height); + uint cells_per_row = texture_width / cell_size.x; + + uint2 glyph_cell_pos = uint2( + cell.glyph_index % cells_per_row, + cell.glyph_index / cells_per_row + ); + uint2 texture_coord = glyph_cell_pos * cell_size + cell_pixel; + float4 glyph_texel = glyph_texture.Load(int3(texture_coord, 0)); + return lerp(bg_color, fg_color, glyph_texel.a); +} diff --git a/src/win32/win32ext.zig b/src/win32/win32ext.zig new file mode 100644 index 0000000..7e05dbb --- /dev/null +++ b/src/win32/win32ext.zig @@ -0,0 +1,32 @@ +const std = @import("std"); +const win32 = @import("win32").everything; + +// todo: these should be available in zigwin32 +fn xFromLparam(lparam: win32.LPARAM) i16 { + return @bitCast(win32.loword(lparam)); +} +fn yFromLparam(lparam: win32.LPARAM) i16 { + return @bitCast(win32.hiword(lparam)); +} +pub fn pointFromLparam(lparam: win32.LPARAM) win32.POINT { + return win32.POINT{ .x = xFromLparam(lparam), .y = yFromLparam(lparam) }; +} + +// TODO: update zigwin32 with a way to get the corresponding IID for any COM interface +pub fn queryInterface(obj: anytype, comptime Interface: type) *Interface { + const obj_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(@TypeOf(obj)), '.')) |i| (i + 1) else 0; + const obj_basename = @typeName(@TypeOf(obj))[obj_basename_start..]; + const iface_basename_start: usize = comptime if (std.mem.lastIndexOfScalar(u8, @typeName(Interface), '.')) |i| (i + 1) else 0; + const iface_basename = @typeName(Interface)[iface_basename_start..]; + + const iid_name = "IID_" ++ iface_basename; + const iid = @field(win32, iid_name); + + var iface: *Interface = undefined; + const hr = obj.IUnknown.QueryInterface(iid, @ptrCast(&iface)); + if (hr < 0) std.debug.panic( + "QueryInferface on " ++ obj_basename ++ " as " ++ iface_basename ++ " failed, hresult={}", + .{@as(u32, @bitCast(hr))}, + ); + return iface; +} diff --git a/src/win32/xterm.zig b/src/win32/xterm.zig new file mode 100644 index 0000000..0e63f63 --- /dev/null +++ b/src/win32/xterm.zig @@ -0,0 +1,36 @@ +pub const colors: [256]u24 = .{ + 0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xc0c0c0, + 0x808080, 0xff0000, 0x00ff00, 0xffff00, 0x0000ff, 0xff00ff, 0x00ffff, 0xffffff, + + 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, + 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f, 0x008787, 0x0087af, + 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, + 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7, 0x00d7ff, 0x00ff00, 0x00ff5f, + 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, + 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f, 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, + 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, + 0x5faf87, 0x5fafaf, 0x5fafd7, 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, + 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, + 0x870000, 0x87005f, 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, + 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, + 0x8787d7, 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, + 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f, + 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, + 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7, 0xaf5fff, + 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, + 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f, 0xafd787, 0xafd7af, + 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, + 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7, 0xd700ff, 0xd75f00, 0xd75f5f, + 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, + 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f, 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, + 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, + 0xd7ff87, 0xd7ffaf, 0xd7ffd7, 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, + 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, + 0xff8700, 0xff875f, 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, + 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, + 0xffd7d7, 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, + + 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e, + 0x585858, 0x606060, 0x666666, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, + 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4, 0xeeeeee, +}; diff --git a/src/win32/xy.zig b/src/win32/xy.zig new file mode 100644 index 0000000..30f1ccc --- /dev/null +++ b/src/win32/xy.zig @@ -0,0 +1,14 @@ +pub fn XY(comptime T: type) type { + return struct { + x: T, + y: T, + pub fn init(x: T, y: T) @This() { + return .{ .x = x, .y = y }; + } + + const Self = @This(); + pub fn eql(self: Self, other: Self) bool { + return self.x == other.x and self.y == other.y; + } + }; +}