const std = @import("std"); const mem = std.mem; const fs = std.fs; const c = @cImport({ @cInclude("stb_image.h"); @cInclude("stb_truetype.h"); }); const Allocator = mem.Allocator; const ArrayListUnmanaged = std.ArrayListUnmanaged; const AutoHashMapUnmanaged = std.AutoHashMapUnmanaged; const Thread = std.Thread; const assert = std.debug.assert; pub const V2i = @Vector(2, i32); pub const V2u = @Vector(2, u32); pub const V2 = @Vector(2, f32); pub const V4 = @Vector(4, f32); pub const Color = struct { vec: @Vector(4, f32), pub fn float(r: f32, g: f32, b: f32, a: f32) Color { return .{.vec = .{b, g, r, a}}; } pub fn hex(argb: u32) Color { return .{.vec = @as(@Vector(4, f32), @floatFromInt(@as(@Vector(4, u8), @bitCast(mem.nativeToLittle(u32, argb))))) / @as(@Vector(4, f32), @splat(255))}; } }; pub const GlyphIndex = i32; pub var prev_mouse_pos: V2 = V2{0, 0}; pub var mouse_pos = V2{0, 0}; pub var mouse_delta = V2{0, 0}; pub fn globalInit(allocator: Allocator) !void { try RenderQueue.init(allocator); } pub fn globalDeinit() void { RenderQueue.deinit(); } pub const Rect = struct { pos: V2, size: V2, pub const screenSpaceFill = Rect{.pos = .{0, 0}, .size = .{1, 1}}; pub fn resolve(r: anytype) Rect { return switch (@TypeOf(r)) { Rect => r, ?Rect => r orelse Rect.screenSpaceFill, else => @compileError("Rect.resolve() only accepts Rect and ?Rect. You passed: "++@typeName(@TypeOf(r))), }; } pub fn contains(r: Rect, v: V2) bool { return @reduce(.And, r.pos <= v) and @reduce(.And, v <= r.pos+r.size); } fn intersection(a: Rect, b: Rect) Rect { const a_min = a.pos; const a_max = a.pos + a.size; const b_min = b.pos; const b_max = b.pos + b.size; const i_min = @max(a_min, b_min); const i_max = @min(a_max, b_max); const i_size = i_max - i_min; if (@reduce(.Or, i_size <= V2{0,0})) { // any component <= 0 means no intersection return .{.pos = .{0,0}, .size = .{0,0}}; } return .{.pos = i_min, .size = i_size}; } }; pub const Area = struct { rect: Rect = .{.pos = .{0, 0}, .size = .{1, 1}}, children_offset: V2 = .{0, 0}, pub fn intersection(self: *Area, other: Area) void { self.rect = self.rect.intersection(other.rect); self.children_offset += other.children_offset; } }; pub const Command = union(enum) { rect: Rect, color: Color, image: ?Image, image_rect: Rect, text_begin: struct {}, text_end: struct {}, //area_begin: Area, //area_end: struct {}, }; pub const CommandBuffer = struct { commands: ArrayListUnmanaged(Command), allocator: Allocator, pub fn init(initial_capacity: u16, allocator: Allocator) !CommandBuffer { return .{ .commands = try ArrayListUnmanaged(Command).initCapacity(allocator, initial_capacity), .allocator = allocator, }; } pub fn deinit(cb: *CommandBuffer) void { cb.commands.deinit(cb.allocator); } pub fn rect(cb: *CommandBuffer, r: Rect) !void { try cb.commands.append(cb.allocator, .{.rect = r}); } pub fn color(cb: *CommandBuffer, value: Color) !void { try cb.commands.append(cb.allocator, .{.color = value}); } pub fn textBegin(cb: *CommandBuffer) !void { try cb.commands.append(cb.allocator, .{.text_begin = .{}}); } pub fn textEnd(cb: *CommandBuffer) !void { try cb.commands.append(cb.allocator, .{.text_end = .{}}); } pub fn image(cb: *CommandBuffer, img: ?Image) !void { try cb.commands.append(cb.allocator, .{.image = img}); } pub fn image_rect(cb: *CommandBuffer, r: Rect) !void { try cb.commands.append(cb.allocator, .{.image_rect = r}); } //pub fn areaBegin(cb: *CommandBuffer, area: Area) !void { // try cb.commands.append(cb.allocator, .{.area_begin = area}); //} //pub fn areaEnd(cb: *CommandBuffer) !void { // try cb.commands.append(cb.allocator, .{.area_end = .{}}); //} }; pub const Image = struct { size: V2, channels: u8, pixels: []Color, allocator: Allocator, pub fn fromPath(path: [:0]const u8, allocator: Allocator) !Image { var width: c_int = undefined; var height: c_int = undefined; var channels: c_int = undefined; const desired_channels = 4; const c_pixels = c.stbi_loadf( path.ptr, &width, &height, &channels, desired_channels, ); if (c_pixels == null) { return error.ImageLoadFail; } defer c.stbi_image_free(c_pixels); const pixels = try allocator.alloc(Color, @as(u32, @intCast(width)) * @as(u32, @intCast(height))); for (pixels, 0..) |*p, i| { const src = c_pixels[i*4..i*4+4]; p.*.vec = .{src[2], src[1], src[0], src[3]}; } return .{ .pixels = pixels, .size = @floatFromInt(@Vector(2, c_int){width, height}), .channels = desired_channels, .allocator = allocator, }; } pub fn deinit (img: Image) void { img.allocator.free(img.pixels); } }; pub const Font = struct { pub const Glyph = struct { image: Image, offset: V2, pub fn deinit(glyph: Glyph) void { glyph.image.deinit(); } }; allocator: Allocator, glyphs: AutoHashMapUnmanaged(u21, Glyph), font_height: f32, // TODO fallback fonts raw: [:0]const u8, info: c.stbtt_fontinfo, scale: f32, pub const FontLoadArgs = struct { font_height: f32 = 32, line_height_scale: f32 = 1 }; pub fn fromPath(path: [:0]const u8, allocator: Allocator, args: FontLoadArgs) !Font { var info: c.stbtt_fontinfo = undefined; const raw = try fs.cwd().readFileAllocOptions(allocator, path, std.math.maxInt(usize), null, .@"1", 0); errdefer allocator.free(raw); if (c.stbtt_InitFont(&info, raw.ptr, 0) == 0) { return error.FontLoadFail; } //const kerning_table_len = @as(usize, @intCast(c.stbtt_GetKerningTableLength(&info))); //if (kerning_table_len != 0) { // var kerning_table = try allocator.alloc(c.stbtt_kerningentry, kerning_table_len); // defer allocator.free(kerning_table); // _ = c.stbtt_GetKerningTable(&info, &kerning_table[0], @intCast(kerning_table.len)); //} const scale = c.stbtt_ScaleForPixelHeight(&info, args.font_height); //var ascent: c_int = undefined; //var descent: c_int = undefined; //var line_gap: c_int = undefined; //c.stbtt_GetFontVMetrics(&info, &ascent, &descent, &line_gap); ////const baseline = @as(c_int, @intFromFloat(@as(f32, @floatFromInt(ascent)) * scale)); //const line_height = @as(c_int, @intFromFloat(@as(f32, @floatFromInt((ascent - descent + line_gap))) * args.line_height_scale * scale)); //std.debug.print("line-gap: {d}\n", .{line_gap}); var font = Font{ .allocator = allocator, .glyphs = .{}, .font_height = args.font_height, .raw = raw, .info = info, .scale = scale, }; try font.glyphs.ensureUnusedCapacity(allocator, 64); errdefer font.glyphs.deinit(allocator); errdefer { var glyphs = font.glyphs.valueIterator(); while (glyphs.next()) |glyph| { glyph.deinit(); } } return font; } pub fn loadGlyph(font: *Font, codepoint: u21) !void { var c_glyph_w: c_int = undefined; var c_glyph_h: c_int = undefined; var c_x_offset: c_int = undefined; var c_y_offset: c_int = undefined; var glyph: Glyph = undefined; const c_bitmap = c.stbtt_GetCodepointBitmap(&font.info, 0, font.scale, codepoint, &c_glyph_w, &c_glyph_h, &c_x_offset, &c_y_offset); defer c.stbtt_FreeBitmap(c_bitmap, null); const glyph_w: u32 = @intCast(c_glyph_w); const glyph_h: u32 = @intCast(c_glyph_h); glyph.image.allocator = font.allocator; glyph.image.pixels = try font.allocator.alloc(Color, glyph_w*glyph_h); errdefer font.allocator.free(glyph.image.pixels); for (0..glyph_w*glyph_h) |j| { const value = c_bitmap[j]; const fvalue = @as(f32, @floatFromInt(value)) / 255; glyph.image.pixels[j].vec = .{1,1,1,fvalue}; } const font_height_2 = @as(V2, @splat(font.font_height)); glyph.image.size = @as(V2, @floatFromInt(V2i{c_glyph_w, c_glyph_h})); glyph.offset = V2{@floatFromInt(c_x_offset), @floatFromInt(c_y_offset)} / font_height_2; try font.glyphs.put(font.allocator, codepoint, glyph); } pub fn deinit(font: *Font) void { var glyphs = font.glyphs.valueIterator(); while (glyphs.next()) |glyph| { glyph.deinit(); } font.glyphs.deinit(font.allocator); font.allocator.free(font.raw); } pub fn getGlyph(font: *Font, cp: u21) ?Glyph { if (font.glyphs.get(cp)) |glyph| { return glyph; } font.loadGlyph(cp) catch return null; return font.glyphs.get(cp); } }; pub const Context = struct { allocator: Allocator, command_buffer: CommandBuffer, area_stack: ArrayListUnmanaged(Area) = .{}, _current_area: Area = .{}, hover_consumed: bool = false, hover_consumed_last_frame: bool = false, dragging: bool = false, dragged_last_frame: bool = false, const InitOptions = struct { allocator: Allocator, command_buffer_initial_capacity: u16 = 128, render_target_stack_initial_capacity: u16 = 1, }; pub fn init(o: InitOptions) !Context { return Context{ .allocator = o.allocator, .command_buffer = try .init(o.command_buffer_initial_capacity, o.allocator), }; } pub fn deinit(ctx: *Context) void { ctx.command_buffer.deinit(); ctx.area_stack.deinit(ctx.allocator); } pub fn frameStart(ctx: *Context) void { mouse_delta = mouse_pos - prev_mouse_pos; ctx.command_buffer.commands.clearRetainingCapacity(); } pub const DrawTarget = struct { pixels: []u8, bytes_per_pixel: u8, pixels_per_row: u32, }; pub fn draw(ctx: *Context, target: DrawTarget) !void { // TODO performance: optimize command buffer to minimize overdraw. This could also allow for more efficient multi-threading prev_mouse_pos = mouse_pos; const ftarget_width: f32 = @floatFromInt(target.pixels_per_row); const ftarget_height: f32 = @floatFromInt(target.pixels.len / target.pixels_per_row / target.bytes_per_pixel); const fdimensions = V2{ftarget_width, ftarget_height}; const commands = ctx.command_buffer.commands.items; if (commands.len == 0) return; var i: u32 = @intCast(commands.len - 1); var color: Color = undefined; var image: ?Image = null; var img_rect: Rect = undefined; var text_mode = false; while (true) { const command = commands[i]; switch (command) { .color => |cc| { color = cc; }, .image => |ii| { image = ii; }, .image_rect => |r| { img_rect = r; }, .rect => |rect| { const scale_factor = @as(V2, @splat(@min(fdimensions[0], fdimensions[1]))); //img_rect = .{.pos = .{0,0.5}, .size = .{1,1}}; try drawRect(target.pixels, image, img_rect, color, rect.pos * scale_factor, rect.size * scale_factor, target.pixels_per_row, text_mode); }, .text_begin => { text_mode = true; RenderQueue.waitUntilAllFinished(); }, .text_end => { text_mode = false; RenderQueue.waitUntilAllFinished(); }, } if (i == 0) break; i -= 1; } assert(ctx.area_stack.items.len == 0); ctx._current_area = .{}; ctx.command_buffer.commands.clearRetainingCapacity(); ctx.hover_consumed_last_frame = ctx.hover_consumed; ctx.hover_consumed = false; ctx.dragged_last_frame = ctx.dragging; RenderQueue.waitUntilAllFinished(); } pub const RectangleOptions = struct { color: Color = .hex(0xFFFF_FFFF), image: ?Image = null, //uvs: [4]V2 = .{.{0, 0}, .{1, 0}, .{0, 1}, .{1, 1}}, hover: ?*bool = null, consume_hover: bool = true, ignore_hover_if_consumed: bool = true, begin_drag_if_hover_and: bool = false, end_drag_if: bool = false, drag: ?*?V2 = null, apply_area_transformations: bool = true, clip_to_area: bool = true, }; pub fn rectangle(ctx: *Context, rect_: Rect, o: RectangleOptions) !void { const current_area_pos = if (o.apply_area_transformations) ctx._current_area.rect.pos else V2{0,0}; const current_area_children_offset = if (o.apply_area_transformations) ctx._current_area.children_offset else V2{0,0}; const pos = rect_.pos + current_area_pos + current_area_children_offset; const size = rect_.size; var rect = Rect{.pos = .{0,0}, .size = .{1,1}}; if (o.clip_to_area) { rect = ctx._current_area.rect.intersection(.{.pos = pos, .size = size}); } if (@reduce(.Or, rect.size == V2{0,0})) return; var img_rect: Rect = .{.pos = .{0,0}, .size = .{1,1}}; if (o.clip_to_area) { assert(@reduce(.Or, size != V2{0,0})); // We already checked rect.size, so size itself should never be 0 img_rect.pos = @max(V2{0,0}, -pos)/size; img_rect.size = @max(V2{0,0}, @min(V2{1, 1}, V2{1,1} - (pos + size - (ctx._current_area.rect.size))/size)) - img_rect.pos; } try ctx.command_buffer.rect(rect); try ctx.command_buffer.image_rect(img_rect); try ctx.command_buffer.image(o.image); try ctx.command_buffer.color(o.color); const hovering = rect.contains(mouse_pos); if (o.hover) |h| { h.* = false; } if (!o.ignore_hover_if_consumed or !ctx.hover_consumed) { if (o.hover) |h| { h.* = hovering; if (h.*) { ctx.hover_consumed = true; } } if (o.end_drag_if) { ctx.dragging = false; } const hovered_last_frame = rect.contains(prev_mouse_pos); if (hovered_last_frame) { if (o.begin_drag_if_hover_and and o.drag != null) { ctx.dragging = true; } if (o.drag != null and ctx.dragging) { const d = o.drag.?; // note that we need to check whether we were hovering during the last frame, // because the cursor may have been dragged outside of this rectangle if (hovered_last_frame) { d.* = mouse_delta; ctx.hover_consumed = true; ctx.dragging = true; ctx.dragged_last_frame = true; } } } } if (o.consume_hover) { ctx.hover_consumed = ctx.hover_consumed or hovering; } } pub fn areaBegin(ctx: *Context, area: Area) !void { try ctx.area_stack.append(ctx.allocator, area); ctx._current_area.intersection(area); } pub fn areaEnd(ctx: *Context) !void { _ = ctx.area_stack.pop(); ctx._current_area = .{}; for (ctx.area_stack.items) |area| { ctx._current_area.intersection(area); } } pub const TextWriter = struct { interface: std.Io.Writer, ctx: *Context, font: *Font, scale: V2, rect: Rect, color: Color, hovered_glyph: ?*GlyphIndex, consume_hover: bool, glyph_index: i15 = 0, glyph_offset: V2 = .{0, 0}, pub fn drain(io_w: *std.Io.Writer, data: []const []const u8, splat: usize) !usize { const w: *@This() = @alignCast(@fieldParentPtr("interface", io_w)); _ = w.writeAll(io_w.buffer[0..io_w.end]) catch return error.WriteFailed; io_w.end = 0; var acc: usize = 0; for (data) |dat| { if (dat.len == 0) continue; acc += w.writeAll(dat) catch return error.WriteFailed; } if (splat > 0) { for (1..splat) |_| { acc += w.writeAll(data[data.len-1]) catch return error.WriteFailed; } } return acc; } pub fn writeAll(w: *TextWriter, bytes: []const u8) !usize { const r = w.rect; var iter = std.unicode.Utf8Iterator{.bytes = bytes, .i = 0}; const font_height_2: V2 = @splat(w.font.font_height); while (iter.nextCodepoint()) |cp| : (w.glyph_index += 1) { const codepoint_info = w.font.getGlyph(cp) orelse return error.NoGlyph; const size = codepoint_info.image.size / font_height_2; var hover: bool = undefined; try w.ctx.rectangle( .{.pos = r.pos + w.glyph_offset + (V2{0,1}+codepoint_info.offset) * w.scale, .size = size * w.scale}, .{ .image = codepoint_info.image, .color = w.color, .hover = if (w.hovered_glyph != null) &hover else null, .consume_hover = w.consume_hover, }); if (w.hovered_glyph) |hg| { if (hover) { hg.* = w.glyph_index; } } // TODO use advanceWidth and leftSideBearing instead of size w.glyph_offset += V2{(size[0] + codepoint_info.offset[0]), 0} * w.scale; } return bytes.len; } }; pub const TextOptions = struct { font: *Font, color: Color = .hex(0xFFFFFFFF), hover: ?*bool = null, consume_hover: bool = true, hovered_glyph: ?*GlyphIndex = null, scale: f32 = 0.05, //TODO align, overflow, rich text }; pub fn textFmt(ctx: *Context, comptime fmt: []const u8, args: anytype, rect: Rect, o: TextOptions) !void { try ctx.command_buffer.textEnd(); var buf = [_]u8{0}**16; const vtable = std.Io.Writer.VTable{ .drain = TextWriter.drain, }; var hovered_glyph: GlyphIndex = -1; var writer = TextWriter{ .interface = std.Io.Writer{ .buffer = &buf, .vtable = &vtable, }, .ctx = ctx, .rect = rect, .font = o.font, .color = o.color, .scale = @as(V2, @splat(o.scale)), .hovered_glyph = &hovered_glyph, // TODO option to ignore for performance, maybe .consume_hover = o.consume_hover, }; try writer.interface.print(fmt, args); try writer.interface.flush(); if (o.hovered_glyph) |hg| { hg.* = hovered_glyph; } if (o.hover) |h| { h.* = hovered_glyph != -1; if (!ctx.hover_consumed) { h.* = h.* or rect.contains(mouse_pos); if (o.consume_hover) { ctx.hover_consumed = ctx.hover_consumed or h.*; } } } try ctx.command_buffer.textBegin(); } pub const TextFieldOptions = struct { text_options: TextOptions, cursor_to_hovered_glpyh_if: bool = false, cursor_move_left: bool = false, cursor_move_right: bool = false, }; pub fn textField(ctx: *Context, str: *TextBuffer, to_insert_utf8: []const u8, rect: Rect, o: TextFieldOptions) !void { var text_options = o.text_options; var hovered_glyph: GlyphIndex = -1; text_options.hovered_glyph = &hovered_glyph; if (str.focus and str.cursor_index >= 0) { const count = try std.unicode.utf8CountCodepoints(to_insert_utf8); if (count > 0) { try str.al.insertSlice(str.allocator, @intCast(str.cursor_index), to_insert_utf8); str.cursor_index += @intCast(to_insert_utf8.len); } } try ctx.textFmt("{s}", .{str.toString()}, rect, text_options); if (o.cursor_to_hovered_glpyh_if) { str.focus = false; if (hovered_glyph != -1) { str.focus = true; str.cursor_index = hovered_glyph; } } if (o.text_options.hovered_glyph) |hg| { hg.* = hovered_glyph; } str.cursor_index = @min(str.cursor_index, str.toString().len); if (str.focus) { if (o.cursor_move_left) { str.cursorMoveLeft(); } if (o.cursor_move_right) { str.cursorMoveRight(); } if (0 <= str.cursor_index and str.cursor_index < str.toString().len) { const text_commands = ctx.getLastTextCommandSequence(); const idx = str.getCodepointIndexAtCursor()*4; if (idx < text_commands.len) { const glyph_rect = text_commands[idx].rect; try ctx.rectangle(glyph_rect, .{ .apply_area_transformations = false, .clip_to_area = true, }); } } } } pub fn getLastTextCommandSequence(ctx: Context) []Command { if (ctx.command_buffer.commands.items.len < 2) return &.{}; var end: usize = ctx.command_buffer.commands.items.len - 1; while (true) { if (ctx.command_buffer.commands.items[end] == .text_begin) break; if (end == 0) return &.{}; end -= 1; } var start = end - 1; while (true) { if (ctx.command_buffer.commands.items[start] == .text_end) break; assert(start != 0); // can't happen, since we already found end start -= 1; } return ctx.command_buffer.commands.items[start+1..end]; } pub fn textBufferFromString(ctx: Context, str: []const u8) !TextBuffer { const allocator = ctx.allocator; var al = try ArrayListUnmanaged(u8).initCapacity(allocator, @max(16, str.len)); try al.appendSlice(allocator, str); return .{ .allocator = allocator, .al = al, }; } }; pub const TextBuffer = struct { allocator: Allocator, al: ArrayListUnmanaged(u8) = .{}, cursor_index: GlyphIndex = 0, focus: bool = false, pub fn deinit(text_buffer: *TextBuffer) void { text_buffer.al.deinit(text_buffer.allocator); } pub fn toString(text_buffer: TextBuffer) []u8 { return text_buffer.al.items; } pub fn len(tb: TextBuffer) u32 { return tb.al.items.len; } pub fn cursorMoveLeft(tb: *TextBuffer) void { while (tb.cursor_index > 0) { tb.cursor_index -= 1; if ((tb.al.items[@intCast(tb.cursor_index)] & 0b1100_0000) != 0b1000_0000) return; } } pub fn cursorMoveRight(tb: *TextBuffer) void { while (tb.cursor_index < tb.al.items.len) { tb.cursor_index += 1; if (tb.cursor_index == tb.al.items.len) return; if ((tb.al.items[@intCast(tb.cursor_index)] & 0b1100_0000) != 0b1000_0000) return; } } pub fn getCodepointIndexAtCursor(tb: TextBuffer) u32 { var iter = std.unicode.Utf8Iterator{.bytes = tb.toString(), .i = 0}; var i: i32 = 0; var codepoint_index: u32 = 0; while (true) { if (i == tb.cursor_index) return codepoint_index; codepoint_index += 1; const cp = iter.nextCodepoint() orelse return codepoint_index; const cp_len = std.unicode.utf8CodepointSequenceLength(cp) catch unreachable; i += cp_len; } } }; // p in [0..1] pub fn setMousePos(p: V2) void { mouse_pos = p; } // === RENDERING === pub fn clear(buffer: []u8) void { @memset(buffer, 0); } pub const RenderQueue = struct { var allocator: Allocator = undefined; var threads: []Thread = undefined; pub var cpu_count: usize = undefined; var work_queue: ArrayListUnmanaged(CallArgs) = .{}; var work_mutex = Thread.Mutex{}; var work_enqueued = Thread.Condition{}; var work_done = Thread.Condition{}; var run: std.atomic.Value(bool) = .init(true); var currently_rendering_threads: std.atomic.Value(u32) = .init(0); pub fn init(allocator_: Allocator) !void { allocator = allocator_; cpu_count = @max(1, try Thread.getCpuCount()); //cpu_count = 1; threads = try allocator.alloc(Thread, cpu_count); work_queue = try .initCapacity(allocator, cpu_count); for (threads) |*t| { t.* = try Thread.spawn(.{.allocator = allocator}, doWork, .{}); } } pub fn deinit() void { run.store(false, .seq_cst); work_enqueued.broadcast(); for (threads) |t| { t.join(); } allocator.free(threads); work_queue.deinit(allocator); } pub const CallArgs = struct { target: []u8, img_pixels: []const V4, img_offset: V2, img_size: V2, img_stride: f32, img_factor: V2, color: Color, pos: V2i, size: V2i, pixels_per_row: i32, y_start: i32, group_y_size: u32 }; // NOTE: order in which work will be executed is undefined pub fn dispatch(work: CallArgs) !void { { work_mutex.lock(); defer work_mutex.unlock(); try work_queue.append(allocator, work); } work_enqueued.signal(); } pub fn waitUntilAllFinished() void { work_mutex.lock(); defer work_mutex.unlock(); while (work_queue.items.len > 0 or currently_rendering_threads.load(.seq_cst) > 0) { work_done.wait(&work_mutex); } } fn doWork() void { while (run.load(.seq_cst)) { var work: ?CallArgs = null; { work_mutex.lock(); defer work_mutex.unlock(); if (work_queue.items.len == 0) { work_enqueued.wait(&work_mutex); } if (work_queue.items.len > 0) { work = work_queue.pop(); } } if (work) |w| { _ = currently_rendering_threads.rmw(.Add, 1, .seq_cst); @call(.auto, drawRectThreaded, .{w}); _ = currently_rendering_threads.rmw(.Sub, 1, .seq_cst); work_mutex.lock(); defer work_mutex.unlock(); work_done.broadcast(); } } } }; pub fn drawRect(target: []u8, image: ?Image, img_rect: Rect, color: Color, pos: @Vector(2, f32), size: @Vector(2, f32), upixels_per_row: u32, rendering_text: bool) !void { const pixels_per_row = @as(i32, @intCast(upixels_per_row)); const single_white_pixel = [1]@Vector(4, f32){.{1,1,1,1}}; var img_pixels: []const V4 = &single_white_pixel; var img_offset: V2 = .{0,0}; var img_size: V2 = .{1,1}; var img_factor: V2 = .{0, 0}; var img_stride: f32 = 1; if (image) |img| { img_pixels = @ptrCast(img.pixels); img_offset = img.size * img_rect.pos; img_size = img.size * img_rect.size; img_factor = img_size / size; img_stride = img.size[0]; } const ipos = @as(V2i, @intFromFloat(pos)); const isiz = @as(V2i, @intFromFloat(size)); if (isiz[0] == 0 or isiz[1] == 0) return; const render_thread_count: u32 = blk: { const minimum_pixels_per_thread = 128; // somewhat arbitrary, but this gives good results on my Ryzen 5 3600 const target_cpu_count = (size[0] * size[1] / minimum_pixels_per_thread); break :blk @max(1, @min(@as(u32, @intFromFloat(target_cpu_count)), RenderQueue.cpu_count)); }; const group_y_count = @abs(isiz[1])/render_thread_count; if (!rendering_text) { // if we're rendering text, we know there won't be any overdraw, // so we don't have to wait for the previous rects to finish rendering. // We handle beginning and end synchronization in `draw` RenderQueue.waitUntilAllFinished(); } for (0..render_thread_count) |i| { const y_start: i32 = @intCast(group_y_count*i); const call_args = RenderQueue.CallArgs{ .target = target, .img_pixels = img_pixels, .img_offset = img_offset, .img_size = img_size, .img_stride = img_stride, .img_factor = img_factor, .color = color, .pos = ipos, .size = isiz, .pixels_per_row = pixels_per_row, .y_start = y_start, .group_y_size = if (i == render_thread_count - 1) @as(u32, @intCast(isiz[1] - y_start)) else group_y_count }; try RenderQueue.dispatch(call_args); } } pub fn drawRectThreaded(args: RenderQueue.CallArgs) void { const target = args.target; const img_pixels = args.img_pixels; const img_offset = args.img_offset; const img_size = args.img_size; _ = img_size; const img_stride = args.img_stride; const img_factor = args.img_factor; const color = args.color; const pos = args.pos; const size = args.size; const pixels_per_row = args.pixels_per_row; const y_start = args.y_start; const group_y_size = args.group_y_size; var y: i32 = y_start; for (0..group_y_size) |_| { defer y += 1; const yy = (pos[1] + y) * pixels_per_row; const fy = @as(f32, @floatFromInt(y)); const img_y = @floor(img_factor[1] * fy + img_offset[1]); var x: i32 = 0; for (0..@intCast(size[0])) |_| { defer x += 1; const idx = @as(u32, @intCast((pos[0] + x + yy) * 4)); const img_x = img_factor[0] * @as(f32, @floatFromInt(x)) + img_offset[0]; const fimage_idx = @floor(img_y * img_stride + img_x); const image_idx = @as(usize, @intFromFloat(fimage_idx)); const image_value = img_pixels[image_idx]; if (image_value[3] == 0) continue; // skip transparent (~10% performance increase when rendering text, barely measurable decrease for solid rectangle) const final_value = color.vec * image_value; const prev_pixel = V4{ @as(f32, @floatFromInt(target[idx+0])), @as(f32, @floatFromInt(target[idx+1])), @as(f32, @floatFromInt(target[idx+2])), @as(f32, @floatFromInt(target[idx+3])) } / @as(V4, @splat(255)); const alpha = final_value[3]; const alphad_value = final_value * V4{alpha,alpha,alpha,1} + prev_pixel * @as(V4, @splat(1-alpha)); const final_vvalue: @Vector(4, u8) = @intFromFloat(alphad_value * @as(@Vector(4, f32), @splat(255))); const final_ivalue: [4]u8 = final_vvalue; // @sizeOf(@Vector(4, u8)) is undefined const final_ptr = @as([]const u8, @ptrCast(&final_ivalue)); assert(final_ptr.len == 4); @memcpy(target[idx..idx+4], final_ptr); } } } // === C ABI === pub fn exportCAbi () void { if (!@inComptime()) @compileError("Must be called at comptime"); const prefix = ""; const c_compat = struct { const root = @import("root"); pub const CRect = extern struct { x: f32, y: f32, w: f32, h: f32, }; pub const InitFn = fn () callconv(.c) bool; pub const LoopFn = fn (dt: f32) callconv(.c) bool; pub const DeinitFn = fn () callconv(.c) void; pub fn c_run( init_fn: *const InitFn, loop_fn: *const LoopFn, deinit_fn: *const DeinitFn, ) callconv(.c) bool { const static = struct { pub const Pixel = packed struct { r: u8 = 0, g: u8 = 0, b: u8 = 0, _a: u8 = undefined, }; var pixel_buffer: *const []Pixel = undefined; var window_size: @Vector(2, u32) = undefined; var fwindow_size: @Vector(2, f32) = undefined; pub fn setWindowSize(ws: @Vector(2, c_uint)) void { window_size = @intCast(ws); fwindow_size = @floatFromInt(window_size); pixel_buffer = @ptrCast(root.pixels); } pub var internal_init: *const InitFn = undefined; pub fn init_wrapper() !void { try globalInit(root.allocator); errdefer globalDeinit(); if (!internal_init()) return error.InternalError; } pub var internal_loop: *const LoopFn = undefined; pub fn loop_wrapper(dt: f32) !bool { if (!internal_loop(dt)) return false; clear(@ptrCast(pixel_buffer.*)); var context_iter = contexts.iterator(0); while (context_iter.next()) |ctx| { try ctx.draw(.{ .pixels = @ptrCast(pixel_buffer.*), .bytes_per_pixel = @sizeOf(Pixel), .pixels_per_row = window_size[0], }); } return true; } pub var internal_deinit: *const DeinitFn = undefined; pub fn deinit_wrapper() void { internal_deinit(); globalDeinit(); contexts.clearAndFree(root.allocator); } }; static.internal_init = init_fn; static.internal_loop = loop_fn; static.internal_deinit = deinit_fn; root.run( static.init_wrapper, static.setWindowSize, static.loop_wrapper, static.deinit_wrapper ) catch return false; return true; } var contexts = std.SegmentedList(Context, 4){}; pub fn c_context_init() callconv(.c) ?*Context { var ctx = Context.init(.{.allocator = root.allocator}) catch return null; contexts.append(root.allocator, ctx) catch { ctx.deinit(); return null; }; return contexts.at(contexts.count()-1); } pub fn c_context_deinit(ctx: *Context) callconv(.c) void { ctx.deinit(); var i: usize = 0; var context_iter = contexts.iterator(0); while (context_iter.next()) |registered_context| { defer i += 1; if (registered_context == ctx) { contexts.at(i).* = contexts.at(contexts.count()-1).*; _ = contexts.pop(); break; } } } const CRectangleOptions = extern struct { color: [4]f32, image: ?*Image, hover: ?*bool, consume_hover: bool, ignore_hover_if_consumed: bool, begin_drag_if_hover_and: bool, end_drag_if: bool, drag: ?*V2, }; pub fn c_rectangle(ctx: *Context, rect: CRect, o: CRectangleOptions) callconv(.c) bool { ctx.rectangle(.{.pos = .{rect.x, rect.y}, .size = .{rect.w, rect.h}}, .{ .color = .{.vec = o.color}, .image = if (o.image) |img| img.* else null, .hover = o.hover, .consume_hover = o.consume_hover, .ignore_hover_if_consumed = o.ignore_hover_if_consumed, .begin_drag_if_hover_and = o.begin_drag_if_hover_and, .end_drag_if = o.end_drag_if, //.drag = TODO }) catch return false; return true; } }; @export(&c_compat.c_run, .{.name = prefix++"run", .linkage = .strong}); @export(&c_compat.c_context_init, .{.name = prefix++"context_init", .linkage = .strong}); @export(&c_compat.c_context_deinit, .{.name = prefix++"context_deinit", .linkage = .strong}); @export(&c_compat.c_rectangle, .{.name = prefix++"rectangle_", .linkage = .strong}); }