278 lines
10 KiB
Zig
278 lines
10 KiB
Zig
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);
|
|
}
|
|
};
|