const std = @import("std"); const papyrus = @import("papyrus.zig"); const Context = papyrus.Context; const core = papyrus.core; const Vector2i = core.Vector2i; const Vector2 = core.Vector2; const colors = core.colors; const ColorRGBA8 = colors.ColorRGBA8; const Color = colors.Color; const LocText = @import("localization.zig").LocText; const Font = @import("Font.zig"); const FontAtlas = Font.FontAtlas; // Bmp software renderer // This is a backend agnostic testing renderer which just renders outlines to a bmp file // Used for testing Layouts. allocator: std.mem.Allocator, ui: *Context, extent: Vector2i, r: BmpWriter, outFile: []const u8 = "Saved/frame.bmp", baseColor: ColorRGBA8 = ColorRGBA8.fromHex(0x080808ff), arena: std.heap.ArenaAllocator, pub fn init(allocator: std.mem.Allocator, ui: *Context, extent: Vector2i) !@This() { return .{ .allocator = allocator, .ui = ui, .extent = extent, .r = try BmpWriter.init(allocator, extent), .arena = std.heap.ArenaAllocator.init(allocator), }; } pub fn setRenderFile(self: *@This(), outFile: []const u8) void { self.outFile = outFile; } pub fn deinit(self: *@This()) void { self.arena.deinit(); self.r.deinit(); } pub fn render(self: *@This()) !void { self.r.clear(self.baseColor); var timer = try std.time.Timer.start(); const tstart = timer.read(); var drawList: papyrus.DrawList = .{}; try self.ui.makeDrawList(self.allocator, &drawList, &self.arena); defer drawList.deinit(self.allocator); const tend = timer.read(); const duration = (@as(f64, @floatFromInt(tend - tstart)) / 1000); std.debug.print(" drawList Assembly: {d}us\n", .{duration}); var i: u32 = 0; while (i < drawList.items.len) : (i += 1) { const cmd = drawList.items[i]; switch (cmd.primitive) { .Rect => |rect| { var tl = Vector2i.from(rect.tl); var size = Vector2i.from(rect.size); const border = ColorRGBA8.fromColor(rect.borderColor); const bg = ColorRGBA8.fromColor(rect.backgroundColor); self.r.drawRectangle(.Line, tl, size, border.r, border.g, border.b); self.r.drawRectangle(.Filled, tl.add(Vector2i{ .x = 1, .y = 1 }), size.add(.{ .x = -2, .y = -2 }), bg.r, bg.g, bg.b); }, .Text => |t| { const tl = Vector2i.from(t.tl); self.r.drawText(self.ui.defaultFont.atlas, tl, t.text, t.color); }, } } try self.r.writeOut(self.outFile); } pub const BmpWriter = struct { const FileHeader = extern struct { sig0: [2]u8 = .{ 'B', 'M' }, filesize: u32 align(1) = 0, rsvd0: u32 align(1) = 0, pixelArrayOffset: u32 align(1) = 0, }; const Windows31Info = extern struct { headerSize: u32 align(1) = @sizeOf(@This()), width: u32 align(1) = 0, height: u32 align(1) = 0, planes: u16 align(1) = 1, bitsPerPixel: u16 align(1) = 24, compression: u32 align(1) = 0, imageSize: u32 align(1) = 0, yPixelPerMeter: u32 align(1) = 0, xPixelPerMeter: u32 align(1) = 0, numColorsPallete: u32 align(1) = 0, mostImpColor: u32 align(1) = 0, }; allocator: std.mem.Allocator, extent: Vector2i = .{ .x = 1920, .y = 1080 }, pixelBuffer: []u8, pub fn init(allocator: std.mem.Allocator, resolution: Vector2i) !@This() { const pixelBuffer = try allocator.alloc(u8, @as(usize, @intCast(resolution.x * resolution.y * 3))); @memset(pixelBuffer, 0x8); return .{ .allocator = allocator, .pixelBuffer = pixelBuffer, .extent = resolution }; } pub fn blitBlackWhite(self: *@This(), pixels: []const u8, width: i32, height: i32, xPos: i32, yPos: i32) void { var x: i32 = 0; var y: i32 = 0; while (y < height) : (y += 1) { x = 0; while (x < width) : (x += 1) { const i = (x + xPos + (@as(i32, @intCast(self.extent.y)) - y - yPos) * @as(i32, @intCast(self.extent.x))) * 3; const clamped = @as(usize, @intCast(@max(i, 0))); self.pixelBuffer[clamped + 0] = pixels[@as(usize, @intCast(y * width + x))]; self.pixelBuffer[clamped + 1] = pixels[@as(usize, @intCast(y * width + x))]; self.pixelBuffer[clamped + 2] = pixels[@as(usize, @intCast(y * width + x))]; } } } pub fn clear(self: *@This(), color: ColorRGBA8) void { var i: u32 = 0; while (i < self.pixelBuffer.len) : (i += 3) { self.pixelBuffer[i + 2] = color.r; self.pixelBuffer[i + 1] = color.g; self.pixelBuffer[i + 0] = color.b; } } pub fn addChar(self: *@This(), atlas: *const FontAtlas, pos: Vector2i, ch: u8, color: Color) void { const metrics = atlas.glyphMetrics[ch]; var row: i32 = 0; while (row < metrics.y) : (row += 1) { var col: i32 = 0; while (col < metrics.x) : (col += 1) { const pixelOffset = @as(usize, @intCast((atlas.glyphStride * ch) + col + row * atlas.atlasSize.x)); const pixelOffset2 = @as(usize, @intCast(((self.extent.y - pos.y - row) * self.extent.x) + pos.x + col)); const alpha = @as(f32, @floatFromInt(atlas.atlasBuffer.?[pixelOffset])) / 255; const old = Color.fromRGB2( @as(f32, @floatFromInt(self.pixelBuffer[pixelOffset2 * 3 + 2])) / 255, @as(f32, @floatFromInt(self.pixelBuffer[pixelOffset2 * 3 + 1])) / 255, @as(f32, @floatFromInt(self.pixelBuffer[pixelOffset2 * 3 + 0])) / 255, ); const new = ColorRGBA8{ .r = @as(u8, @intFromFloat(255 * (alpha * color.r + (1 - alpha) * old.r))), .g = @as(u8, @intFromFloat(255 * (alpha * color.g + (1 - alpha) * old.g))), .b = @as(u8, @intFromFloat(255 * (alpha * color.r + (1 - alpha) * old.b))), }; self.pixelBuffer[pixelOffset2 * 3 + 2] = new.r; self.pixelBuffer[pixelOffset2 * 3 + 1] = new.g; self.pixelBuffer[pixelOffset2 * 3 + 0] = new.b; } } } pub fn drawText(self: *@This(), atlas: *const FontAtlas, topLeft: Vector2i, text: LocText, color: Color) void { // blit each character from the atlas onto the thing const str = text.getRead(); var accum: i32 = 0; for (str, 0..) |ch, i| { _ = i; const box = atlas.glyphBox1[ch]; self.addChar( atlas, topLeft.add(.{ .x = accum, .y = @as(i32, @intFromFloat(atlas.fontSize)), }).add(.{ .x = box.x, .y = box.y }), ch, color, ); accum += box.x + atlas.glyphMetrics[ch].x; if (ch == ' ') { accum += atlas.glyphStride; } } } pub fn drawRectangle(self: *@This(), style: enum { Filled, Line }, topLeft: Vector2i, size: Vector2i, r: u8, g: u8, b: u8) void { var i: i32 = 0; while (i < size.y) : (i += 1) { const row = i + topLeft.y; if (row < 0 or row >= self.extent.y) { continue; } const flippedRow = self.extent.y - row - 1; { const col = topLeft.x; if (col >= 0 and col < self.extent.x) { const pixelOffset = @as(usize, @intCast(flippedRow * self.extent.x + col)); self.pixelBuffer[pixelOffset * 3 + 2] = r; self.pixelBuffer[pixelOffset * 3 + 1] = g; self.pixelBuffer[pixelOffset * 3 + 0] = b; } } if (i == 0 or i == size.y - 1 or style == .Filled) { var col: i32 = topLeft.x + 1; while (col < topLeft.x + size.x) : (col += 1) { if (col < 0 or col >= self.extent.x) { continue; } const pixelOffset = @as(usize, @intCast((flippedRow) * self.extent.x + col)); self.pixelBuffer[pixelOffset * 3 + 2] = r; self.pixelBuffer[pixelOffset * 3 + 1] = g; self.pixelBuffer[pixelOffset * 3 + 0] = b; } } { const col = topLeft.x + size.x; if (col >= 0 and col < self.extent.x) { const pixelOffset = @as(usize, @intCast(flippedRow * self.extent.x + col)); self.pixelBuffer[pixelOffset * 3 + 2] = r; self.pixelBuffer[pixelOffset * 3 + 1] = g; self.pixelBuffer[pixelOffset * 3 + 0] = b; } } } } pub fn writeOut(self: @This(), outFile: []const u8) !void { var header: FileHeader = .{ .rsvd0 = 0, .filesize = @as(u32, @intCast(self.extent.x * self.extent.y * 3 + @as(u32, @intCast(@sizeOf(FileHeader))))), .pixelArrayOffset = @sizeOf(FileHeader) + @sizeOf(Windows31Info), }; var info: Windows31Info = .{ .planes = 1, .bitsPerPixel = 24, .compression = 0, .numColorsPallete = 0, .mostImpColor = 0, .xPixelPerMeter = 0x130B, .yPixelPerMeter = 0x130B, .width = @as(u32, @intCast(self.extent.x)), .height = @as(u32, @intCast(self.extent.y)), .imageSize = @as(u32, @intCast(self.extent.x * self.extent.y * 3)), }; const cwd = std.fs.cwd(); cwd.makePath("Saved") catch unreachable; var logFile = try cwd.createFile(outFile, .{}); defer logFile.close(); try logFile.writeAll(std.mem.asBytes(&header)); try logFile.writeAll(std.mem.asBytes(&info)); try logFile.writeAll(self.pixelBuffer); } pub fn deinit(self: *@This()) void { self.allocator.free(self.pixelBuffer); } };