diff --git a/src/coral/coral.zig b/src/coral/coral.zig index 24c5946..d760db7 100644 --- a/src/coral/coral.zig +++ b/src/coral/coral.zig @@ -2,6 +2,8 @@ pub const ascii = @import("./ascii.zig"); pub const dag = @import("./dag.zig"); +pub const files = @import("./files.zig"); + pub const hashes = @import("./hashes.zig"); pub const heap = @import("./heap.zig"); diff --git a/src/coral/files.zig b/src/coral/files.zig new file mode 100644 index 0000000..273202f --- /dev/null +++ b/src/coral/files.zig @@ -0,0 +1,124 @@ +const builtin = @import("builtin"); + +const io = @import("./io.zig"); + +const std = @import("std"); + +pub const Error = error { + FileNotFound, + FileInaccessible, +}; + +pub const Stat = struct { + size: u64, +}; + +pub const Storage = struct { + userdata: *anyopaque, + vtable: *const VTable, + + pub const VTable = struct { + stat: *const fn (*anyopaque, []const u8) Error!Stat, + read: *const fn (*anyopaque, []const u8, usize, []io.Byte) Error!usize, + }; + + pub fn read_bytes(self: Storage, path: []const u8, offset: usize, output: []io.Byte) Error!usize { + return self.vtable.read(self.userdata, path, offset, output); + } + + pub fn read_foreign(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type { + const decoded = (try self.read_native(path, offset, Type)) orelse { + return null; + }; + + return switch (@typeInfo(Type)) { + .Struct => std.mem.byteSwapAllFields(Type, &decoded), + else => @byteSwap(decoded), + }; + } + + pub fn read_native(self: Storage, path: []const u8, offset: usize, comptime Type: type) Error!?Type { + var buffer = @as([@sizeOf(Type)]io.Byte, undefined); + + if (try self.vtable.read(self.userdata, path, offset, &buffer) != buffer.len) { + return null; + } + + return @as(*align(1) const Type, @ptrCast(&buffer)).*; + } + + pub const read_little = switch (native_endian) { + .little => read_native, + .big => read_foreign, + }; + + pub const read_big = switch (native_endian) { + .little => read_foreign, + .big => read_native, + }; +}; + +pub const bundle = init: { + const Bundle = struct { + fn full_path(path: []const u8) Error![4095:0]u8 { + var buffer = [_:0]u8{0} ** 4095; + + _ = std.fs.cwd().realpath(path, &buffer) catch { + return error.FileInaccessible; + }; + + return buffer; + } + + fn read(_: *anyopaque, path: []const u8, offset: usize, output: []io.Byte) Error!usize { + var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { + return switch (open_error) { + error.FileNotFound => error.FileNotFound, + else => error.FileInaccessible, + }; + }; + + defer file.close(); + + if (offset != 0) { + file.seekTo(offset) catch { + return error.FileInaccessible; + }; + } + + return file.read(output) catch error.FileInaccessible; + } + + fn stat(_: *anyopaque, path: []const u8) Error!Stat { + const file_stat = get: { + var file = std.fs.openFileAbsoluteZ(&(try full_path(path)), .{.mode = .read_only}) catch |open_error| { + return switch (open_error) { + error.FileNotFound => error.FileNotFound, + else => error.FileInaccessible, + }; + }; + + defer file.close(); + + break: get file.stat() catch { + return error.FileInaccessible; + }; + }; + + return .{ + .size = file_stat.size, + }; + } + }; + + break: init Storage{ + .userdata = undefined, + + .vtable = &.{ + .stat = Bundle.stat, + .read = Bundle.read, + }, + }; +}; + +const native_endian = builtin.cpu.arch.endian(); diff --git a/src/coral/io.zig b/src/coral/io.zig index 8177525..214ce8e 100644 --- a/src/coral/io.zig +++ b/src/coral/io.zig @@ -6,23 +6,30 @@ const slices = @import("./slices.zig"); const std = @import("std"); -pub const Byte = u8; +pub const Writable = struct { + data: []Byte, -pub const Decoder = coral.io.Functor(coral.io.Error!void, &.{[]coral.Byte}); + pub fn writer(self: *Writable) Writer { + return Writer.bind(Writable, self, write); + } + + fn write(self: *Writable, buffer: []const u8) !usize { + const range = @min(buffer.len, self.data.len); + + @memcpy(self.data[0 .. range], buffer[0 .. range]); + + self.data = self.data[range ..]; + + return buffer.len; + } +}; + +pub const Byte = u8; pub const Error = error { UnavailableResource, }; -pub fn FixedBuffer(comptime len: usize, comptime default_value: anytype) type { - const Value = @TypeOf(default_value); - - return struct { - filled: usize = 0, - values: [len]Value = [_]Value{default_value} ** len, - }; -} - pub fn Functor(comptime Output: type, comptime input_types: []const type) type { const InputTuple = std.meta.Tuple(input_types); @@ -119,20 +126,6 @@ pub fn Generator(comptime Output: type, comptime input_types: []const type) type }; } -pub const NullWritable = struct { - written: usize = 0, - - pub fn writer(self: *NullWritable) Writer { - return Writer.bind(NullWritable, self, write); - } - - pub fn write(self: *NullWritable, buffer: []const u8) !usize { - self.written += buffer.len; - - return buffer.len; - } -}; - pub const PrintError = Error || error { IncompleteWrite, }; @@ -141,8 +134,6 @@ pub const Reader = Generator(Error!usize, &.{[]coral.Byte}); pub const Writer = Generator(Error!usize, &.{[]const coral.Byte}); -const native_endian = builtin.cpu.arch.endian(); - pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral.Byte { const buffer = coral.Stack(coral.Byte){.allocator = allocator}; @@ -153,20 +144,6 @@ pub fn alloc_read(input: coral.io.Reader, allocator: std.mem.Allocator) []coral. return buffer.to_allocation(streamed); } -pub fn are_equal(a: []const Byte, b: []const Byte) bool { - if (a.len != b.len) { - return false; - } - - for (0 .. a.len) |i| { - if (a[i] != b[i]) { - return false; - } - } - - return true; -} - pub const bits_per_byte = 8; pub fn bytes_of(value: anytype) []const Byte { @@ -179,14 +156,6 @@ pub fn bytes_of(value: anytype) []const Byte { }; } -pub fn ends_with(haystack: []const Byte, needle: []const Byte) bool { - if (needle.len > haystack.len) { - return false; - } - - return are_equal(haystack[haystack.len - needle.len ..], needle); -} - pub fn print(writer: Writer, utf8: []const u8) PrintError!void { if (try writer.yield(.{utf8}) != utf8.len) { return error.IncompleteWrite; @@ -208,35 +177,6 @@ pub fn skip_n(input: Reader, distance: u64) Error!void { } } -pub fn read_foreign(input: Reader, comptime Type: type) Error!Type { - const decoded = try read_native(input, Type); - - return switch (@typeInfo(input)) { - .Struct => std.mem.byteSwapAllFields(Type, &decoded), - else => @byteSwap(decoded), - }; -} - -pub fn read_native(input: Reader, comptime Type: type) Error!Type { - var buffer = @as([@sizeOf(Type)]coral.Byte, undefined); - - if (try input.yield(.{&buffer}) != buffer.len) { - return error.UnavailableResource; - } - - return @as(*align(1) const Type, @ptrCast(&buffer)).*; -} - -pub const read_little = switch (native_endian) { - .little => read_native, - .big => read_foreign, -}; - -pub const read_big = switch (native_endian) { - .little => read_foreign, - .big => read_native, -}; - pub fn slice_sentineled(comptime sen: anytype, ptr: [*:sen]const @TypeOf(sen)) [:sen]const @TypeOf(sen) { var len = @as(usize, 0); diff --git a/src/main.zig b/src/main.zig index c5d050d..c8ebec9 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const ona = @import("ona"); const Actors = struct { instances: coral.stack.Sequential(ona.gfx.Point2D) = .{.allocator = coral.heap.allocator}, + quad_mesh_2d: ona.gfx.Handle = .none, body_texture: ona.gfx.Handle = .none, }; @@ -24,6 +25,7 @@ pub fn main() !void { fn load(display: coral.Write(ona.gfx.Display), actors: coral.Write(Actors), assets: coral.Write(ona.gfx.Assets)) !void { display.res.width, display.res.height = .{1280, 720}; actors.res.body_texture = try assets.res.open_file("actor.bmp"); + actors.res.quad_mesh_2d = try assets.res.open_quad_mesh_2d(@splat(1)); try actors.res.instances.push_grow(.{0, 0}); } @@ -32,11 +34,11 @@ fn exit(actors: coral.Write(Actors)) void { actors.res.instances.deinit(); } -fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors), assets: coral.Read(ona.gfx.Assets)) !void { +fn render(queue: ona.gfx.Queue, actors: coral.Write(Actors)) !void { for (actors.res.instances.values) |instance| { try queue.commands.append(.{ .instance_2d = .{ - .mesh_2d = assets.res.primitives.quad_mesh, + .mesh_2d = actors.res.quad_mesh_2d, .texture = actors.res.body_texture, .transform = .{ diff --git a/src/ona/gfx.zig b/src/ona/gfx.zig index 074485a..72aa211 100644 --- a/src/ona/gfx.zig +++ b/src/ona/gfx.zig @@ -14,41 +14,99 @@ const std = @import("std"); pub const Assets = struct { context: device.Context, - primitives: Primitives, formats: coral.stack.Sequential(Format), + staging_arena: std.heap.ArenaAllocator, pub const Format = struct { extension: []const u8, - open: *const fn ([]const u8) std.mem.Allocator.Error!Handle, + open_file: *const fn (*std.heap.ArenaAllocator, []const u8) Error!Desc, + + pub const Error = std.mem.Allocator.Error || coral.files.Error || error { + Unsupported, + }; }; - const Primitives = struct { - quad_mesh: Handle, - }; + pub fn open_file(self: *Assets, path: []const u8) (std.mem.Allocator.Error || Format.Error)!Handle { + defer { + const max_cache_size = 536870912; - pub fn close(self: *Assets, handle: Handle) void { - return self.context.close(handle); - } + if (!self.staging_arena.reset(.{.retain_with_limit = max_cache_size})) { + std.log.warn("failed to retain staging arena size of {} bytes", .{max_cache_size}); + } + } - pub fn open_file(self: *Assets, path: []const u8) std.mem.Allocator.Error!Handle { for (self.formats.values) |format| { if (!std.mem.endsWith(u8, path, format.extension)) { continue; } - return format.open(path); + return self.context.open(try format.open_file(&self.staging_arena, path)); } return .none; } - pub fn open_mesh_2d(self: *Assets, mesh_2d: Mesh2D) std.mem.Allocator.Error!Handle { - return self.context.open(.{.mesh_2d = mesh_2d}); + pub fn open_quad_mesh_2d(self: *Assets, extents: Point2D) std.mem.Allocator.Error!Handle { + const width, const height = extents / @as(Point2D, @splat(2)); + + return self.context.open(.{ + .mesh_2d = .{ + .indices = &.{0, 1, 2, 0, 2, 3}, + + .vertices = &.{ + .{.xy = .{-width, height}, .uv = .{0, 1}}, + .{.xy = .{width, height}, .uv = .{1, 1}}, + .{.xy = .{width, -height}, .uv = .{1, 0}}, + .{.xy = .{-width, -height}, .uv = .{0, 0}}, + }, + }, + }); } }; pub const Color = @Vector(4, f32); +pub const Desc = union (enum) { + texture: Texture, + mesh_2d: Mesh2D, + + pub const Mesh2D = struct { + vertices: []const Vertex, + indices: []const u16, + + pub const Vertex = struct { + xy: Point2D, + uv: Point2D, + }; + }; + + pub const Texture = struct { + data: []const coral.io.Byte, + width: u16, + format: Format, + access: Access, + + pub const Access = enum { + static, + }; + + pub const Format = enum { + rgba8888, + bgra8888, + argb8888, + rgb888, + bgr888, + + pub fn byte_size(self: Format) usize { + return switch (self) { + .rgba8888, .bgra8888, .argb8888 => 4, + .rgb888, .bgr888 => 3, + }; + } + }; + }; +}; + pub const Display = struct { width: u16 = 1280, height: u16 = 720, @@ -85,16 +143,6 @@ pub const Input = union (enum) { pub const Point2D = @Vector(2, f32); -pub const Mesh2D = struct { - vertices: []const Vertex, - indices: []const u16, - - pub const Vertex = struct { - xy: Point2D, - uv: Point2D, - }; -}; - pub const Queue = struct { commands: *device.RenderList, @@ -104,6 +152,10 @@ pub const Queue = struct { pub fn bind(_: coral.system.BindContext) std.mem.Allocator.Error!State { // TODO: Review how good of an idea this global state is, even if bind is guaranteed to always be ran on main. + if (renders.is_empty()) { + renders = .{.allocator = coral.heap.allocator}; + } + const command_index = renders.len(); try renders.push_grow(device.RenderChain.init(coral.heap.allocator)); @@ -121,39 +173,21 @@ pub const Queue = struct { pub fn unbind(state: *State) void { std.debug.assert(!renders.is_empty()); - std.mem.swap(device.RenderChain, &renders.values[state.command_index], renders.get_ptr().?); + + const render = &renders.values[state.command_index]; + + render.deinit(); + std.mem.swap(device.RenderChain, render, renders.get_ptr().?); std.debug.assert(renders.pop()); + + if (renders.is_empty()) { + Queue.renders.deinit(); + } } var renders = coral.stack.Sequential(device.RenderChain){.allocator = coral.heap.allocator}; }; -pub const Texture = struct { - data: []const coral.io.Byte, - width: u16, - format: Format, - access: Access, - - pub const Access = enum { - static, - }; - - pub const Format = enum { - rgba8888, - bgra8888, - argb8888, - rgb888, - bgr888, - - pub fn byte_size(self: Format) usize { - return switch (self) { - .rgba8888, .bgra8888, .argb8888 => 4, - .rgb888, .bgr888 => 3, - }; - } - }; -}; - pub const Transform2D = extern struct { xbasis: Point2D = .{1, 0}, ybasis: Point2D = .{0, 1}, @@ -163,7 +197,7 @@ pub const Transform2D = extern struct { const builtin_formats = [_]Assets.Format{ .{ .extension = "bmp", - .open = formats.open_bmp, + .open_file = formats.load_bmp, }, }; @@ -210,24 +244,8 @@ pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || st try registered_formats.grow(builtin_formats.len); std.debug.assert(registered_formats.push_all(&builtin_formats)); - const half_extent = 0.5; - try world.set_resource(.none, Assets{ - .primitives = .{ - .quad_mesh = try context.open(.{ - .mesh_2d = .{ - .indices = &.{0, 1, 2, 0, 2, 3}, - - .vertices = &.{ - .{.xy = .{-half_extent, half_extent}, .uv = .{0, 1}}, - .{.xy = .{half_extent, half_extent}, .uv = .{1, 1}}, - .{.xy = .{half_extent, -half_extent}, .uv = .{1, 0}}, - .{.xy = .{-half_extent, -half_extent}, .uv = .{0, 0}}, - }, - }, - }), - }, - + .staging_arena = std.heap.ArenaAllocator.init(coral.heap.allocator), .formats = registered_formats, .context = context, }); @@ -239,6 +257,8 @@ pub fn setup(world: *coral.World, events: App.Events) (error {Unsupported} || st } pub fn stop(assets: coral.Write(Assets)) void { + assets.res.staging_arena.deinit(); + assets.res.formats.deinit(); assets.res.context.deinit(); } diff --git a/src/ona/gfx/device.zig b/src/ona/gfx/device.zig index f1284f3..d0d05cc 100644 --- a/src/ona/gfx/device.zig +++ b/src/ona/gfx/device.zig @@ -10,11 +10,6 @@ const sokol = @import("sokol"); const std = @import("std"); -pub const Asset = union (enum) { - texture: gfx.Texture, - mesh_2d: gfx.Mesh2D, -}; - pub const Context = struct { window: *ext.SDL_Window, thread: std.Thread, @@ -42,6 +37,7 @@ pub const Context = struct { self.loop.is_running.store(false, .monotonic); self.loop.ready.post(); self.thread.join(); + self.loop.deinit(); coral.heap.allocator.destroy(self.loop); ext.SDL_DestroyWindow(self.window); @@ -84,16 +80,20 @@ pub const Context = struct { }; } - pub fn open(self: *Context, asset: Asset) std.mem.Allocator.Error!gfx.Handle { + pub fn open(self: *Context, desc: gfx.Desc) std.mem.Allocator.Error!gfx.Handle { const open_commands = self.loop.opens.pending(); const index = self.loop.closed_indices.get() orelse open_commands.stack.len(); try open_commands.append(.{ .index = index, - .payload = asset, + .desc = desc, }); - try self.loop.closes.pending().stack.grow(1); + const pending_closes = self.loop.closes.pending(); + + if (pending_closes.stack.len() == pending_closes.stack.cap) { + try pending_closes.stack.grow(1); + } _ = self.loop.closed_indices.pop(); @@ -145,13 +145,13 @@ const Loop = struct { const OpenCommand = struct { index: usize, - payload: Asset, + desc: gfx.Desc, fn clone(command: OpenCommand, arena: *std.heap.ArenaAllocator) std.mem.Allocator.Error!OpenCommand { const allocator = arena.allocator(); return .{ - .payload = switch (command.payload) { + .desc = switch (command.desc) { .texture => |texture| .{ .texture = .{ .data = try allocator.dupe(coral.io.Byte, texture.data), @@ -163,7 +163,7 @@ const Loop = struct { .mesh_2d => |mesh_2d| .{ .mesh_2d = .{ - .vertices = try allocator.dupe(gfx.Mesh2D.Vertex, mesh_2d.vertices), + .vertices = try allocator.dupe(gfx.Desc.Mesh2D.Vertex, mesh_2d.vertices), .indices = try allocator.dupe(u16, mesh_2d.indices), }, }, @@ -178,6 +178,12 @@ const Loop = struct { const OpenChain = commands.Chain(OpenCommand, OpenCommand.clone); + fn deinit(self: *Loop) void { + self.closes.deinit(); + self.opens.deinit(); + self.closed_indices.deinit(); + } + fn run(self: *Loop, window: *ext.SDL_Window) !void { const context = configure_and_create: { var result = @as(c_int, 0); @@ -230,7 +236,7 @@ const Loop = struct { defer open_commands.clear(); for (open_commands.stack.values) |command| { - switch (command.payload) { + switch (command.desc) { .texture => |texture| { const stride = texture.width * texture.format.byte_size(); diff --git a/src/ona/gfx/formats.zig b/src/ona/gfx/formats.zig index 0bb116d..481e25b 100644 --- a/src/ona/gfx/formats.zig +++ b/src/ona/gfx/formats.zig @@ -1,9 +1,69 @@ +const coral = @import("coral"); + const gfx = @import("../gfx.zig"); const std = @import("std"); -pub fn open_bmp(path: []const u8) std.mem.Allocator.Error!gfx.Handle { - // TODO: Implement. - _ = path; - unreachable; +pub fn load_bmp(arena: *std.heap.ArenaAllocator, path: []const u8) gfx.Assets.Format.Error!gfx.Desc { + const header = try coral.files.bundle.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.Unsupported; + }; + + if (!std.mem.eql(u8, &header.type, "BM")) { + return error.Unsupported; + } + + const pixel_width = std.math.cast(u16, header.pixel_width) orelse { + return error.Unsupported; + }; + + const pixels = try arena.allocator().alloc(coral.io.Byte, header.image_size); + const bytes_per_pixel = header.bits_per_pixel / coral.io.bits_per_byte; + 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 = coral.scalars.sub(padded_byte_stride, byte_stride) orelse 0; + var buffer_offset: usize = 0; + var file_offset = @as(usize, header.image_offset); + + while (buffer_offset < pixels.len) { + const line = pixels[buffer_offset .. buffer_offset + byte_stride]; + + if (try coral.files.bundle.read_bytes(path, file_offset, line) != byte_stride) { + return error.Unsupported; + } + + file_offset = line.len + byte_padding; + buffer_offset += padded_byte_stride; + } + + return .{ + .texture = .{ + .format = switch (header.bits_per_pixel) { + 24 => .bgr888, + 32 => .bgra8888, + else => return error.Unsupported, + }, + + .width = pixel_width, + .data = pixels, + .access = .static, + } + }; }