ona/src/gfx/gfx.zig
kayomn 9f0f565b0b
All checks were successful
continuous-integration/drone/push Build is passing
Add mouse input, axis mappings, and an input demo (closes #63)
2024-07-25 01:11:58 +01:00

590 lines
14 KiB
Zig

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();
}
}