pub const colors = @import("./colors.zig"); const hid = @import("hid"); const ona = @import("ona"); const ext = @cImport({ @cInclude("SDL2/SDL.h"); }); const rendering = @import("./rendering.zig"); const std = @import("std"); pub const Assets = struct { window: *ext.SDL_Window, texture_formats: ona.stack.Sequential(TextureFormat), frame_rendered: std.Thread.ResetEvent = .{}, pending_work: WorkQueue = .{}, has_worker_thread: ?std.Thread = null, pub const LoadError = std.mem.Allocator.Error; pub const LoadFileError = LoadError || ona.files.ReadAllError || error { FormatUnsupported, }; pub const TextureFormat = struct { extension: []const u8, load_file: *const fn (*std.heap.ArenaAllocator, ona.files.Storage, []const u8) LoadFileError!Texture.Desc, }; pub const WorkQueue = ona.asyncio.BlockingQueue(1024, union (enum) { load_effect: LoadEffectWork, load_texture: LoadTextureWork, render_frame: RenderFrameWork, shutdown, unload_effect: UnloadEffectWork, unload_texture: UnloadTextureWork, const LoadEffectWork = struct { desc: Effect.Desc, loaded: *ona.asyncio.Future(std.mem.Allocator.Error!Effect), }; const LoadTextureWork = struct { desc: Texture.Desc, loaded: *ona.asyncio.Future(std.mem.Allocator.Error!Texture), }; const RenderFrameWork = struct { clear_color: Color, width: u16, height: u16, finished: *std.Thread.ResetEvent, has_command_params: ?*ona.Params(Commands).Node, }; const UnloadEffectWork = struct { handle: Effect, }; const UnloadTextureWork = struct { handle: Texture, }; }); fn deinit(self: *Assets) void { self.pending_work.enqueue(.shutdown); if (self.has_worker_thread) |worker_thread| { worker_thread.join(); } self.texture_formats.deinit(); self.* = undefined; } fn init() !Assets { const window = create: { const position = ext.SDL_WINDOWPOS_CENTERED; const flags = ext.SDL_WINDOW_OPENGL; const width = 640; const height = 480; break: create ext.SDL_CreateWindow("Ona", position, position, width, height, flags) orelse { return error.Unsupported; }; }; errdefer { ext.SDL_DestroyWindow(window); } return .{ .texture_formats = .{.allocator = ona.heap.allocator}, .window = window, }; } pub fn load_effect_file(self: *Assets, storage: ona.files.Storage, path: []const u8) LoadFileError!Effect { if (!std.mem.endsWith(u8, path, ".spv")) { return error.FormatUnsupported; } const fragment_file_stat = try storage.stat(path); const fragment_spirv_ops = try ona.heap.allocator.alloc(u32, fragment_file_stat.size / @alignOf(u32)); defer { ona.heap.allocator.free(fragment_spirv_ops); } const bytes_read = try storage.read_all(path, std.mem.sliceAsBytes(fragment_spirv_ops), .{}); std.debug.assert(bytes_read.len == fragment_file_stat.size); var loaded = ona.asyncio.Future(std.mem.Allocator.Error!Effect){}; self.pending_work.enqueue(.{ .load_effect = .{ .desc = .{ .fragment_spirv_ops = fragment_spirv_ops, }, .loaded = &loaded, }, }); return loaded.get().*; } pub fn load_texture(self: *Assets, desc: Texture.Desc) std.mem.Allocator.Error!Texture { var loaded = ona.asyncio.Future(std.mem.Allocator.Error!Texture){}; self.pending_work.enqueue(.{ .load_texture = .{ .desc = desc, .loaded = &loaded, }, }); return loaded.get().*; } pub fn load_texture_file(self: *Assets, storage: ona.files.Storage, path: []const u8) LoadFileError!Texture { var arena = std.heap.ArenaAllocator.init(ona.heap.allocator); defer { arena.deinit(); } for (self.texture_formats.values) |format| { if (!std.mem.endsWith(u8, path, format.extension)) { continue; } return self.load_texture(try format.load_file(&arena, storage, path)); } return error.FormatUnsupported; } pub const thread_restriction = .main; }; pub const Color = @Vector(4, f32); pub const Commands = struct { pending: *List, const Command = union (enum) { draw_texture: DrawTextureCommand, set_effect: SetEffectCommand, set_target: SetTargetCommand, }; pub const DrawTextureCommand = struct { texture: Texture, transform: Transform2D, }; pub const SetEffectCommand = struct { effect: Effect, properties: []const u8, }; pub const SetTargetCommand = struct { texture: Texture, clear_color: ?Color, clear_depth: ?f32, clear_stencil: ?u8, }; pub const List = struct { arena: std.heap.ArenaAllocator, stack: ona.stack.Sequential(Command), fn clear(self: *List) void { self.stack.clear(); if (!self.arena.reset(.retain_capacity)) { std.log.warn("failed to reset the buffer of a gfx queue with retained capacity", .{}); } } fn deinit(self: *List) void { self.arena.deinit(); self.stack.deinit(); self.* = undefined; } fn init(allocator: std.mem.Allocator) List { return .{ .arena = std.heap.ArenaAllocator.init(allocator), .stack = .{.allocator = allocator}, }; } }; pub const Param = struct { swap_lists: [2]List, swap_state: u1 = 0, fn deinit(self: *Param) void { for (&self.swap_lists) |*list| { list.deinit(); } self.* = undefined; } fn pending_list(self: *Param) *List { return &self.swap_lists[self.swap_state]; } fn rotate(self: *Param) void { const swapped_state = self.swap_state ^ 1; self.swap_lists[swapped_state].clear(); self.swap_state = swapped_state; } pub fn submitted_commands(self: Param) []const Command { return self.swap_lists[self.swap_state ^ 1].stack.values; } }; pub fn bind(_: ona.World.BindContext) std.mem.Allocator.Error!Param { return .{ .swap_lists = .{ List.init(ona.heap.allocator), List.init(ona.heap.allocator), }, }; } pub fn init(param: *Param) Commands { return .{ .pending = param.pending_list(), }; } pub fn draw_texture(self: Commands, command: DrawTextureCommand) std.mem.Allocator.Error!void { try self.pending.stack.push_grow(.{.draw_texture = command}); } pub fn set_effect(self: Commands, command: SetEffectCommand) std.mem.Allocator.Error!void { try self.pending.stack.push_grow(.{ .set_effect = .{ .properties = try self.pending.arena.allocator().dupe(u8, command.properties), .effect = command.effect, }, }); } pub fn unbind(param: *Param, _: ona.World.UnbindContext) void { param.deinit(); } pub fn set_target(self: Commands, command: SetTargetCommand) std.mem.Allocator.Error!void { try self.pending.stack.push_grow(.{.set_target = command}); } }; pub const Display = struct { width: u16 = 1280, height: u16 = 720, clear_color: Color = colors.black, }; pub const Effect = enum (u32) { default, _, pub const Desc = struct { fragment_spirv_ops: []const u32, }; }; pub const Rect = struct { left: f32, top: f32, right: f32, bottom: f32, }; pub const Texture = enum (u32) { default, backbuffer, _, pub const Desc = struct { format: Format, access: Access, pub const Access = union (enum) { static: StaticAccess, render: RenderAccess, }; pub const StaticAccess = struct { width: u16, data: []const u8, }; pub const RenderAccess = struct { width: u16, height: u16, }; }; pub const Format = enum { rgba8, bgra8, pub fn byte_size(self: Format) usize { return switch (self) { .rgba8, .bgra8 => 4, }; } }; }; pub const Transform2D = extern struct { xbasis: Vector = .{1, 0}, ybasis: Vector = .{0, 1}, origin: Vector = @splat(0), pub const Simplified = struct { translation: Vector = @splat(0), rotation: f32 = 0, scale: Vector = @splat(1), skew: f32 = 0, }; pub const Vector = @Vector(2, f32); pub fn from_simplified(simplified: Simplified) Transform2D { const rotation_skew = simplified.rotation + simplified.skew; return .{ .xbasis = simplified.scale * Vector{std.math.cos(simplified.rotation), std.math.sin(simplified.rotation)}, .ybasis = simplified.scale * Vector{-std.math.sin(rotation_skew), std.math.cos(rotation_skew)}, .origin = simplified.translation, }; } pub fn translated(self: Transform2D, translation: Vector) Transform2D { var transform = self; transform.origin += translation; return transform; } }; fn load_bmp_texture(arena: *std.heap.ArenaAllocator, storage: ona.files.Storage, path: []const u8) !Texture.Desc { const header = try storage.read_little(path, 0, extern struct { type: [2]u8 align (1), file_size: u32 align (1), reserved: [2]u16 align (1), image_offset: u32 align (1), header_size: u32 align (1), pixel_width: i32 align (1), pixel_height: i32 align (1), color_planes: u16 align (1), bits_per_pixel: u16 align (1), compression_method: u32 align (1), image_size: u32 align(1), pixels_per_meter_x: i32 align (1), pixels_per_meter_y: i32 align (1), palette_colors_used: u32 align (1), important_colors_used: u32 align (1), }) orelse { return error.FormatUnsupported; }; if (!std.mem.eql(u8, &header.type, "BM")) { return error.FormatUnsupported; } const pixel_width = std.math.cast(u16, header.pixel_width) orelse { return error.FormatUnsupported; }; const pixels = try arena.allocator().alloc(u8, header.image_size); const bytes_per_pixel = header.bits_per_pixel / @bitSizeOf(u8); const alignment = 4; const byte_stride = pixel_width * bytes_per_pixel; const padded_byte_stride = alignment * @divTrunc((byte_stride + alignment - 1), alignment); const byte_padding = ona.scalars.sub(padded_byte_stride, byte_stride) orelse 0; var buffer_offset: usize = 0; var file_offset = @as(usize, header.image_offset); switch (header.bits_per_pixel) { 32 => { while (buffer_offset < pixels.len) { const line = pixels[buffer_offset .. buffer_offset + byte_stride]; if (try storage.read(path, line, file_offset) != byte_stride) { return error.FormatUnsupported; } for (0 .. pixel_width) |i| { const line_offset = i * 4; const pixel = line[line_offset .. line_offset + 4]; std.mem.swap(u8, &pixel[0], &pixel[2]); } file_offset += line.len + byte_padding; buffer_offset += padded_byte_stride; } }, else => return error.FormatUnsupported, } return .{ .format = .rgba8, .access = .{ .static = .{ .width = pixel_width, .data = pixels, }, }, }; } pub fn poll(app: ona.Write(ona.App), events: ona.Send(hid.Event)) !void { var event = @as(ext.SDL_Event, undefined); while (ext.SDL_PollEvent(&event) != 0) { switch (event.type) { ext.SDL_QUIT => { app.state.quit(); }, ext.SDL_KEYUP => { try events.push(.{.key_up = @enumFromInt(event.key.keysym.scancode)}); }, ext.SDL_KEYDOWN => { try events.push(.{.key_down = @enumFromInt(event.key.keysym.scancode)}); }, ext.SDL_MOUSEBUTTONUP => { try events.push(.{ .mouse_up = switch (event.button.button) { ext.SDL_BUTTON_LEFT => .left, ext.SDL_BUTTON_RIGHT => .right, ext.SDL_BUTTON_MIDDLE => .middle, else => unreachable, }, }); }, ext.SDL_MOUSEBUTTONDOWN => { try events.push(.{ .mouse_down = switch (event.button.button) { ext.SDL_BUTTON_LEFT => .left, ext.SDL_BUTTON_RIGHT => .right, ext.SDL_BUTTON_MIDDLE => .middle, else => unreachable, }, }); }, ext.SDL_MOUSEMOTION => { try events.push(.{ .mouse_motion = .{ .relative_position = .{@floatFromInt(event.motion.xrel), @floatFromInt(event.motion.yrel)}, .absolute_position = .{@floatFromInt(event.motion.x), @floatFromInt(event.motion.y)}, }, }); }, else => {}, } } } pub fn setup(world: *ona.World, events: ona.App.Events) (error {Unsupported} || std.Thread.SpawnError || std.mem.Allocator.Error)!void { if (ext.SDL_Init(ext.SDL_INIT_VIDEO | ext.SDL_INIT_EVENTS) != 0) { return error.Unsupported; } const assets = create: { var assets = try Assets.init(); errdefer { assets.deinit(); } break: create try world.set_get_state(assets); }; assets.frame_rendered.set(); errdefer { assets.deinit(); } assets.has_worker_thread = try std.Thread.spawn(.{}, rendering.process_work, .{ &assets.pending_work, assets.window, }); const builtin_texture_formats = [_]Assets.TextureFormat{ .{ .extension = "bmp", .load_file = load_bmp_texture, }, }; for (builtin_texture_formats) |format| { try assets.texture_formats.push_grow(format); } try world.set_state(Display{}); try world.on_event(events.pre_update, ona.system_fn(poll), .{.label = "poll gfx"}); try world.on_event(events.exit, ona.system_fn(stop), .{.label = "stop gfx"}); try world.on_event(events.finish, ona.system_fn(synchronize), .{.label = "synchronize gfx"}); } pub fn stop(assets: ona.Write(Assets)) void { assets.state.deinit(); } pub fn synchronize(exclusive: ona.Exclusive(&.{Assets, Display})) !void { const assets, const display = exclusive.states; assets.frame_rendered.wait(); assets.frame_rendered.reset(); { var has_command_param = exclusive.world.get_params(Commands).has_head; while (has_command_param) |command_param| : (has_command_param = command_param.has_next) { command_param.param.rotate(); } } var display_width, var display_height = [_]c_int{0, 0}; ext.SDL_GL_GetDrawableSize(assets.window, &display_width, &display_height); if (display.width != display_width or display.height != display_height) { ext.SDL_SetWindowSize(assets.window, display.width, display.height); } if (exclusive.world.get_params(Commands).has_head) |command_param| { assets.pending_work.enqueue(.{ .render_frame = .{ .has_command_params = command_param, .width = display.width, .height = display.height, .clear_color = display.clear_color, .finished = &assets.frame_rendered, }, }); } else { assets.frame_rendered.set(); } }