From 3e41b78a9f85cbdc6c383ab018b4d171e2ea01d3 Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sat, 2 Nov 2024 17:17:04 +0100 Subject: [PATCH 1/6] Cleanup --- src/deecy_ui.zig | 10 ++- src/jit/sh4_jit.zig | 6 +- src/memtest.zig | 190 -------------------------------------------- 3 files changed, 10 insertions(+), 196 deletions(-) delete mode 100644 src/memtest.zig diff --git a/src/deecy_ui.zig b/src/deecy_ui.zig index 9d848823..64075828 100644 --- a/src/deecy_ui.zig +++ b/src/deecy_ui.zig @@ -321,9 +321,13 @@ pub fn draw(self: *@This(), d: *Deecy) !void { "None"; if (zgui.beginCombo("Device" ++ number, .{ .preview_value = name })) { for (available_controllers.items, 0..) |item, index| { - const idx = @as(u32, @intCast(index)); - if (zgui.selectable(item.name, .{ .selected = d.controllers[i] != null and d.controllers[i].?.id == available_controllers.items[idx].id })) - d.controllers[i] = .{ .id = available_controllers.items[idx].id.? }; + if (available_controllers.items[index].id) |id| { + if (zgui.selectable(item.name, .{ .selected = d.controllers[i] != null and d.controllers[i].?.id == id })) + d.controllers[i] = .{ .id = id }; + } else { + if (zgui.selectable(item.name, .{ .selected = d.controllers[i] == null })) + d.controllers[i] = null; + } } zgui.endCombo(); } diff --git a/src/jit/sh4_jit.zig b/src/jit/sh4_jit.zig index 05ed03f1..96cd0bc5 100644 --- a/src/jit/sh4_jit.zig +++ b/src/jit/sh4_jit.zig @@ -220,9 +220,9 @@ const VirtualAddressSpace = if (ExperimentalFastMem) struct { } }, else => { - std.debug.print(" Unhandled Exception: {X}\n", .{info.ExceptionRecord.ExceptionCode}); - std.debug.print(" Info: {X}\n", .{info.ExceptionRecord.ExceptionInformation[0]}); - std.debug.print(" {X}\n", .{info.ExceptionRecord.ExceptionInformation[1]}); + // std.debug.print(" Unhandled Exception: {X}\n", .{info.ExceptionRecord.ExceptionCode}); + // std.debug.print(" Info: {X}\n", .{info.ExceptionRecord.ExceptionInformation[0]}); + // std.debug.print(" {X}\n", .{info.ExceptionRecord.ExceptionInformation[1]}); return std.os.windows.EXCEPTION_CONTINUE_SEARCH; }, } diff --git a/src/memtest.zig b/src/memtest.zig deleted file mode 100644 index 9deacab0..00000000 --- a/src/memtest.zig +++ /dev/null @@ -1,190 +0,0 @@ -const std = @import("std"); -const windows = @import("windows.zig"); -const x86_64 = @import("jit/x86_64.zig"); - -fn handleSegfaultWindows(info: *std.os.windows.EXCEPTION_POINTERS) callconv(std.os.windows.WINAPI) c_long { - switch (info.ExceptionRecord.ExceptionCode) { - std.os.windows.EXCEPTION_ACCESS_VIOLATION => { - const access_type: enum(u1) { read = 0, write = 1 } = @enumFromInt(info.ExceptionRecord.ExceptionInformation[0]); - const fault_address = info.ExceptionRecord.ExceptionInformation[1]; - - std.debug.print(" Access Violation: {s} @ {X}\n", .{ @tagName(access_type), fault_address }); - - const instruction: [*]u8 = @ptrFromInt(info.ContextRecord.Rip); - //std.debug.print(" Instr: {X:0>2} {X:0>2} {X:0>2} {X:0>2} {X:0>2} {X:0>2} {X:0>2} {X:0>2}\n", .{ - // instruction[0], - // instruction[1], - // instruction[2], - // instruction[3], - // instruction[4], - // instruction[5], - // instruction[6], - // instruction[7], - //}); - const modrm: x86_64.MODRM = @bitCast(instruction[1]); - //std.debug.print(" MODRM: {any}\n", .{modrm}); - - switch (access_type) { - .read => { - // TODO: Skip instruction - switch (instruction[0]) { - 0x8A => { - switch (modrm.reg_opcode) { - 0 => info.ContextRecord.Rax = 42, - 1 => info.ContextRecord.Rcx = 43, - else => return std.os.windows.EXCEPTION_CONTINUE_SEARCH, - } - switch (modrm.mod) { - .indirect => info.ContextRecord.Rip += 2, - .disp8 => info.ContextRecord.Rip += 3, - .disp32 => info.ContextRecord.Rip += 6, - else => return std.os.windows.EXCEPTION_CONTINUE_SEARCH, - } - }, - else => { - std.debug.print("REX?", .{}); - return std.os.windows.EXCEPTION_CONTINUE_SEARCH; - }, - } - }, - .write => { - switch (instruction[0]) { - // TODO: Skip instruction - 0xC6 => { - switch (modrm.mod) { - .indirect => info.ContextRecord.Rip += 3, - .disp8 => info.ContextRecord.Rip += 4, - .disp32 => info.ContextRecord.Rip += 7, - else => return std.os.windows.EXCEPTION_CONTINUE_SEARCH, - } - }, - else => { - std.debug.print("REX?", .{}); - return std.os.windows.EXCEPTION_CONTINUE_SEARCH; - }, - } - }, - } - return windows.EXCEPTION_CONTINUE_EXECUTION; // Not defined in std - }, - else => { - std.debug.print(" Unhandled Exception: {}\n", .{info.ExceptionRecord.ExceptionCode}); - return std.os.windows.EXCEPTION_CONTINUE_SEARCH; - }, - } - return std.os.windows.EXCEPTION_CONTINUE_SEARCH; -} - -pub fn main() !void { - const reserved_memory = try std.os.windows.VirtualAlloc( - null, - 0x1_0000_0000, - std.os.windows.MEM_RESERVE, - //std.os.windows.MEM_RESERVE | std.os.windows.MEM_COMMIT, - std.os.windows.PAGE_NOACCESS, - // std.os.windows.PAGE_READWRITE, - ); - std.os.windows.VirtualFree(reserved_memory, 0, std.os.windows.MEM_RELEASE); - var slice: [*]u8 = @as([*]u8, @ptrCast(reserved_memory)); - - const boot = try std.os.windows.VirtualAlloc( - @ptrFromInt(@intFromPtr(reserved_memory) + 0x0000_0000), - 0x20_0000, - std.os.windows.MEM_RESERVE | std.os.windows.MEM_COMMIT, - std.os.windows.PAGE_READWRITE, - ); - var boot_slice: []u8 = @as([*]u8, @ptrCast(boot))[0..0x20_0000]; - - const RAMSize = 0x100_0000; - - //const ram = try std.os.windows.VirtualAlloc(@ptrFromInt(@intFromPtr(reserved_memory) + 0x0C00_0000), RAMSize, std.os.windows.MEM_COMMIT, std.os.windows.PAGE_READWRITE); - - const ram_handle = windows.CreateFileMappingA( - std.os.windows.INVALID_HANDLE_VALUE, - null, - std.os.windows.PAGE_READWRITE, - 0, - RAMSize, - null, - ).?; - - //var lpflOldProtect: std.os.windows.DWORD = undefined; - //_ = try std.os.windows.VirtualProtect( - // @ptrFromInt(@intFromPtr(reserved_memory) + 0x0C00_0000), - // RAMSize, - // std.os.windows.PAGE_READWRITE, - // &lpflOldProtect, - //); - - for (0..4) |i| { - const addr: *u64 = @ptrFromInt(@intFromPtr(reserved_memory) + 0x0C00_0000 + i * RAMSize); - //_ = try std.os.windows.VirtualAlloc( - // addr, - // RAMSize, - // std.os.windows.MEM_RESERVE | std.os.windows.MEM_COMMIT, - // std.os.windows.PAGE_READWRITE, - //); - //if (!windows.VirtualProtect( - // addr, - // RAMSize, - // std.os.windows.PAGE_READWRITE, - // &lpflOldProtect, - //)) { - // std.debug.print("VirtualProtect Error: {}\n", .{std.os.windows.GetLastError()}); - //} - //std.os.windows.VirtualFree(addr, RAMSize, std.os.windows.MEM_RELEASE); - const result = windows.MapViewOfFileEx( - ram_handle, - windows.FILE_MAP_ALL_ACCESS, - 0, - 0, - RAMSize, - addr, - ); - - if (result == null) { - std.debug.print("MapViewOfFileEx({}, {X:0>8}) Error: {}\n", .{ i, addr, std.os.windows.GetLastError() }); - } - } - const ram: [*]u8 = @ptrFromInt(@intFromPtr(reserved_memory) + 0x0C00_0000); - var ram_slice: []u8 = @as([*]u8, @ptrCast(ram))[0..RAMSize]; - - _ = std.os.windows.kernel32.AddVectoredExceptionHandler(1, handleSegfaultWindows); - - slice[0xAA_AAAA] = 0x42; - slice[0xBB_BBBB] = 0x64; - - std.debug.print("Base: {X:0>2} {X:0>2}\n", .{ slice[0xAA_AAAA], slice[0xBB_BBBB] }); - - std.debug.print("RAM slice: {X:0>2} {X:0>2}\n", .{ ram_slice[0], ram_slice[1] }); - std.debug.print("RAM 0x0C00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0C00_0000], slice[0x0C00_0001] }); - std.debug.print("RAM 0x0D00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0D00_0000], slice[0x0D00_0001] }); - - ram_slice[0] = 1; - ram_slice[1] = 2; - - std.debug.print("RAM slice: {X:0>2} {X:0>2}\n", .{ ram_slice[0], ram_slice[1] }); - std.debug.print("RAM 0x0C00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0C00_0000], slice[0x0C00_0001] }); - std.debug.print("RAM 0x0D00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0D00_0000], slice[0x0D00_0001] }); - - slice[0x0C00_0000] = 0x42; - slice[0x0C00_0001] = 0x64; - - std.debug.print("RAM slice: {X:0>2} {X:0>2}\n", .{ ram_slice[0], ram_slice[1] }); - std.debug.print("RAM 0x0C00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0C00_0000], slice[0x0C00_0001] }); - std.debug.print("RAM 0x0D00_0000: {X:0>2} {X:0>2}\n", .{ slice[0x0D00_0000], slice[0x0D00_0001] }); - - boot_slice[0] = 3; - boot_slice[1] = 4; - - std.debug.print("Boot: {X:0>2} {X:0>2}\n", .{ boot_slice[0], boot_slice[1] }); - - slice[0] = 0x42; - slice[1] = 0x64; - - std.debug.print("Boot: {X:0>2} {X:0>2}\n", .{ boot_slice[0], boot_slice[1] }); - - slice[0x0D00_0000] = 0x98; - slice[0x0D00_0001] = 0x99; - std.debug.print("RAM Mirror: {X:0>2} {X:0>2}\n", .{ slice[0x0D00_0000], slice[0x0D00_0001] }); -} From 36f0619e9dbe144b813b9a60894286314cd4a8dd Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sun, 3 Nov 2024 08:00:51 +0100 Subject: [PATCH 2/6] Resolution setting --- src/debug_ui.zig | 2 +- src/deecy.zig | 1374 +++++++++++++++++++++++----------------------- src/deecy_ui.zig | 9 +- src/main.zig | 2 +- 4 files changed, 695 insertions(+), 692 deletions(-) diff --git a/src/debug_ui.zig b/src/debug_ui.zig index 1b42256f..118dad52 100644 --- a/src/debug_ui.zig +++ b/src/debug_ui.zig @@ -20,7 +20,7 @@ const fRGBA = Colors.fRGBA; const RendererModule = @import("renderer.zig"); -const Deecy = @import("deecy.zig").Deecy; +const Deecy = @import("deecy.zig"); const vram_width: u32 = 640; const vram_height: u32 = 480; diff --git a/src/deecy.zig b/src/deecy.zig index d26b73f6..c4855289 100644 --- a/src/deecy.zig +++ b/src/deecy.zig @@ -13,7 +13,7 @@ const Dreamcast = DreamcastModule.Dreamcast; const AICA = DreamcastModule.AICAModule.AICA; const GDI = @import("./gdi.zig").GDI; -const Renderer = @import("./renderer.zig").Renderer; +pub const Renderer = @import("./renderer.zig").Renderer; const DeecyUI = @import("./deecy_ui.zig"); const DebugUI = @import("./debug_ui.zig"); @@ -32,7 +32,7 @@ fn glfw_key_callback( _ = scancode; _ = mods; - const maybe_app = window.getUserPointer(Deecy); + const maybe_app = window.getUserPointer(@This()); if (maybe_app) |app| { if (action == .press) { @@ -91,7 +91,7 @@ fn glfw_drop_callback( count: i32, paths: [*][*:0]const u8, ) callconv(.C) void { - const maybe_app = window.getUserPointer(Deecy); + const maybe_app = window.getUserPointer(@This()); if (maybe_app) |app| { if (count > 0) { app.load_and_start(std.mem.span(paths[0])) catch |err| { @@ -140,829 +140,827 @@ const Configuration = struct { display_debug_ui: bool = false, }; -pub const Deecy = struct { - pub const TmpDirPath = "./userdata/.tmp_deecy"; // Be careful when editing this, it will deleted on program exit! +pub const TmpDirPath = "./userdata/.tmp_deecy"; // Be careful when editing this, it will deleted on program exit! - const ExperimentalThreadedDC = true; +const ExperimentalThreadedDC = true; - window: *zglfw.Window, - gctx: *zgpu.GraphicsContext = undefined, - scale_factor: f32 = 1.0, +window: *zglfw.Window, +gctx: *zgpu.GraphicsContext = undefined, +scale_factor: f32 = 1.0, - dc: *Dreamcast = undefined, - renderer: Renderer = undefined, - audio_device: *zaudio.Device = undefined, +dc: *Dreamcast = undefined, +renderer: Renderer = undefined, +audio_device: *zaudio.Device = undefined, - config: Configuration = .{}, +config: Configuration = .{}, - last_frame_timestamp: i64, - last_n_frametimes: std.fifo.LinearFifo(i64, .Dynamic), +last_frame_timestamp: i64, +last_n_frametimes: std.fifo.LinearFifo(i64, .Dynamic), - running: bool = false, - dc_thread: std.Thread = undefined, - dc_thread_semaphore: std.Thread.Semaphore = .{}, - dc_last_frame: std.time.Instant = undefined, +running: bool = false, +dc_thread: std.Thread = undefined, +dc_thread_semaphore: std.Thread.Semaphore = .{}, +dc_last_frame: std.time.Instant = undefined, - enable_jit: bool = true, - breakpoints: std.ArrayList(u32), +enable_jit: bool = true, +breakpoints: std.ArrayList(u32), - controllers: [4]?struct { id: zglfw.Joystick.Id, deadzone: f32 = 0.1 } = .{null} ** 4, +controllers: [4]?struct { id: zglfw.Joystick.Id, deadzone: f32 = 0.1 } = .{null} ** 4, - display_ui: bool = true, - ui: DeecyUI = undefined, - debug_ui: DebugUI = undefined, +display_ui: bool = true, +ui: DeecyUI = undefined, +debug_ui: DebugUI = undefined, - save_state_slots: [4]bool = .{ false, false, false, false }, +save_state_slots: [4]bool = .{ false, false, false, false }, - _allocator: std.mem.Allocator, +_allocator: std.mem.Allocator, - pub fn create(allocator: std.mem.Allocator) !*Deecy { - std.fs.cwd().makeDir("userdata") catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; +pub fn create(allocator: std.mem.Allocator) !*@This() { + std.fs.cwd().makeDir("userdata") catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; - try zglfw.init(); + try zglfw.init(); - // IDK, prevents device lost crash on Linux. See https://github.com/zig-gamedev/zig-gamedev/commit/9bd4cf860c8e295f4f0db9ec4357905e090b5b98 - zglfw.windowHintTyped(.client_api, .no_api); + // IDK, prevents device lost crash on Linux. See https://github.com/zig-gamedev/zig-gamedev/commit/9bd4cf860c8e295f4f0db9ec4357905e090b5b98 + zglfw.windowHintTyped(.client_api, .no_api); - // TODO: Load from config. - const default_resolution = Renderer.Resolution{ .width = 2 * @ceil((16.0 / 9.0 * @as(f32, @floatFromInt(Renderer.NativeResolution.height)))), .height = 2 * Renderer.NativeResolution.height }; + // TODO: Load from config. + const default_resolution = Renderer.Resolution{ .width = 2 * @ceil((16.0 / 9.0 * @as(f32, @floatFromInt(Renderer.NativeResolution.height)))), .height = 2 * Renderer.NativeResolution.height }; - const self = try allocator.create(Deecy); - self.* = Deecy{ - .window = try zglfw.Window.create(default_resolution.width, default_resolution.height, "Deecy", null), - .last_frame_timestamp = std.time.microTimestamp(), - .last_n_frametimes = std.fifo.LinearFifo(i64, .Dynamic).init(allocator), - .breakpoints = std.ArrayList(u32).init(allocator), - ._allocator = allocator, - }; + const self = try allocator.create(@This()); + self.* = .{ + .window = try zglfw.Window.create(default_resolution.width, default_resolution.height, "Deecy", null), + .last_frame_timestamp = std.time.microTimestamp(), + .last_n_frametimes = std.fifo.LinearFifo(i64, .Dynamic).init(allocator), + .breakpoints = std.ArrayList(u32).init(allocator), + ._allocator = allocator, + }; - self.window.setUserPointer(self); - _ = self.window.setKeyCallback(glfw_key_callback); - _ = self.window.setDropCallback(glfw_drop_callback); - - self.gctx = try zgpu.GraphicsContext.create(allocator, .{ - .window = self.window, - .fn_getTime = @ptrCast(&zglfw.getTime), - .fn_getFramebufferSize = @ptrCast(&zglfw.Window.getFramebufferSize), - .fn_getWin32Window = @ptrCast(&zglfw.getWin32Window), - .fn_getX11Display = @ptrCast(&zglfw.getX11Display), - .fn_getX11Window = @ptrCast(&zglfw.getX11Window), - .fn_getCocoaWindow = @ptrCast(&zglfw.getCocoaWindow), - }, .{ - .present_mode = .mailbox, - .required_features = &[_]zgpu.wgpu.FeatureName{ .bgra8_unorm_storage, .depth32_float_stencil8 }, - .required_limits = &.{ .limits = .{ .max_texture_array_layers = 512 } }, - }); - - brk_limits: { - var device_limits: zgpu.wgpu.SupportedLimits = .{}; - var adapter_limits: zgpu.wgpu.SupportedLimits = .{}; - if (!self.gctx.device.getLimits(&device_limits)) { - deecy_log.err("Failed to get device limits.", .{}); - break :brk_limits; - } - if (!self.gctx.device.getAdapter().getLimits(&adapter_limits)) { - deecy_log.err("Failed to get adapter limits.", .{}); - break :brk_limits; - } - deecy_log.info("WebGPU Limits (Device/Adapter):", .{}); - inline for (std.meta.fields(zgpu.wgpu.Limits)) |field| { - deecy_log.info("{s: >48}: {d: >10} / {d: >10}", .{ field.name, @field(device_limits.limits, field.name), @field(adapter_limits.limits, field.name) }); - } + self.window.setUserPointer(self); + _ = self.window.setKeyCallback(glfw_key_callback); + _ = self.window.setDropCallback(glfw_drop_callback); + + self.gctx = try zgpu.GraphicsContext.create(allocator, .{ + .window = self.window, + .fn_getTime = @ptrCast(&zglfw.getTime), + .fn_getFramebufferSize = @ptrCast(&zglfw.Window.getFramebufferSize), + .fn_getWin32Window = @ptrCast(&zglfw.getWin32Window), + .fn_getX11Display = @ptrCast(&zglfw.getX11Display), + .fn_getX11Window = @ptrCast(&zglfw.getX11Window), + .fn_getCocoaWindow = @ptrCast(&zglfw.getCocoaWindow), + }, .{ + .present_mode = .mailbox, + .required_features = &[_]zgpu.wgpu.FeatureName{ .bgra8_unorm_storage, .depth32_float_stencil8 }, + .required_limits = &.{ .limits = .{ .max_texture_array_layers = 512 } }, + }); + + brk_limits: { + var device_limits: zgpu.wgpu.SupportedLimits = .{}; + var adapter_limits: zgpu.wgpu.SupportedLimits = .{}; + if (!self.gctx.device.getLimits(&device_limits)) { + deecy_log.err("Failed to get device limits.", .{}); + break :brk_limits; + } + if (!self.gctx.device.getAdapter().getLimits(&adapter_limits)) { + deecy_log.err("Failed to get adapter limits.", .{}); + break :brk_limits; + } + deecy_log.info("WebGPU Limits (Device/Adapter):", .{}); + inline for (std.meta.fields(zgpu.wgpu.Limits)) |field| { + deecy_log.info("{s: >48}: {d: >10} / {d: >10}", .{ field.name, @field(device_limits.limits, field.name), @field(adapter_limits.limits, field.name) }); } + } - const scale = self.window.getContentScale(); - self.scale_factor = @max(scale[0], scale[1]); + const scale = self.window.getContentScale(); + self.scale_factor = @max(scale[0], scale[1]); - self.ui = DeecyUI.init(allocator, self.gctx); - try self.ui_init(); + self.ui = DeecyUI.init(allocator, self.gctx); + try self.ui_init(); - self.dc = Dreamcast.create(allocator) catch |err| { - switch (err) { - error.BiosNotFound => { - self.display_unrecoverable_error("Missing BIOS. Please copy your bios file to 'data/dc_boot.bin'."); - }, - else => { - self.display_unrecoverable_error("Error initializing Dreamcast"); - }, - } - return err; - }; + self.dc = Dreamcast.create(allocator) catch |err| { + switch (err) { + error.BiosNotFound => { + self.display_unrecoverable_error("Missing BIOS. Please copy your bios file to 'data/dc_boot.bin'."); + }, + else => { + self.display_unrecoverable_error("Error initializing Dreamcast"); + }, + } + return err; + }; - self.renderer = try Renderer.init(self._allocator, self.gctx); - self.dc.on_render_start = .{ - .function = @ptrCast(&Renderer.on_render_start), - .context = &self.renderer, - }; + self.renderer = try Renderer.init(self._allocator, self.gctx); + self.dc.on_render_start = .{ + .function = @ptrCast(&Renderer.on_render_start), + .context = &self.renderer, + }; - zaudio.init(allocator); - - var audio_device_config = zaudio.Device.Config.init(.playback); - audio_device_config.sample_rate = DreamcastModule.AICAModule.AICA.SampleRate; - audio_device_config.data_callback = audio_callback; - audio_device_config.user_data = self; - audio_device_config.period_size_in_frames = 16; - audio_device_config.playback.format = .signed32; - audio_device_config.playback.channels = 2; - // std.debug.print("Audio device config: {}\n", .{audio_device_config}); - self.audio_device = try zaudio.Device.create(null, audio_device_config); - - try self.audio_device.setMasterVolume(0.3); - try self.audio_device.start(); - - var curr_pad: usize = 0; - for (0..zglfw.Joystick.maximum_supported) |idx| { - const jid: zglfw.Joystick.Id = @intCast(idx); - if (zglfw.Joystick.get(jid)) |joystick| { - if (joystick.asGamepad()) |_| { - self.controllers[curr_pad] = .{ .id = jid }; - curr_pad += 1; - if (curr_pad >= 4) - break; - } + zaudio.init(allocator); + + var audio_device_config = zaudio.Device.Config.init(.playback); + audio_device_config.sample_rate = DreamcastModule.AICAModule.AICA.SampleRate; + audio_device_config.data_callback = audio_callback; + audio_device_config.user_data = self; + audio_device_config.period_size_in_frames = 16; + audio_device_config.playback.format = .signed32; + audio_device_config.playback.channels = 2; + // std.debug.print("Audio device config: {}\n", .{audio_device_config}); + self.audio_device = try zaudio.Device.create(null, audio_device_config); + + try self.audio_device.setMasterVolume(0.3); + try self.audio_device.start(); + + var curr_pad: usize = 0; + for (0..zglfw.Joystick.maximum_supported) |idx| { + const jid: zglfw.Joystick.Id = @intCast(idx); + if (zglfw.Joystick.get(jid)) |joystick| { + if (joystick.asGamepad()) |_| { + self.controllers[curr_pad] = .{ .id = jid }; + curr_pad += 1; + if (curr_pad >= 4) + break; } } + } - self.debug_ui = try DebugUI.init(self); + self.debug_ui = try DebugUI.init(self); - try self.check_save_state_slots(); + try self.check_save_state_slots(); - return self; - } + return self; +} - pub fn destroy(self: *Deecy) void { - self.stop(); +pub fn destroy(self: *@This()) void { + self.stop(); - self.breakpoints.deinit(); + self.breakpoints.deinit(); - self.audio_device.destroy(); + self.audio_device.destroy(); - self.renderer.deinit(); + self.renderer.deinit(); - self.dc.deinit(); - self._allocator.destroy(self.dc); + self.dc.deinit(); + self._allocator.destroy(self.dc); - self.debug_ui.deinit(); - self.ui_deinit(); + self.debug_ui.deinit(); + self.ui_deinit(); - zaudio.deinit(); + zaudio.deinit(); - self.gctx.destroy(self._allocator); + self.gctx.destroy(self._allocator); - self.window.destroy(); - zglfw.terminate(); + self.window.destroy(); + zglfw.terminate(); - self._allocator.destroy(self); - } + self._allocator.destroy(self); +} - fn ui_init(self: *Deecy) !void { - zgui.init(self._allocator); - zgui.io.setConfigFlags(.{ .dock_enable = true }); - - _ = zgui.io.addFontFromMemory( - DefaultFont, - std.math.floor(16.0 * self.scale_factor), - ); - - var style = zgui.getStyle(); - - style.scaleAllSizes(self.scale_factor); - - // Based on Deep Dark style by janekb04 from ImThemes - style.alpha = 1.0; - style.disabled_alpha = 0.6000000238418579; - style.window_padding = .{ 8.0, 8.0 }; - style.window_rounding = 7.0; - style.window_border_size = 1.0; - style.window_min_size = .{ 32.0, 32.0 }; - style.window_title_align = .{ 0.0, 0.5 }; - style.window_menu_button_position = .left; - style.child_rounding = 4.0; - style.child_border_size = 1.0; - style.popup_rounding = 4.0; - style.popup_border_size = 1.0; - style.frame_padding = .{ 6.0, 4.0 }; - style.frame_rounding = 3.0; - style.frame_border_size = 1.0; - style.item_spacing = .{ 6.0, 6.0 }; - style.item_inner_spacing = .{ 6.0, 6.0 }; - style.cell_padding = .{ 6.0, 6.0 }; - style.indent_spacing = 25.0; - style.columns_min_spacing = 6.0; - style.scrollbar_size = 15.0; - style.scrollbar_rounding = 9.0; - style.grab_min_size = 10.0; - style.grab_rounding = 3.0; - style.tab_rounding = 4.0; - style.tab_border_size = 1.0; - style.tab_min_width_for_close_button = 0.0; - style.color_button_position = .right; - style.button_text_align = .{ 0.5, 0.5 }; - style.selectable_text_align = .{ 0.0, 0.0 }; - - const EULogoColor: [4]f32 = .{ 0.231, 0.463, 0.761, 1.0 }; - //const JPLogoColor: [4]f32 = .{ 0.929, 0.518, 0.192, 1.0 }; - //const USLogoColor: [4]f32 = .{ 0.816, 0.2, 0.071, 1.0 }; - - style.setColor(.text, .{ 1.0, 1.0, 1.0, 1.0 }); - style.setColor(.text_disabled, .{ 0.4980392158031464, 0.4980392158031464, 0.4980392158031464, 1.0 }); - style.setColor(.window_bg, .{ 0.09803921729326248, 0.09803921729326248, 0.09803921729326248, 1.0 }); - style.setColor(.child_bg, .{ 0.0, 0.0, 0.0, 0.0 }); - style.setColor(.popup_bg, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.9200000166893005 }); - style.setColor(.border, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.2899999916553497 }); - style.setColor(.border_shadow, .{ 0.0, 0.0, 0.0, 0.239999994635582 }); - style.setColor(.frame_bg, .{ 0.0470588244497776, 0.0470588244497776, 0.0470588244497776, 0.5400000214576721 }); - style.setColor(.frame_bg_hovered, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.5400000214576721 }); - style.setColor(.frame_bg_active, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 1.0 }); - style.setColor(.title_bg, .{ 0.0, 0.0, 0.0, 1.0 }); - style.setColor(.title_bg_active, .{ 0.05882352963089943, 0.05882352963089943, 0.05882352963089943, 1.0 }); - style.setColor(.title_bg_collapsed, .{ 0.0, 0.0, 0.0, 1.0 }); - style.setColor(.menu_bar_bg, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); - style.setColor(.scrollbar_bg, .{ 0.0470588244497776, 0.0470588244497776, 0.0470588244497776, 0.5400000214576721 }); - style.setColor(.scrollbar_grab, .{ 0.3372549116611481, 0.3372549116611481, 0.3372549116611481, 0.5400000214576721 }); - style.setColor(.scrollbar_grab_hovered, .{ 0.4000000059604645, 0.4000000059604645, 0.4000000059604645, 0.5400000214576721 }); - style.setColor(.scrollbar_grab_active, .{ 0.5568627715110779, 0.5568627715110779, 0.5568627715110779, 0.5400000214576721 }); - style.setColor(.check_mark, EULogoColor); - style.setColor(.slider_grab, .{ 0.3372549116611481, 0.3372549116611481, 0.3372549116611481, 0.5400000214576721 }); - style.setColor(.slider_grab_active, .{ 0.5568627715110779, 0.5568627715110779, 0.5568627715110779, 0.5400000214576721 }); - style.setColor(.button, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.2 }); - style.setColor(.button_hovered, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.35 }); - style.setColor(.button_active, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.7 }); - style.setColor(.header, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); - style.setColor(.header_hovered, .{ 0.0, 0.0, 0.0, 0.3600000143051147 }); - style.setColor(.header_active, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 0.3300000131130219 }); - style.setColor(.separator, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); - style.setColor(.separator_hovered, .{ 0.4392156898975372, 0.4392156898975372, 0.4392156898975372, 0.2899999916553497 }); - style.setColor(.separator_active, .{ 0.4000000059604645, 0.4392156898975372, 0.4666666686534882, 1.0 }); - style.setColor(.resize_grip, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); - style.setColor(.resize_grip_hovered, .{ 0.4392156898975372, 0.4392156898975372, 0.4392156898975372, 0.2899999916553497 }); - style.setColor(.resize_grip_active, .{ 0.4000000059604645, 0.4392156898975372, 0.4666666686534882, 1.0 }); - style.setColor(.tab, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); - style.setColor(.tab_hovered, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); - style.setColor(.tab_selected, .{ 0.2000000029802322, 0.2000000029802322, 0.2000000029802322, 0.3600000143051147 }); - style.setColor(.tab_dimmed, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); - style.setColor(.tab_dimmed_selected, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); - style.setColor(.plot_lines, EULogoColor); - style.setColor(.plot_lines_hovered, EULogoColor); - style.setColor(.plot_histogram, EULogoColor); - style.setColor(.plot_histogram_hovered, EULogoColor); - style.setColor(.table_header_bg, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); - style.setColor(.table_border_strong, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); - style.setColor(.table_border_light, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); - style.setColor(.table_row_bg, .{ 0.0, 0.0, 0.0, 0.0 }); - style.setColor(.table_row_bg_alt, .{ 1.0, 1.0, 1.0, 0.05999999865889549 }); - style.setColor(.text_selected_bg, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 1.0 }); - style.setColor(.drag_drop_target, .{ 0.3294117748737335, 0.6666666865348816, 0.8588235378265381, 1.0 }); - style.setColor(.nav_highlight, EULogoColor); - style.setColor(.nav_windowing_highlight, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.7 }); - style.setColor(.nav_windowing_dim_bg, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.2 }); - style.setColor(.modal_window_dim_bg, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.35 }); - - zgui.backend.init( - self.window, - self.gctx.device, - @intFromEnum(zgpu.GraphicsContext.swapchain_format), - @intFromEnum(zgpu.wgpu.TextureFormat.undef), - ); - - zgui.plot.init(); - - self.ui.gctx = self.gctx; - } +fn ui_init(self: *@This()) !void { + zgui.init(self._allocator); + zgui.io.setConfigFlags(.{ .dock_enable = true }); + + _ = zgui.io.addFontFromMemory( + DefaultFont, + std.math.floor(16.0 * self.scale_factor), + ); + + var style = zgui.getStyle(); + + style.scaleAllSizes(self.scale_factor); + + // Based on Deep Dark style by janekb04 from ImThemes + style.alpha = 1.0; + style.disabled_alpha = 0.6000000238418579; + style.window_padding = .{ 8.0, 8.0 }; + style.window_rounding = 7.0; + style.window_border_size = 1.0; + style.window_min_size = .{ 32.0, 32.0 }; + style.window_title_align = .{ 0.0, 0.5 }; + style.window_menu_button_position = .left; + style.child_rounding = 4.0; + style.child_border_size = 1.0; + style.popup_rounding = 4.0; + style.popup_border_size = 1.0; + style.frame_padding = .{ 6.0, 4.0 }; + style.frame_rounding = 3.0; + style.frame_border_size = 1.0; + style.item_spacing = .{ 6.0, 6.0 }; + style.item_inner_spacing = .{ 6.0, 6.0 }; + style.cell_padding = .{ 6.0, 6.0 }; + style.indent_spacing = 25.0; + style.columns_min_spacing = 6.0; + style.scrollbar_size = 15.0; + style.scrollbar_rounding = 9.0; + style.grab_min_size = 10.0; + style.grab_rounding = 3.0; + style.tab_rounding = 4.0; + style.tab_border_size = 1.0; + style.tab_min_width_for_close_button = 0.0; + style.color_button_position = .right; + style.button_text_align = .{ 0.5, 0.5 }; + style.selectable_text_align = .{ 0.0, 0.0 }; + + const EULogoColor: [4]f32 = .{ 0.231, 0.463, 0.761, 1.0 }; + //const JPLogoColor: [4]f32 = .{ 0.929, 0.518, 0.192, 1.0 }; + //const USLogoColor: [4]f32 = .{ 0.816, 0.2, 0.071, 1.0 }; + + style.setColor(.text, .{ 1.0, 1.0, 1.0, 1.0 }); + style.setColor(.text_disabled, .{ 0.4980392158031464, 0.4980392158031464, 0.4980392158031464, 1.0 }); + style.setColor(.window_bg, .{ 0.09803921729326248, 0.09803921729326248, 0.09803921729326248, 1.0 }); + style.setColor(.child_bg, .{ 0.0, 0.0, 0.0, 0.0 }); + style.setColor(.popup_bg, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.9200000166893005 }); + style.setColor(.border, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.2899999916553497 }); + style.setColor(.border_shadow, .{ 0.0, 0.0, 0.0, 0.239999994635582 }); + style.setColor(.frame_bg, .{ 0.0470588244497776, 0.0470588244497776, 0.0470588244497776, 0.5400000214576721 }); + style.setColor(.frame_bg_hovered, .{ 0.1882352977991104, 0.1882352977991104, 0.1882352977991104, 0.5400000214576721 }); + style.setColor(.frame_bg_active, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 1.0 }); + style.setColor(.title_bg, .{ 0.0, 0.0, 0.0, 1.0 }); + style.setColor(.title_bg_active, .{ 0.05882352963089943, 0.05882352963089943, 0.05882352963089943, 1.0 }); + style.setColor(.title_bg_collapsed, .{ 0.0, 0.0, 0.0, 1.0 }); + style.setColor(.menu_bar_bg, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); + style.setColor(.scrollbar_bg, .{ 0.0470588244497776, 0.0470588244497776, 0.0470588244497776, 0.5400000214576721 }); + style.setColor(.scrollbar_grab, .{ 0.3372549116611481, 0.3372549116611481, 0.3372549116611481, 0.5400000214576721 }); + style.setColor(.scrollbar_grab_hovered, .{ 0.4000000059604645, 0.4000000059604645, 0.4000000059604645, 0.5400000214576721 }); + style.setColor(.scrollbar_grab_active, .{ 0.5568627715110779, 0.5568627715110779, 0.5568627715110779, 0.5400000214576721 }); + style.setColor(.check_mark, EULogoColor); + style.setColor(.slider_grab, .{ 0.3372549116611481, 0.3372549116611481, 0.3372549116611481, 0.5400000214576721 }); + style.setColor(.slider_grab_active, .{ 0.5568627715110779, 0.5568627715110779, 0.5568627715110779, 0.5400000214576721 }); + style.setColor(.button, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.2 }); + style.setColor(.button_hovered, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.35 }); + style.setColor(.button_active, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.7 }); + style.setColor(.header, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); + style.setColor(.header_hovered, .{ 0.0, 0.0, 0.0, 0.3600000143051147 }); + style.setColor(.header_active, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 0.3300000131130219 }); + style.setColor(.separator, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); + style.setColor(.separator_hovered, .{ 0.4392156898975372, 0.4392156898975372, 0.4392156898975372, 0.2899999916553497 }); + style.setColor(.separator_active, .{ 0.4000000059604645, 0.4392156898975372, 0.4666666686534882, 1.0 }); + style.setColor(.resize_grip, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); + style.setColor(.resize_grip_hovered, .{ 0.4392156898975372, 0.4392156898975372, 0.4392156898975372, 0.2899999916553497 }); + style.setColor(.resize_grip_active, .{ 0.4000000059604645, 0.4392156898975372, 0.4666666686534882, 1.0 }); + style.setColor(.tab, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); + style.setColor(.tab_hovered, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); + style.setColor(.tab_selected, .{ 0.2000000029802322, 0.2000000029802322, 0.2000000029802322, 0.3600000143051147 }); + style.setColor(.tab_dimmed, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); + style.setColor(.tab_dimmed_selected, .{ 0.1372549086809158, 0.1372549086809158, 0.1372549086809158, 1.0 }); + style.setColor(.plot_lines, EULogoColor); + style.setColor(.plot_lines_hovered, EULogoColor); + style.setColor(.plot_histogram, EULogoColor); + style.setColor(.plot_histogram_hovered, EULogoColor); + style.setColor(.table_header_bg, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); + style.setColor(.table_border_strong, .{ 0.0, 0.0, 0.0, 0.5199999809265137 }); + style.setColor(.table_border_light, .{ 0.2784313857555389, 0.2784313857555389, 0.2784313857555389, 0.2899999916553497 }); + style.setColor(.table_row_bg, .{ 0.0, 0.0, 0.0, 0.0 }); + style.setColor(.table_row_bg_alt, .{ 1.0, 1.0, 1.0, 0.05999999865889549 }); + style.setColor(.text_selected_bg, .{ 0.2000000029802322, 0.2196078449487686, 0.2274509817361832, 1.0 }); + style.setColor(.drag_drop_target, .{ 0.3294117748737335, 0.6666666865348816, 0.8588235378265381, 1.0 }); + style.setColor(.nav_highlight, EULogoColor); + style.setColor(.nav_windowing_highlight, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.7 }); + style.setColor(.nav_windowing_dim_bg, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.2 }); + style.setColor(.modal_window_dim_bg, .{ EULogoColor[0], EULogoColor[1], EULogoColor[2], 0.35 }); + + zgui.backend.init( + self.window, + self.gctx.device, + @intFromEnum(zgpu.GraphicsContext.swapchain_format), + @intFromEnum(zgpu.wgpu.TextureFormat.undef), + ); + + zgui.plot.init(); + + self.ui.gctx = self.gctx; +} - fn ui_deinit(_: *Deecy) void { - zgui.plot.deinit(); - zgui.backend.deinit(); - zgui.deinit(); - } +fn ui_deinit(_: *@This()) void { + zgui.plot.deinit(); + zgui.backend.deinit(); + zgui.deinit(); +} - fn reset(self: *Deecy) !void { - try self.dc.reset(); - self.renderer.reset(); - self.last_frame_timestamp = std.time.microTimestamp(); - self.last_n_frametimes.discard(self.last_n_frametimes.count); - try self.check_save_state_slots(); - } +fn reset(self: *@This()) !void { + try self.dc.reset(); + self.renderer.reset(); + self.last_frame_timestamp = std.time.microTimestamp(); + self.last_n_frametimes.discard(self.last_n_frametimes.count); + try self.check_save_state_slots(); +} - pub fn pool_controllers(self: *Deecy) void { - for (0..4) |controller_idx| { - if (self.dc.maple.ports[controller_idx].main) |*guest_controller| { - switch (guest_controller.*) { - .Controller => |*c| { - // NOTE: Hackish keyboard input for controller 1. - var any_keyboard_key_pressed = false; - if (controller_idx == 0) { - const keybinds: [9]struct { zglfw.Key, DreamcastModule.Maple.ControllerButtons } = .{ - .{ .enter, .{ .start = 0 } }, - .{ .up, .{ .up = 0 } }, - .{ .down, .{ .down = 0 } }, - .{ .left, .{ .left = 0 } }, - .{ .right, .{ .right = 0 } }, - .{ .q, .{ .a = 0 } }, - .{ .w, .{ .b = 0 } }, - .{ .a, .{ .x = 0 } }, - .{ .s, .{ .y = 0 } }, - }; - for (keybinds) |keybind| { - const key_status = self.window.getKey(keybind[0]); - if (key_status == .press) { - any_keyboard_key_pressed = true; - c.press_buttons(keybind[1]); - } else if (key_status == .release) { - c.release_buttons(keybind[1]); - } +pub fn pool_controllers(self: *@This()) void { + for (0..4) |controller_idx| { + if (self.dc.maple.ports[controller_idx].main) |*guest_controller| { + switch (guest_controller.*) { + .Controller => |*c| { + // NOTE: Hackish keyboard input for controller 1. + var any_keyboard_key_pressed = false; + if (controller_idx == 0) { + const keybinds: [9]struct { zglfw.Key, DreamcastModule.Maple.ControllerButtons } = .{ + .{ .enter, .{ .start = 0 } }, + .{ .up, .{ .up = 0 } }, + .{ .down, .{ .down = 0 } }, + .{ .left, .{ .left = 0 } }, + .{ .right, .{ .right = 0 } }, + .{ .q, .{ .a = 0 } }, + .{ .w, .{ .b = 0 } }, + .{ .a, .{ .x = 0 } }, + .{ .s, .{ .y = 0 } }, + }; + for (keybinds) |keybind| { + const key_status = self.window.getKey(keybind[0]); + if (key_status == .press) { + any_keyboard_key_pressed = true; + c.press_buttons(keybind[1]); + } else if (key_status == .release) { + c.release_buttons(keybind[1]); } - c.axis[0] = if (self.window.getKey(.w) == .press) 0 else 255; - c.axis[1] = if (self.window.getKey(.x) == .press) 0 else 255; - c.axis[2] = if (self.window.getKey(.kp_4) == .press) 0 else if (self.window.getKey(.kp_6) == .press) 255 else 128; - c.axis[3] = if (self.window.getKey(.kp_5) == .press) 0 else if (self.window.getKey(.kp_8) == .press) 255 else 128; } + c.axis[0] = if (self.window.getKey(.w) == .press) 0 else 255; + c.axis[1] = if (self.window.getKey(.x) == .press) 0 else 255; + c.axis[2] = if (self.window.getKey(.kp_4) == .press) 0 else if (self.window.getKey(.kp_6) == .press) 255 else 128; + c.axis[3] = if (self.window.getKey(.kp_5) == .press) 0 else if (self.window.getKey(.kp_8) == .press) 255 else 128; + } - if (!any_keyboard_key_pressed) { - if (self.controllers[controller_idx]) |host_controller| { - if (zglfw.Joystick.get(host_controller.id)) |joystick| { - if (joystick.asGamepad()) |gamepad| { - const gamepad_state = gamepad.getState(); - const gamepad_binds: [9]struct { zglfw.Gamepad.Button, DreamcastModule.Maple.ControllerButtons } = .{ - .{ .start, .{ .start = 0 } }, - .{ .dpad_up, .{ .up = 0 } }, - .{ .dpad_down, .{ .down = 0 } }, - .{ .dpad_left, .{ .left = 0 } }, - .{ .dpad_right, .{ .right = 0 } }, - .{ .a, .{ .a = 0 } }, - .{ .b, .{ .b = 0 } }, - .{ .x, .{ .x = 0 } }, - .{ .y, .{ .y = 0 } }, - }; - for (gamepad_binds) |keybind| { - const key_status = gamepad_state.buttons[@intFromEnum(keybind[0])]; - if (key_status == .press) { - c.press_buttons(keybind[1]); - } else if (key_status == .release) { - c.release_buttons(keybind[1]); - } + if (!any_keyboard_key_pressed) { + if (self.controllers[controller_idx]) |host_controller| { + if (zglfw.Joystick.get(host_controller.id)) |joystick| { + if (joystick.asGamepad()) |gamepad| { + const gamepad_state = gamepad.getState(); + const gamepad_binds: [9]struct { zglfw.Gamepad.Button, DreamcastModule.Maple.ControllerButtons } = .{ + .{ .start, .{ .start = 0 } }, + .{ .dpad_up, .{ .up = 0 } }, + .{ .dpad_down, .{ .down = 0 } }, + .{ .dpad_left, .{ .left = 0 } }, + .{ .dpad_right, .{ .right = 0 } }, + .{ .a, .{ .a = 0 } }, + .{ .b, .{ .b = 0 } }, + .{ .x, .{ .x = 0 } }, + .{ .y, .{ .y = 0 } }, + }; + for (gamepad_binds) |keybind| { + const key_status = gamepad_state.buttons[@intFromEnum(keybind[0])]; + if (key_status == .press) { + c.press_buttons(keybind[1]); + } else if (key_status == .release) { + c.release_buttons(keybind[1]); } - c.axis[0] = @as(u8, @intFromFloat((gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.right_trigger)] * 0.5 + 0.5) * 255)); - c.axis[1] = @as(u8, @intFromFloat((gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_trigger)] * 0.5 + 0.5) * 255)); - - var x_axis = gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_x)]; - var y_axis = gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_y)]; - if (@abs(x_axis) < host_controller.deadzone) - x_axis = 0.0; - if (@abs(y_axis) < host_controller.deadzone) - y_axis = 0.0; - // TODO: Remap with deadzone? - x_axis = x_axis * 0.5 + 0.5; - y_axis = y_axis * 0.5 + 0.5; - c.axis[2] = @as(u8, @intFromFloat(std.math.ceil(x_axis * 255))); - c.axis[3] = @as(u8, @intFromFloat(std.math.ceil(y_axis * 255))); } - } else { - // Not valid anymore? Disconnected? - self.controllers[controller_idx] = null; + c.axis[0] = @as(u8, @intFromFloat((gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.right_trigger)] * 0.5 + 0.5) * 255)); + c.axis[1] = @as(u8, @intFromFloat((gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_trigger)] * 0.5 + 0.5) * 255)); + + var x_axis = gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_x)]; + var y_axis = gamepad_state.axes[@intFromEnum(zglfw.Gamepad.Axis.left_y)]; + if (@abs(x_axis) < host_controller.deadzone) + x_axis = 0.0; + if (@abs(y_axis) < host_controller.deadzone) + y_axis = 0.0; + // TODO: Remap with deadzone? + x_axis = x_axis * 0.5 + 0.5; + y_axis = y_axis * 0.5 + 0.5; + c.axis[2] = @as(u8, @intFromFloat(std.math.ceil(x_axis * 255))); + c.axis[3] = @as(u8, @intFromFloat(std.math.ceil(y_axis * 255))); } + } else { + // Not valid anymore? Disconnected? + self.controllers[controller_idx] = null; } } - }, - else => {}, - } + } + }, + else => {}, } } } +} - pub fn load_and_start(self: *Deecy, path: []const u8) !void { - self.stop(); - try self.load_disk(path); - self.dc.set_region(self.dc.gdrom.disk.?.get_region()) catch |err| { - switch (err) { - error.FileNotFound => return error.MissingFlash, - else => return err, - } - }; - try self.on_game_load(); - try self.dc.reset(); - self.start(); - self.display_ui = false; - } +pub fn load_and_start(self: *@This(), path: []const u8) !void { + self.stop(); + try self.load_disk(path); + self.dc.set_region(self.dc.gdrom.disk.?.get_region()) catch |err| { + switch (err) { + error.FileNotFound => return error.MissingFlash, + else => return err, + } + }; + try self.on_game_load(); + try self.dc.reset(); + self.start(); + self.display_ui = false; +} - pub fn load_disk(self: *Deecy, path: []const u8) !void { - if (std.mem.endsWith(u8, path, ".zip")) { - var zip_file = try std.fs.cwd().openFile(path, .{}); - defer zip_file.close(); - var stream = zip_file.seekableStream(); - var iter = try std.zip.Iterator(std.fs.File.SeekableStream).init(stream); - var filename_buf: [std.fs.max_path_bytes]u8 = undefined; - var gdi_filename: []u8 = ""; - while (try iter.next()) |entry| { - const filename = filename_buf[0..entry.filename_len]; - try zip_file.seekTo(entry.header_zip_offset + @sizeOf(std.zip.CentralDirectoryFileHeader)); - std.debug.assert(try stream.context.reader().readAll(filename) == filename.len); - if (std.mem.endsWith(u8, filename, ".gdi")) { - gdi_filename = filename; - break; - } - } - if (gdi_filename.len == 0) { - std.log.err("Could not find GDI file in zip file '{s}'.", .{path}); - return error.GDIFileNotFound; +pub fn load_disk(self: *@This(), path: []const u8) !void { + if (std.mem.endsWith(u8, path, ".zip")) { + var zip_file = try std.fs.cwd().openFile(path, .{}); + defer zip_file.close(); + var stream = zip_file.seekableStream(); + var iter = try std.zip.Iterator(std.fs.File.SeekableStream).init(stream); + var filename_buf: [std.fs.max_path_bytes]u8 = undefined; + var gdi_filename: []u8 = ""; + while (try iter.next()) |entry| { + const filename = filename_buf[0..entry.filename_len]; + try zip_file.seekTo(entry.header_zip_offset + @sizeOf(std.zip.CentralDirectoryFileHeader)); + std.debug.assert(try stream.context.reader().readAll(filename) == filename.len); + if (std.mem.endsWith(u8, filename, ".gdi")) { + gdi_filename = filename; + break; } - var gdi_path_buf: [std.fs.max_path_bytes]u8 = undefined; - const tmp_gdi_path = try std.fmt.bufPrint(&gdi_path_buf, TmpDirPath ++ "/{s}", .{gdi_filename}); - std.log.info("Found GDI file: '{s}'.", .{gdi_filename}); - std.log.info("Extracting zip to '{s}'...", .{TmpDirPath}); - var tmp_dir = try std.fs.cwd().makeOpenPath(TmpDirPath, .{}); - defer tmp_dir.close(); - try std.zip.extract(tmp_dir, stream, .{}); - self.dc.gdrom.disk = try GDI.init(tmp_gdi_path, self._allocator); - } else { - self.dc.gdrom.disk = try GDI.init(path, self._allocator); } + if (gdi_filename.len == 0) { + std.log.err("Could not find GDI file in zip file '{s}'.", .{path}); + return error.GDIFileNotFound; + } + var gdi_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const tmp_gdi_path = try std.fmt.bufPrint(&gdi_path_buf, TmpDirPath ++ "/{s}", .{gdi_filename}); + std.log.info("Found GDI file: '{s}'.", .{gdi_filename}); + std.log.info("Extracting zip to '{s}'...", .{TmpDirPath}); + var tmp_dir = try std.fs.cwd().makeOpenPath(TmpDirPath, .{}); + defer tmp_dir.close(); + try std.zip.extract(tmp_dir, stream, .{}); + self.dc.gdrom.disk = try GDI.init(tmp_gdi_path, self._allocator); + } else { + self.dc.gdrom.disk = try GDI.init(path, self._allocator); } +} - pub fn get_product_name(self: *const @This()) ?[]const u8 { - return if (self.dc.gdrom.disk) |disk| disk.get_product_name() else null; - } +pub fn get_product_name(self: *const @This()) ?[]const u8 { + return if (self.dc.gdrom.disk) |disk| disk.get_product_name() else null; +} - pub fn get_product_id(self: *const @This()) ?[]const u8 { - return if (self.dc.gdrom.disk) |disk| disk.get_product_id() else null; - } +pub fn get_product_id(self: *const @This()) ?[]const u8 { + return if (self.dc.gdrom.disk) |disk| disk.get_product_id() else null; +} - pub fn on_game_load(self: *@This()) !void { - if (self.config.per_game_vmu) { - if (self.get_product_id()) |product_id| { - var vmu_path = std.ArrayList(u8).init(self._allocator); - defer vmu_path.deinit(); - try vmu_path.writer().print("./userdata/{s}/vmu_0.bin", .{product_id}); - safe_path(vmu_path.items); - - if (self.dc.maple.ports[0].subperipherals[0]) |*peripheral| { - switch (peripheral.*) { - .VMU => |*vmu| vmu.deinit(self._allocator), - else => {}, - } +pub fn on_game_load(self: *@This()) !void { + if (self.config.per_game_vmu) { + if (self.get_product_id()) |product_id| { + var vmu_path = std.ArrayList(u8).init(self._allocator); + defer vmu_path.deinit(); + try vmu_path.writer().print("./userdata/{s}/vmu_0.bin", .{product_id}); + safe_path(vmu_path.items); + + if (self.dc.maple.ports[0].subperipherals[0]) |*peripheral| { + switch (peripheral.*) { + .VMU => |*vmu| vmu.deinit(self._allocator), + else => {}, } - self.dc.maple.ports[0].subperipherals[0] = .{ .VMU = try DreamcastModule.Maple.VMU.init(self._allocator, vmu_path.items) }; - self.dc.maple.ports[0].subperipherals[0].?.VMU.on_screen_update = .{ .function = @ptrCast(&DeecyUI.update_vmu_screen_0_0), .userdata = &self.ui }; } + self.dc.maple.ports[0].subperipherals[0] = .{ .VMU = try DreamcastModule.Maple.VMU.init(self._allocator, vmu_path.items) }; + self.dc.maple.ports[0].subperipherals[0].?.VMU.on_screen_update = .{ .function = @ptrCast(&DeecyUI.update_vmu_screen_0_0), .userdata = &self.ui }; } - try self.check_save_state_slots(); - - var title = try std.ArrayList(u8).initCapacity(self._allocator, 64); - defer title.deinit(); - try title.appendSlice("Deecy"); - if (self.get_product_name()) |name| { - try title.appendSlice(" - "); - try title.appendSlice(name); - if (self.get_product_id()) |id| { - try title.appendSlice(" ("); - try title.appendSlice(id); - try title.append(')'); - } + } + try self.check_save_state_slots(); + + var title = try std.ArrayList(u8).initCapacity(self._allocator, 64); + defer title.deinit(); + try title.appendSlice("Deecy"); + if (self.get_product_name()) |name| { + try title.appendSlice(" - "); + try title.appendSlice(name); + if (self.get_product_id()) |id| { + try title.appendSlice(" ("); + try title.appendSlice(id); + try title.append(')'); } - try title.append(0); - self.window.setTitle(title.items[0 .. title.items.len - 1 :0]); } + try title.append(0); + self.window.setTitle(title.items[0 .. title.items.len - 1 :0]); +} - // Caller owns the returned ArrayList - fn save_state_path(self: *const @This(), index: usize) !std.ArrayList(u8) { - const product_id = self.get_product_id() orelse "default"; - var save_slot_path = std.ArrayList(u8).init(self._allocator); - try save_slot_path.writer().print("./userdata/{s}/save_{d}.sav", .{ product_id, index }); - safe_path(save_slot_path.items); - return save_slot_path; - } +// Caller owns the returned ArrayList +fn save_state_path(self: *const @This(), index: usize) !std.ArrayList(u8) { + const product_id = self.get_product_id() orelse "default"; + var save_slot_path = std.ArrayList(u8).init(self._allocator); + try save_slot_path.writer().print("./userdata/{s}/save_{d}.sav", .{ product_id, index }); + safe_path(save_slot_path.items); + return save_slot_path; +} - fn check_save_state_slots(self: *@This()) !void { - for (0..self.save_state_slots.len) |i| { - var save_slot_path = try self.save_state_path(i); - defer save_slot_path.deinit(); - self.save_state_slots[i] = try file_exists(save_slot_path.items); - } +fn check_save_state_slots(self: *@This()) !void { + for (0..self.save_state_slots.len) |i| { + var save_slot_path = try self.save_state_path(i); + defer save_slot_path.deinit(); + self.save_state_slots[i] = try file_exists(save_slot_path.items); } +} - fn reset_per_frame_throttling(self: *Deecy) void { - reset_semaphore(&self.dc_thread_semaphore); - self.dc_last_frame = std.time.Instant.now() catch unreachable; - } +fn reset_per_frame_throttling(self: *@This()) void { + reset_semaphore(&self.dc_thread_semaphore); + self.dc_last_frame = std.time.Instant.now() catch unreachable; +} - pub fn set_throttle_method(self: *Deecy, method: CPUThrottleMethod) void { - if (method == self.config.cpu_throttling_method) return; +pub fn set_throttle_method(self: *@This(), method: CPUThrottleMethod) void { + if (method == self.config.cpu_throttling_method) return; - switch (method) { - .None => self.dc_thread_semaphore.post(), // Make sure to wake up. - .PerFrame => self.reset_per_frame_throttling(), - } - self.config.cpu_throttling_method = method; + switch (method) { + .None => self.dc_thread_semaphore.post(), // Make sure to wake up. + .PerFrame => self.reset_per_frame_throttling(), } + self.config.cpu_throttling_method = method; +} - pub fn start(self: *Deecy) void { - if (!self.running) { - if (self.dc.region == .Unknown) { - self.dc.set_region(.USA) catch { - @panic("Failed to set default region"); - }; - } - self.running = true; - self.reset_per_frame_throttling(); - if (ExperimentalThreadedDC) { - self.dc_thread = std.Thread.spawn(.{}, dreamcast_thread_fn, .{self}) catch |err| { - self.running = false; - deecy_log.err(termcolor.red("Failed to start dreamcast thread: {s}"), .{@errorName(err)}); - return undefined; - }; - } +pub fn start(self: *@This()) void { + if (!self.running) { + if (self.dc.region == .Unknown) { + self.dc.set_region(.USA) catch { + @panic("Failed to set default region"); + }; + } + self.running = true; + self.reset_per_frame_throttling(); + if (ExperimentalThreadedDC) { + self.dc_thread = std.Thread.spawn(.{}, dreamcast_thread_fn, .{self}) catch |err| { + self.running = false; + deecy_log.err(termcolor.red("Failed to start dreamcast thread: {s}"), .{@errorName(err)}); + return undefined; + }; } } +} - pub fn stop(self: *Deecy) void { - if (self.running) { - self.running = false; - if (ExperimentalThreadedDC) { - self.dc_thread_semaphore.post(); - self.dc_thread.join(); - } +pub fn stop(self: *@This()) void { + if (self.running) { + self.running = false; + if (ExperimentalThreadedDC) { + self.dc_thread_semaphore.post(); + self.dc_thread.join(); } } +} - pub fn draw_ui(self: *@This()) !void { - zgui.backend.newFrame( - self.gctx.swapchain_descriptor.width, - self.gctx.swapchain_descriptor.height, - ); - - _ = zgui.DockSpaceOverViewport(0, zgui.getMainViewport(), .{ .passthru_central_node = true }); - - self.ui.draw_vmus(self.display_ui); - - if (self.display_ui) { - try self.ui.draw(self); - if (self.config.display_debug_ui) - try self.debug_ui.draw(self); - } else { - zgui.setNextWindowPos(.{ .x = 0, .y = 0 }); - if (zgui.begin("##FPSCounter", .{ .flags = .{ .no_resize = true, .no_move = true, .no_background = true, .no_title_bar = true, .no_mouse_inputs = true, .no_nav_inputs = true, .no_nav_focus = true } })) { - var sum: i128 = 0; - for (0..self.last_n_frametimes.count) |i| { - sum += self.last_n_frametimes.peekItem(i); - } - const avg: f32 = @as(f32, @floatFromInt(sum)) / @as(f32, @floatFromInt(self.last_n_frametimes.count)); - zgui.text("FPS: {d: >4.1} ({d: >3.1}ms)", .{ 1000000.0 / avg, avg / 1000.0 }); +pub fn draw_ui(self: *@This()) !void { + zgui.backend.newFrame( + self.gctx.swapchain_descriptor.width, + self.gctx.swapchain_descriptor.height, + ); + + _ = zgui.DockSpaceOverViewport(0, zgui.getMainViewport(), .{ .passthru_central_node = true }); + + self.ui.draw_vmus(self.display_ui); + + if (self.display_ui) { + try self.ui.draw(self); + if (self.config.display_debug_ui) + try self.debug_ui.draw(self); + } else { + zgui.setNextWindowPos(.{ .x = 0, .y = 0 }); + if (zgui.begin("##FPSCounter", .{ .flags = .{ .no_resize = true, .no_move = true, .no_background = true, .no_title_bar = true, .no_mouse_inputs = true, .no_nav_inputs = true, .no_nav_focus = true } })) { + var sum: i128 = 0; + for (0..self.last_n_frametimes.count) |i| { + sum += self.last_n_frametimes.peekItem(i); } - zgui.end(); + const avg: f32 = @as(f32, @floatFromInt(sum)) / @as(f32, @floatFromInt(self.last_n_frametimes.count)); + zgui.text("FPS: {d: >4.1} ({d: >3.1}ms)", .{ 1000000.0 / avg, avg / 1000.0 }); } - - self.submit_ui(); + zgui.end(); } - fn submit_ui(self: *@This()) void { - const swapchain_texv = self.gctx.swapchain.getCurrentTextureView(); - defer swapchain_texv.release(); - - const commands = commands: { - const encoder = self.gctx.device.createCommandEncoder(null); - defer encoder.release(); - // GUI pass - { - const pass = zgpu.beginRenderPassSimple(encoder, .load, swapchain_texv, null, null, null); - defer zgpu.endReleasePass(pass); - zgui.backend.draw(pass); - } - break :commands encoder.finish(null); - }; - defer commands.release(); + self.submit_ui(); +} - self.gctx.submit(&.{commands}); - } +fn submit_ui(self: *@This()) void { + const swapchain_texv = self.gctx.swapchain.getCurrentTextureView(); + defer swapchain_texv.release(); + + const commands = commands: { + const encoder = self.gctx.device.createCommandEncoder(null); + defer encoder.release(); + // GUI pass + { + const pass = zgpu.beginRenderPassSimple(encoder, .load, swapchain_texv, null, null, null); + defer zgpu.endReleasePass(pass); + zgui.backend.draw(pass); + } + break :commands encoder.finish(null); + }; + defer commands.release(); - pub fn one_frame(self: *Deecy) void { - if (ExperimentalThreadedDC) { - const target_frame_time = std.time.ns_per_s / 60; // FIXME: Adjust that based on the DC settings... + self.gctx.submit(&.{commands}); +} - // Internal representation of std.time.Instant is plateform dependent. To do arithmetic with it, we need to learn about it. - // FIXME: This is not ideal... It is unknown at compile tile, on Windows at least. But it should be constant for the duration of the program, I hope. - const static = struct { - var frame_time: u64 = 0; // In nanoseconds - var timestamp_diff: if (builtin.os.tag == .windows) u64 else std.posix.timespec = undefined; // In platform-dependent units - }; - if (static.frame_time != target_frame_time) { - static.frame_time = target_frame_time; - if (builtin.os.tag == .windows) { - const timestamp_scale = (std.time.Instant{ .timestamp = 1_000_000_000 }).since(std.time.Instant{ .timestamp = 0 }); - static.timestamp_diff = (target_frame_time * 1_000_000_000) / timestamp_scale; - } else { - static.timestamp_diff.sec = 0; - static.timestamp_diff.nsec = target_frame_time; - } +pub fn one_frame(self: *@This()) void { + if (ExperimentalThreadedDC) { + const target_frame_time = std.time.ns_per_s / 60; // FIXME: Adjust that based on the DC settings... + + // Internal representation of std.time.Instant is plateform dependent. To do arithmetic with it, we need to learn about it. + // FIXME: This is not ideal... It is unknown at compile tile, on Windows at least. But it should be constant for the duration of the program, I hope. + const static = struct { + var frame_time: u64 = 0; // In nanoseconds + var timestamp_diff: if (builtin.os.tag == .windows) u64 else std.posix.timespec = undefined; // In platform-dependent units + }; + if (static.frame_time != target_frame_time) { + static.frame_time = target_frame_time; + if (builtin.os.tag == .windows) { + const timestamp_scale = (std.time.Instant{ .timestamp = 1_000_000_000 }).since(std.time.Instant{ .timestamp = 0 }); + static.timestamp_diff = (target_frame_time * 1_000_000_000) / timestamp_scale; + } else { + static.timestamp_diff.sec = 0; + static.timestamp_diff.nsec = target_frame_time; } + } - if (self.running and self.config.cpu_throttling_method == .PerFrame) { - const now = std.time.Instant.now() catch unreachable; - const since = now.since(self.dc_last_frame); - if (since >= target_frame_time) { - self.dc_thread_semaphore.post(); // Schedule a new frame - - // Update last frame timestamp - if (since < 1_000_000 + target_frame_time) { - // Adding to the previous timestamp rather than using 'now' will compensate the latency between calls to one_frame(). - if (builtin.os.tag == .windows) { - self.dc_last_frame.timestamp += static.timestamp_diff; - } else { - self.dc_last_frame.timestamp.sec += static.timestamp_diff.sec; - self.dc_last_frame.timestamp.nsec += static.timestamp_diff.nsec; - self.dc_last_frame.timestamp.sec += @divTrunc(self.dc_last_frame.timestamp.nsec, std.time.ns_per_s); - self.dc_last_frame.timestamp.nsec = @rem(self.dc_last_frame.timestamp.nsec, std.time.ns_per_s); - } + if (self.running and self.config.cpu_throttling_method == .PerFrame) { + const now = std.time.Instant.now() catch unreachable; + const since = now.since(self.dc_last_frame); + if (since >= target_frame_time) { + self.dc_thread_semaphore.post(); // Schedule a new frame + + // Update last frame timestamp + if (since < 1_000_000 + target_frame_time) { + // Adding to the previous timestamp rather than using 'now' will compensate the latency between calls to one_frame(). + if (builtin.os.tag == .windows) { + self.dc_last_frame.timestamp += static.timestamp_diff; } else { - // We're way too slow, don't try to compensate. - self.dc_last_frame = now; + self.dc_last_frame.timestamp.sec += static.timestamp_diff.sec; + self.dc_last_frame.timestamp.nsec += static.timestamp_diff.nsec; + self.dc_last_frame.timestamp.sec += @divTrunc(self.dc_last_frame.timestamp.nsec, std.time.ns_per_s); + self.dc_last_frame.timestamp.nsec = @rem(self.dc_last_frame.timestamp.nsec, std.time.ns_per_s); } + } else { + // We're way too slow, don't try to compensate. + self.dc_last_frame = now; } } - } else { - self.run_dreamcast_until_next_frame(); } + } else { + self.run_dreamcast_until_next_frame(); } +} - fn run_dreamcast_until_next_frame(self: *Deecy) void { - var cycles: u64 = 0; - if (!self.enable_jit) { - while (self.running and !self.dc.gpu.vblank_signal()) { - const max_instructions: u8 = if (self.breakpoints.items.len == 0) 16 else 1; - - cycles += self.dc.tick(max_instructions) catch unreachable; - - // Doesn't make sense to try to have breakpoints if the interpreter can execute more than one instruction at a time. - if (max_instructions == 1) { - const breakpoint = for (self.breakpoints.items, 0..) |addr, index| { - if (addr & 0x1FFFFFFF == self.dc.cpu.pc & 0x1FFFFFFF) break index; - } else null; - if (breakpoint != null) { - self.running = false; - } +fn run_dreamcast_until_next_frame(self: *@This()) void { + var cycles: u64 = 0; + if (!self.enable_jit) { + while (self.running and !self.dc.gpu.vblank_signal()) { + const max_instructions: u8 = if (self.breakpoints.items.len == 0) 16 else 1; + + cycles += self.dc.tick(max_instructions) catch unreachable; + + // Doesn't make sense to try to have breakpoints if the interpreter can execute more than one instruction at a time. + if (max_instructions == 1) { + const breakpoint = for (self.breakpoints.items, 0..) |addr, index| { + if (addr & 0x1FFFFFFF == self.dc.cpu.pc & 0x1FFFFFFF) break index; + } else null; + if (breakpoint != null) { + self.running = false; } } - } else { - while (!self.dc.gpu.vblank_signal()) { - cycles += self.dc.tick_jit() catch unreachable; - } } - self.dc.maple.flush_vmus(); // FIXME: Won't flush if paused! + } else { + while (!self.dc.gpu.vblank_signal()) { + cycles += self.dc.tick_jit() catch unreachable; + } } + self.dc.maple.flush_vmus(); // FIXME: Won't flush if paused! +} - fn dreamcast_thread_fn(self: *Deecy) void { - deecy_log.info(termcolor.green("Dreamcast thread started."), .{}); +fn dreamcast_thread_fn(self: *@This()) void { + deecy_log.info(termcolor.green("Dreamcast thread started."), .{}); - while (self.running) { - if (self.config.cpu_throttling_method == .PerFrame) { - self.dc_thread_semaphore.wait(); - } - self.run_dreamcast_until_next_frame(); + while (self.running) { + if (self.config.cpu_throttling_method == .PerFrame) { + self.dc_thread_semaphore.wait(); } - - deecy_log.info(termcolor.red("Dreamcast thread stopped."), .{}); + self.run_dreamcast_until_next_frame(); } - // Display an error message and wait for the user to close the window. - fn display_unrecoverable_error(self: *@This(), comptime msg: []const u8) void { - while (!self.window.shouldClose()) { - zglfw.pollEvents(); + deecy_log.info(termcolor.red("Dreamcast thread stopped."), .{}); +} - zgui.backend.newFrame(self.gctx.swapchain_descriptor.width, self.gctx.swapchain_descriptor.height); +// Display an error message and wait for the user to close the window. +fn display_unrecoverable_error(self: *@This(), comptime msg: []const u8) void { + while (!self.window.shouldClose()) { + zglfw.pollEvents(); - if (!zgui.isPopupOpen("Error##Modal", .{})) { - zgui.openPopup("Error##Modal", .{}); - } + zgui.backend.newFrame(self.gctx.swapchain_descriptor.width, self.gctx.swapchain_descriptor.height); - if (zgui.beginPopupModal("Error##Modal", .{})) { - zgui.text(msg, .{}); - if (zgui.button("OK", .{})) { - self.window.setShouldClose(true); - } - zgui.endPopup(); + if (!zgui.isPopupOpen("Error##Modal", .{})) { + zgui.openPopup("Error##Modal", .{}); + } + + if (zgui.beginPopupModal("Error##Modal", .{})) { + zgui.text(msg, .{}); + if (zgui.button("OK", .{})) { + self.window.setShouldClose(true); } + zgui.endPopup(); + } - self.submit_ui(); + self.submit_ui(); - _ = self.gctx.present(); - } + _ = self.gctx.present(); } +} - fn audio_callback( - device: *zaudio.Device, - output: ?*anyopaque, - _: ?*const anyopaque, // Input - frame_count: u32, - ) callconv(.C) void { - const self: *@This() = @ptrCast(@alignCast(device.getUserData())); - const aica = &self.dc.aica; - - if (!self.running) return; - - aica.sample_mutex.lock(); - defer aica.sample_mutex.unlock(); - - if (AICA.ExperimentalExternalSampleGeneration) { - aica.generate_samples(self.dc, frame_count); - aica.update_timers(self.dc, frame_count); - - const sh4_cycles = (AICA.SH4CyclesPerSample + 1) * frame_count; - if (AICA.ExperimentalThreadedARM) { - aica.run_arm(sh4_cycles) catch |err| { - deecy_log.err("Failed to run AICA ARM core: {}\n", .{err}); - }; - } - } +fn audio_callback( + device: *zaudio.Device, + output: ?*anyopaque, + _: ?*const anyopaque, // Input + frame_count: u32, +) callconv(.C) void { + const self: *@This() = @ptrCast(@alignCast(device.getUserData())); + const aica = &self.dc.aica; - var out: [*]i32 = @ptrCast(@alignCast(output)); + if (!self.running) return; - var available: i64 = @as(i64, @intCast(aica.sample_write_offset)) - @as(i64, @intCast(aica.sample_read_offset)); - if (available < 0) available += aica.sample_buffer.len; - if (available <= 0) return; + aica.sample_mutex.lock(); + defer aica.sample_mutex.unlock(); - // std.debug.print("audio_callback: frame_count={d}, available={d}\n", .{ frame_count, available }); + if (AICA.ExperimentalExternalSampleGeneration) { + aica.generate_samples(self.dc, frame_count); + aica.update_timers(self.dc, frame_count); - for (0..@min(@as(usize, @intCast(available)), 2 * frame_count)) |i| { - out[i] = 30000 *| aica.sample_buffer[aica.sample_read_offset]; - aica.sample_read_offset = (aica.sample_read_offset + 1) % aica.sample_buffer.len; + const sh4_cycles = (AICA.SH4CyclesPerSample + 1) * frame_count; + if (AICA.ExperimentalThreadedARM) { + aica.run_arm(sh4_cycles) catch |err| { + deecy_log.err("Failed to run AICA ARM core: {}\n", .{err}); + }; } } - pub fn save_state(self: *Deecy, index: usize) !void { - const was_running = self.running; - if (was_running) self.stop(); - defer { - if (was_running) self.start(); - } + var out: [*]i32 = @ptrCast(@alignCast(output)); - const start_time = std.time.milliTimestamp(); - deecy_log.info("Saving State #{d}...", .{index}); + var available: i64 = @as(i64, @intCast(aica.sample_write_offset)) - @as(i64, @intCast(aica.sample_read_offset)); + if (available < 0) available += aica.sample_buffer.len; + if (available <= 0) return; - var uncompressed_array = try std.ArrayList(u8).initCapacity(self._allocator, 32 * 1024 * 1024); - _ = try self.dc.serialize(uncompressed_array.writer()); + // std.debug.print("audio_callback: frame_count={d}, available={d}\n", .{ frame_count, available }); - deecy_log.info(" Serialized state in {d} ms. Compressing...", .{std.time.milliTimestamp() - start_time}); + for (0..@min(@as(usize, @intCast(available)), 2 * frame_count)) |i| { + out[i] = 30000 *| aica.sample_buffer[aica.sample_read_offset]; + aica.sample_read_offset = (aica.sample_read_offset + 1) % aica.sample_buffer.len; + } +} - // FIXME: Not exactly the safest way of parallelizing this... - var thread = try std.Thread.spawn(.{ .allocator = self._allocator }, compress_and_dump_save_state, .{ - self, index, uncompressed_array, - }); - thread.detach(); +pub fn save_state(self: *@This(), index: usize) !void { + const was_running = self.running; + if (was_running) self.stop(); + defer { + if (was_running) self.start(); } - fn compress_and_dump_save_state(self: *@This(), index: usize, uncompressed_array: std.ArrayList(u8)) !void { - const start_time = std.time.milliTimestamp(); - defer uncompressed_array.deinit(); + const start_time = std.time.milliTimestamp(); + deecy_log.info("Saving State #{d}...", .{index}); - const compressed = try lz4.Standard.compress(self._allocator, uncompressed_array.items); - defer self._allocator.free(compressed); + var uncompressed_array = try std.ArrayList(u8).initCapacity(self._allocator, 32 * 1024 * 1024); + _ = try self.dc.serialize(uncompressed_array.writer()); - var save_slot_path = try self.save_state_path(index); - defer save_slot_path.deinit(); - var file = try std.fs.cwd().createFile(save_slot_path.items, .{}); - defer file.close(); - _ = try file.write(std.mem.asBytes(&uncompressed_array.items.len)); - _ = try file.write(compressed); + deecy_log.info(" Serialized state in {d} ms. Compressing...", .{std.time.milliTimestamp() - start_time}); - self.save_state_slots[index] = true; + // FIXME: Not exactly the safest way of parallelizing this... + var thread = try std.Thread.spawn(.{ .allocator = self._allocator }, compress_and_dump_save_state, .{ + self, index, uncompressed_array, + }); + thread.detach(); +} - deecy_log.info(" Saved State #{d} to '{s}' in {d}ms", .{ index, save_slot_path.items, std.time.milliTimestamp() - start_time }); - } +fn compress_and_dump_save_state(self: *@This(), index: usize, uncompressed_array: std.ArrayList(u8)) !void { + const start_time = std.time.milliTimestamp(); + defer uncompressed_array.deinit(); - pub fn load_state(self: *Deecy, index: usize) !void { - const was_running = self.running; - if (was_running) self.stop(); - defer { - if (was_running) self.start(); - } + const compressed = try lz4.Standard.compress(self._allocator, uncompressed_array.items); + defer self._allocator.free(compressed); - var save_slot_path = try self.save_state_path(index); - defer save_slot_path.deinit(); + var save_slot_path = try self.save_state_path(index); + defer save_slot_path.deinit(); + var file = try std.fs.cwd().createFile(save_slot_path.items, .{}); + defer file.close(); + _ = try file.write(std.mem.asBytes(&uncompressed_array.items.len)); + _ = try file.write(compressed); - deecy_log.info("Loading State #{d} from '{s}'...", .{ index, save_slot_path.items }); + self.save_state_slots[index] = true; - const start_time = std.time.milliTimestamp(); + deecy_log.info(" Saved State #{d} to '{s}' in {d}ms", .{ index, save_slot_path.items, std.time.milliTimestamp() - start_time }); +} - var file = try std.fs.cwd().openFile(save_slot_path.items, .{}); - defer file.close(); +pub fn load_state(self: *@This(), index: usize) !void { + const was_running = self.running; + if (was_running) self.stop(); + defer { + if (was_running) self.start(); + } - var expected_size: usize = 0; - _ = try file.read(std.mem.asBytes(&expected_size)); + var save_slot_path = try self.save_state_path(index); + defer save_slot_path.deinit(); - const compressed = try file.readToEndAllocOptions(self._allocator, 32 * 1024 * 1024, null, 8, null); - defer self._allocator.free(compressed); + deecy_log.info("Loading State #{d} from '{s}'...", .{ index, save_slot_path.items }); - const decompressed = try lz4.Standard.decompress(self._allocator, compressed, expected_size); - defer self._allocator.free(decompressed); + const start_time = std.time.milliTimestamp(); - var uncompressed_stream = std.io.fixedBufferStream(decompressed); - var reader = uncompressed_stream.reader(); + var file = try std.fs.cwd().openFile(save_slot_path.items, .{}); + defer file.close(); - try self.reset(); + var expected_size: usize = 0; + _ = try file.read(std.mem.asBytes(&expected_size)); - _ = try self.dc.deserialize(&reader); - deecy_log.info("Loaded State #{d} from '{s}' in {d}ms", .{ index, save_slot_path.items, std.time.milliTimestamp() - start_time }); - } -}; + const compressed = try file.readToEndAllocOptions(self._allocator, 32 * 1024 * 1024, null, 8, null); + defer self._allocator.free(compressed); + + const decompressed = try lz4.Standard.decompress(self._allocator, compressed, expected_size); + defer self._allocator.free(decompressed); + + var uncompressed_stream = std.io.fixedBufferStream(decompressed); + var reader = uncompressed_stream.reader(); + + try self.reset(); + + _ = try self.dc.deserialize(&reader); + deecy_log.info("Loaded State #{d} from '{s}' in {d}ms", .{ index, save_slot_path.items, std.time.milliTimestamp() - start_time }); +} diff --git a/src/deecy_ui.zig b/src/deecy_ui.zig index 64075828..978c24ec 100644 --- a/src/deecy_ui.zig +++ b/src/deecy_ui.zig @@ -8,7 +8,7 @@ const ui_log = std.log.scoped(.ui); const nfd = @import("nfd"); -const Deecy = @import("deecy.zig").Deecy; +const Deecy = @import("deecy.zig"); const MapleModule = @import("maple.zig"); last_error: []const u8 = "", @@ -276,7 +276,12 @@ pub fn draw(self: *@This(), d: *Deecy) !void { } if (zgui.beginTabItem("Renderer", .{})) { - zgui.text("Resolution: {d}x{d}", .{ d.renderer.resolution.width, d.renderer.resolution.height }); + zgui.text("Curent Resolution: {d}x{d}", .{ d.renderer.resolution.width, d.renderer.resolution.height }); + var resolution: enum(u8) { Native = 1, x2 = 2, x3 = 3, x4 = 4 } = @enumFromInt(d.renderer.resolution.width / Deecy.Renderer.NativeResolution.width); + if (zgui.comboFromEnum("Resolution", &resolution)) { + d.renderer.resolution = .{ .width = Deecy.Renderer.NativeResolution.width * @intFromEnum(resolution), .height = Deecy.Renderer.NativeResolution.height * @intFromEnum(resolution) }; + d.renderer.on_inner_resolution_change(); + } if (zgui.comboFromEnum("Display Mode", &d.renderer.display_mode)) d.renderer.update_blit_to_screen_vertex_buffer(); zgui.endTabItem(); diff --git a/src/main.zig b/src/main.zig index ae275d9f..f1746c08 100644 --- a/src/main.zig +++ b/src/main.zig @@ -22,7 +22,7 @@ const zglfw = @import("zglfw"); const RendererModule = @import("renderer.zig"); const Renderer = RendererModule.Renderer; -const Deecy = @import("deecy.zig").Deecy; +const Deecy = @import("deecy.zig"); pub fn customLog( comptime message_level: std.log.Level, From c8d588a98890eb84ca425cbce0efbcba66677103 Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sun, 3 Nov 2024 10:10:54 +0100 Subject: [PATCH 3/6] . --- src/deecy_ui.zig | 1 + src/renderer.zig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/deecy_ui.zig b/src/deecy_ui.zig index 978c24ec..37040672 100644 --- a/src/deecy_ui.zig +++ b/src/deecy_ui.zig @@ -279,6 +279,7 @@ pub fn draw(self: *@This(), d: *Deecy) !void { zgui.text("Curent Resolution: {d}x{d}", .{ d.renderer.resolution.width, d.renderer.resolution.height }); var resolution: enum(u8) { Native = 1, x2 = 2, x3 = 3, x4 = 4 } = @enumFromInt(d.renderer.resolution.width / Deecy.Renderer.NativeResolution.width); if (zgui.comboFromEnum("Resolution", &resolution)) { + // NOTE: This might not be the best idea to do this here without explicit synchronization but... This has worked flawlessly so far. d.renderer.resolution = .{ .width = Deecy.Renderer.NativeResolution.width * @intFromEnum(resolution), .height = Deecy.Renderer.NativeResolution.height * @intFromEnum(resolution) }; d.renderer.on_inner_resolution_change(); } diff --git a/src/renderer.zig b/src/renderer.zig index 6ed037c7..e9664421 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -739,7 +739,7 @@ pub const Renderer = struct { .{ .binding = 7, .texture_view_handle = texture_array_views[6] }, .{ .binding = 8, .texture_view_handle = texture_array_views[7] }, .{ .binding = 9, .buffer_handle = strips_metadata_buffer, .offset = 0, .size = StripMetadataSize }, - .{ .binding = 10, .buffer_handle = palette_buffer, .offset = 0, .size = 4 * 1024 }, // FIXME: zgpu limits bindings to 10 by group. + .{ .binding = 10, .buffer_handle = palette_buffer, .offset = 0, .size = 4 * 1024 }, }); const vertex_buffer = gctx.createBuffer(.{ From 39f6891ff9ce9e47abc7a89a0fae585d549b40d1 Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sun, 3 Nov 2024 13:09:16 +0100 Subject: [PATCH 4/6] WIP --- src/holly.zig | 2 +- src/mipmap.zig | 94 +++++++++++++++++++++++++++++++ src/renderer.zig | 29 +++++++--- src/shaders/fragment_color.wgsl | 10 +++- src/shaders/generate_mipmaps.wgsl | 9 +++ 5 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/mipmap.zig create mode 100644 src/shaders/generate_mipmaps.wgsl diff --git a/src/holly.zig b/src/holly.zig index 61aad503..4103e355 100644 --- a/src/holly.zig +++ b/src/holly.zig @@ -477,7 +477,7 @@ pub const TSPInstructionWord = packed struct(u32) { texture_shading_instruction: TextureShadingInstruction, mipmap_d_adjust: u4, supersample_texture: u1, - filter_mode: u2, + filter_mode: enum(u2) { Point = 0, Bilinear = 1, TrilinearPassA = 2, TrilinearPassB = 3 }, clamp_uv: u2, flip_uv: u2, ignore_texture_alpha: u1, diff --git a/src/mipmap.zig b/src/mipmap.zig new file mode 100644 index 00000000..496e6dfc --- /dev/null +++ b/src/mipmap.zig @@ -0,0 +1,94 @@ +const std = @import("std"); +const zgpu = @import("zgpu"); + +const ShaderSource = @embedFile("shaders/generate_mipmaps.wgsl"); + +bind_group_layout: zgpu.BindGroupLayoutHandle, +pipeline: zgpu.ComputePipelineHandle, + +pub fn init(gctx: *zgpu.GraphicsContext) @This() { + const bind_group_layout = gctx.createBindGroupLayout(&.{ + zgpu.textureEntry(0, .{ .compute = true }, .unfilterable_float, .tvdim_2d, false), + zgpu.storageTextureEntry(1, .{ .compute = true }, .write_only, .bgra8_unorm, .tvdim_2d), + }); + const pipeline_layout = gctx.createPipelineLayout(&.{ + bind_group_layout, + }); + defer gctx.releaseResource(pipeline_layout); + + const cs_module = zgpu.createWgslShaderModule(gctx.device, ShaderSource, "generate_mipmaps"); + defer cs_module.release(); + + return .{ + .bind_group_layout = bind_group_layout, + .pipeline = gctx.createComputePipeline(pipeline_layout, .{ + .compute = .{ + .module = cs_module, + .entry_point = "main", + }, + }), + }; +} + +pub fn deinit(self: *@This(), gctx: *zgpu.GraphicsContext) void { + gctx.releaseResource(self.bind_group_layout); + gctx.releaseResource(self.pipeline); +} + +pub fn generate_mipmaps(self: @This(), gctx: *zgpu.GraphicsContext, texture: zgpu.TextureHandle, layer: u32) void { + const texture_info = gctx.lookupResourceInfo(texture) orelse return; + if (texture_info.mip_level_count == 1) return; + + std.debug.assert(texture_info.usage.copy_dst == true); + std.debug.assert(texture_info.dimension == .tdim_2d); + std.debug.assert(texture_info.size.width == texture_info.size.height); + std.debug.assert(texture_info.size.width >= 8 and texture_info.size.width <= 1024); + std.debug.assert(std.math.isPowerOfTwo(texture_info.size.width)); + + const src_texture_view = gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = 0, + .mip_level_count = 1, + }); + defer gctx.releaseResource(src_texture_view); + const dst_texture_view = gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = 1, + .mip_level_count = 1, + }); + defer gctx.releaseResource(dst_texture_view); + + const bind_group = gctx.createBindGroup(self.bind_group_layout, &.{ + .{ .binding = 0, .texture_view_handle = src_texture_view }, + .{ .binding = 1, .texture_view_handle = dst_texture_view }, + }); + defer gctx.releaseResource(bind_group); + + const commands = commands: { + const encoder = gctx.device.createCommandEncoder(null); + defer encoder.release(); + + { + const pass = encoder.beginComputePass(null); + defer { + pass.end(); + pass.release(); + } + pass.setPipeline(gctx.lookupResource(self.pipeline).?); + + pass.setBindGroup(0, gctx.lookupResource(bind_group).?, &.{}); + + const num_groups = [2]u32{ @divExact(texture_info.size.width, 8), @divExact(texture_info.size.height, 8) }; + pass.dispatchWorkgroups(num_groups[0], num_groups[1], 1); + } + + break :commands encoder.finish(null); + }; + defer commands.release(); + + gctx.submit(&.{commands}); +} diff --git a/src/renderer.zig b/src/renderer.zig index e9664421..a1de219a 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -18,6 +18,8 @@ const YUV422 = Colors.YUV422; const Dreamcast = @import("dreamcast.zig").Dreamcast; const HollyModule = @import("holly.zig"); +const MipMap = @import("mipmap.zig"); + pub const ExperimentalFBWriteBack = false; // First 1024 values of the Moser de Bruijin sequence, Textures on the dreamcast are limited to 1024*1024 pixels. @@ -81,7 +83,8 @@ const ShadingInstructions = packed struct(u32) { shadow_bit: u1, gouraud_bit: u1, volume_bit: u1, - _: u7 = 0, + mipmap_bit: u1, + _: u6 = 0, }; fn sampler_index(mag_filter: wgpu.FilterMode, min_filter: wgpu.FilterMode, mipmap_filter: wgpu.MipmapFilterMode, address_mode_u: wgpu.AddressMode, address_mode_v: wgpu.AddressMode) u8 { @@ -544,6 +547,8 @@ pub const Renderer = struct { strips_metadata: std.ArrayList(StripMetadata) = undefined, // Just here to avoid repeated allocations. modifier_volume_vertices: std.ArrayList([4]f32) = undefined, + mipmap_gen_pipeline: MipMap, + _scratch_pad: []u8 align(4), // Used to avoid temporary allocations before GPU uploads for example. 4 * 1024 * 1024, since this is the maximum texture size supported by the DC. _gctx: *zgpu.GraphicsContext, @@ -706,14 +711,14 @@ pub const Renderer = struct { var texture_array_views: [8]zgpu.TextureViewHandle = undefined; for (0..8) |i| { texture_arrays[i] = gctx.createTexture(.{ - .usage = .{ .texture_binding = true, .copy_dst = true }, + .usage = .{ .texture_binding = true, .storage_binding = true, .copy_dst = true }, .size = .{ .width = @as(u32, 8) << @intCast(i), .height = @as(u32, 8) << @intCast(i), .depth_or_array_layers = Renderer.MaxTextures[i], }, .format = .bgra8_unorm, - .mip_level_count = 1, // std.math.log2_int(u32, @as(u32, 8))) + 1, + .mip_level_count = @intCast(3 + i), }); texture_array_views[i] = gctx.createTextureView(texture_arrays[i], .{}); } @@ -1163,6 +1168,8 @@ pub const Renderer = struct { .ta_lists = HollyModule.TALists.init(allocator), + .mipmap_gen_pipeline = MipMap.init(gctx), + ._scratch_pad = try allocator.allocWithOptions(u8, 4 * 1024 * 1024, 4, null), ._gctx = gctx, @@ -1179,6 +1186,8 @@ pub const Renderer = struct { self._allocator.free(self._scratch_pad); + self.mipmap_gen_pipeline.deinit(self._gctx); + self.translucent_pass.deinit(); self.punchthrough_pass.deinit(); self.opaque_pass.deinit(); @@ -1692,8 +1701,7 @@ pub const Renderer = struct { } if (texture_control_word.mip_mapped == 1) { - // TODO: Here we'd want to generate mipmaps. - // See zgpu.generateMipmaps, maybe? + self.mipmap_gen_pipeline.generate_mipmaps(self._gctx, self.texture_arrays[size_index], texture_index); } return texture_index; @@ -1853,7 +1861,7 @@ pub const Renderer = struct { .index = tex_idx, .palette = .{ .palette = texture_control.pixel_format == .Palette4BPP or texture_control.pixel_format == .Palette8BPP, - .filtered = tsp_instruction.filter_mode != 0, + .filtered = tsp_instruction.filter_mode != .Point, .selector = @truncate(texture_control.palette_selector() >> 4), }, .shading = .{ @@ -1868,6 +1876,7 @@ pub const Renderer = struct { .shadow_bit = 0, .gouraud_bit = isp_tsp_instruction.gouraud, .volume_bit = 0, + .mipmap_bit = 0, }, }; @@ -2127,7 +2136,7 @@ pub const Renderer = struct { // TODO: Add support for mipmapping (Tri-linear filtering) (And figure out what Pass A and Pass B means!). // Force nearest filtering when using palette textures (we'll be sampling indices into the palette). Filtering will have to be done in the shader. - const filter_mode: wgpu.FilterMode = if (texture_control.pixel_format == .Palette4BPP or texture_control.pixel_format == .Palette8BPP) .nearest else if (tsp_instruction.filter_mode == 0) .nearest else .linear; + const filter_mode: wgpu.FilterMode = if (texture_control.pixel_format == .Palette4BPP or texture_control.pixel_format == .Palette8BPP) .nearest else if (tsp_instruction.filter_mode == .Point) .nearest else .linear; const sampler = if (textured) sampler_index(filter_mode, filter_mode, .linear, u_addr_mode, v_addr_mode) else sampler_index(.linear, .linear, .linear, .clamp_to_edge, .clamp_to_edge); @@ -2135,7 +2144,7 @@ pub const Renderer = struct { .index = tex_idx, .palette = .{ .palette = texture_control.pixel_format == .Palette4BPP or texture_control.pixel_format == .Palette8BPP, - .filtered = tsp_instruction.filter_mode != 0, + .filtered = tsp_instruction.filter_mode != .Point, .selector = @truncate(texture_control.palette_selector() >> 4), }, .shading = .{ @@ -2152,6 +2161,7 @@ pub const Renderer = struct { .shadow_bit = parameter_control_word.obj_control.shadow, .gouraud_bit = isp_tsp_instruction.gouraud, .volume_bit = parameter_control_word.obj_control.volume, + .mipmap_bit = texture_control.mip_mapped, }, }; @@ -2159,7 +2169,7 @@ pub const Renderer = struct { .index = tex_idx_area_1, .palette = .{ .palette = if (area1_texture_control) |a| a.pixel_format == .Palette4BPP or a.pixel_format == .Palette8BPP else false, - .filtered = atspi.filter_mode != 0, + .filtered = atspi.filter_mode != .Point, .selector = if (area1_texture_control) |a| @truncate(a.palette_selector() >> 4) else 0, }, .shading = .{ @@ -2176,6 +2186,7 @@ pub const Renderer = struct { .shadow_bit = parameter_control_word.obj_control.shadow, .gouraud_bit = isp_tsp_instruction.gouraud, .volume_bit = parameter_control_word.obj_control.volume, + .mipmap_bit = if (area1_texture_control) |a| a.mip_mapped else 0, }, } else VertexTextureInfo.invalid(); diff --git a/src/shaders/fragment_color.wgsl b/src/shaders/fragment_color.wgsl index 503dc626..e9f319e5 100644 --- a/src/shaders/fragment_color.wgsl +++ b/src/shaders/fragment_color.wgsl @@ -20,10 +20,14 @@ fn bilinear_interpolation(u_min_v_min: vec4, u_min_v_max: vec4, u_max_ fn tex_array_sample(tex_array: texture_2d_array, uv: vec2, duvdx: vec2, duvdy: vec2, palette_instructions: u32, control: u32, index: u32) -> vec4 { if index >= textureNumLayers(tex_array) { return vec4(1.0, 0.0, 0.0, 1.0); } + let mipmap: bool = ((control >> 25) & 1) == 1; + if (palette_instructions & 1) == 1 { // Palette Texture let palette_selector = ((palette_instructions >> 2) & 0x3F) << 4; + // TODO: Handle mipmaps? + if ((palette_instructions >> 1) & 1) == 1 { // with Bilinear filtering let texel_coord = vec2(textureDimensions(tex_array)) * uv; @@ -67,7 +71,11 @@ fn tex_array_sample(tex_array: texture_2d_array, uv: vec2, duvdx: vec2 return unpack4x8unorm(palette[palette_selector + palette_index]).zyxw; } } else { - return textureSampleGrad(tex_array, image_sampler, uv, index, duvdx, duvdy); + if mipmap { + return textureSampleGrad(tex_array, image_sampler, uv, index, duvdx, duvdy); + } else { + return textureSampleLevel(tex_array, image_sampler, uv, index, 0); + } } } diff --git a/src/shaders/generate_mipmaps.wgsl b/src/shaders/generate_mipmaps.wgsl new file mode 100644 index 00000000..7e4ee927 --- /dev/null +++ b/src/shaders/generate_mipmaps.wgsl @@ -0,0 +1,9 @@ +@group(0) @binding(0) var previousMipLevel: texture_2d; +@group(0) @binding(1) var nextMipLevel: texture_storage_2d; + +@compute @workgroup_size(8, 8) +fn main(@builtin(global_invocation_id) id: vec3) { + let offset = vec2(0, 1); + let color = (textureLoad(previousMipLevel, 2 * id.xy + offset.xx, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.xy, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.yx, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.yy, 0)) * 0.25; + textureStore(nextMipLevel, id.xy, color); +} \ No newline at end of file From 964d236bff7d0e418ca540c8c60c41ff4194479a Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sun, 3 Nov 2024 14:55:14 +0100 Subject: [PATCH 5/6] Mipmap generation --- src/mipmap.zig | 89 ++++++++++++++++++++----------- src/renderer.zig | 2 +- src/shaders/generate_mipmaps.wgsl | 43 ++++++++++++--- 3 files changed, 94 insertions(+), 40 deletions(-) diff --git a/src/mipmap.zig b/src/mipmap.zig index 496e6dfc..907d6bca 100644 --- a/src/mipmap.zig +++ b/src/mipmap.zig @@ -10,6 +10,8 @@ pub fn init(gctx: *zgpu.GraphicsContext) @This() { const bind_group_layout = gctx.createBindGroupLayout(&.{ zgpu.textureEntry(0, .{ .compute = true }, .unfilterable_float, .tvdim_2d, false), zgpu.storageTextureEntry(1, .{ .compute = true }, .write_only, .bgra8_unorm, .tvdim_2d), + zgpu.storageTextureEntry(2, .{ .compute = true }, .write_only, .bgra8_unorm, .tvdim_2d), + zgpu.storageTextureEntry(3, .{ .compute = true }, .write_only, .bgra8_unorm, .tvdim_2d), }); const pipeline_layout = gctx.createPipelineLayout(&.{ bind_group_layout, @@ -37,7 +39,6 @@ pub fn deinit(self: *@This(), gctx: *zgpu.GraphicsContext) void { pub fn generate_mipmaps(self: @This(), gctx: *zgpu.GraphicsContext, texture: zgpu.TextureHandle, layer: u32) void { const texture_info = gctx.lookupResourceInfo(texture) orelse return; - if (texture_info.mip_level_count == 1) return; std.debug.assert(texture_info.usage.copy_dst == true); std.debug.assert(texture_info.dimension == .tdim_2d); @@ -45,47 +46,73 @@ pub fn generate_mipmaps(self: @This(), gctx: *zgpu.GraphicsContext, texture: zgp std.debug.assert(texture_info.size.width >= 8 and texture_info.size.width <= 1024); std.debug.assert(std.math.isPowerOfTwo(texture_info.size.width)); - const src_texture_view = gctx.createTextureView(texture, .{ - .dimension = .tvdim_2d, - .base_array_layer = layer, - .array_layer_count = 1, - .base_mip_level = 0, - .mip_level_count = 1, - }); - defer gctx.releaseResource(src_texture_view); - const dst_texture_view = gctx.createTextureView(texture, .{ - .dimension = .tvdim_2d, - .base_array_layer = layer, - .array_layer_count = 1, - .base_mip_level = 1, - .mip_level_count = 1, - }); - defer gctx.releaseResource(dst_texture_view); - - const bind_group = gctx.createBindGroup(self.bind_group_layout, &.{ - .{ .binding = 0, .texture_view_handle = src_texture_view }, - .{ .binding = 1, .texture_view_handle = dst_texture_view }, - }); - defer gctx.releaseResource(bind_group); - const commands = commands: { const encoder = gctx.device.createCommandEncoder(null); defer encoder.release(); - { - const pass = encoder.beginComputePass(null); + const pass = encoder.beginComputePass(null); + pass.setPipeline(gctx.lookupResource(self.pipeline).?); + + const last_mip_level = texture_info.mip_level_count - 1; + var base_mip_level: u32 = 0; + while (base_mip_level < last_mip_level) { + const src_texture_view = gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = base_mip_level, + .mip_level_count = 1, + }); + defer gctx.releaseResource(src_texture_view); + const dst_texture_views = [3]zgpu.TextureViewHandle{ + gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = base_mip_level + 1, + .mip_level_count = 1, + }), + gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = base_mip_level + 2, + .mip_level_count = 1, + }), + gctx.createTextureView(texture, .{ + .dimension = .tvdim_2d, + .base_array_layer = layer, + .array_layer_count = 1, + .base_mip_level = base_mip_level + 3, + .mip_level_count = 1, + }), + }; defer { - pass.end(); - pass.release(); + for (dst_texture_views) |dst_texture_view| + gctx.releaseResource(dst_texture_view); } - pass.setPipeline(gctx.lookupResource(self.pipeline).?); - pass.setBindGroup(0, gctx.lookupResource(bind_group).?, &.{}); + const bind_group = gctx.createBindGroup(self.bind_group_layout, &.{ + .{ .binding = 0, .texture_view_handle = src_texture_view }, + .{ .binding = 1, .texture_view_handle = dst_texture_views[0] }, + .{ .binding = 2, .texture_view_handle = dst_texture_views[1] }, + .{ .binding = 3, .texture_view_handle = dst_texture_views[2] }, + }); + defer gctx.releaseResource(bind_group); - const num_groups = [2]u32{ @divExact(texture_info.size.width, 8), @divExact(texture_info.size.height, 8) }; + pass.setBindGroup(0, gctx.lookupResource(bind_group).?, &.{}); + const num_groups = [2]u32{ @divExact(texture_info.size.width, std.math.pow(u32, 2, 1 + base_mip_level) * 4), @divExact(texture_info.size.height, std.math.pow(u32, 2, 1 + base_mip_level) * 4) }; pass.dispatchWorkgroups(num_groups[0], num_groups[1], 1); + + // We want the source to always be at least 4 * 4 (a single workgroup). We'll compute some small (<=8x8) mipmaps twice, but that's ok. + if (last_mip_level - base_mip_level > 3 and last_mip_level - base_mip_level < 6) { + base_mip_level = last_mip_level - 3; + } else base_mip_level += 3; } + pass.end(); + pass.release(); + break :commands encoder.finish(null); }; defer commands.release(); diff --git a/src/renderer.zig b/src/renderer.zig index a1de219a..2c330336 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -718,7 +718,7 @@ pub const Renderer = struct { .depth_or_array_layers = Renderer.MaxTextures[i], }, .format = .bgra8_unorm, - .mip_level_count = @intCast(3 + i), + .mip_level_count = @intCast(4 + i), }); texture_array_views[i] = gctx.createTextureView(texture_arrays[i], .{}); } diff --git a/src/shaders/generate_mipmaps.wgsl b/src/shaders/generate_mipmaps.wgsl index 7e4ee927..9a273006 100644 --- a/src/shaders/generate_mipmaps.wgsl +++ b/src/shaders/generate_mipmaps.wgsl @@ -1,9 +1,36 @@ -@group(0) @binding(0) var previousMipLevel: texture_2d; -@group(0) @binding(1) var nextMipLevel: texture_storage_2d; - -@compute @workgroup_size(8, 8) -fn main(@builtin(global_invocation_id) id: vec3) { - let offset = vec2(0, 1); - let color = (textureLoad(previousMipLevel, 2 * id.xy + offset.xx, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.xy, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.yx, 0) + textureLoad(previousMipLevel, 2 * id.xy + offset.yy, 0)) * 0.25; - textureStore(nextMipLevel, id.xy, color); +@group(0) @binding(0) var src: texture_2d; +@group(0) @binding(1) var dst0: texture_storage_2d; +@group(0) @binding(2) var dst1: texture_storage_2d; +@group(0) @binding(3) var dst2: texture_storage_2d; + +var cache : array, 16>; + +@compute @workgroup_size(4, 4) +fn main(@builtin(global_invocation_id) id: vec3, @builtin(local_invocation_id) local_id: vec3, @builtin(local_invocation_index) local_idx: u32) { + var color = (textureLoad(src, 2 * id.xy + vec2(0, 0), 0) + // + textureLoad(src, 2 * id.xy + vec2(0, 1), 0) + // + textureLoad(src, 2 * id.xy + vec2(1, 0), 0) + // + textureLoad(src, 2 * id.xy + vec2(1, 1), 0)) * 0.25; + textureStore(dst0, id.xy, color); + cache[local_idx] = color; + + workgroupBarrier(); + + if local_id.x % 2 == 0 && local_id.y % 2 == 0 { + color = (cache[local_idx / 2 + 0] + cache[local_idx / 2 + 1] + cache[local_idx / 2 + 4] + cache[local_idx / 2 + 5]) * 0.25; + textureStore(dst1, id.xy / 2u, color); + } + + workgroupBarrier(); + + if local_id.x % 2 == 0 && local_id.y % 2 == 0 { + cache[local_idx / 2] = color; + } + + workgroupBarrier(); + + if local_idx == 0 { + color = (cache[0] + cache[1] + cache[2] + cache[3]) * 0.25; + textureStore(dst2, id.xy / 4u, color); + } } \ No newline at end of file From 05716fc0d47eaa41cbf713573d31dbeabf2f40d2 Mon Sep 17 00:00:00 2001 From: Senryoku Date: Sun, 3 Nov 2024 15:08:23 +0100 Subject: [PATCH 6/6] Readme --- Readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 63959018..ba361e1a 100644 --- a/Readme.md +++ b/Readme.md @@ -53,7 +53,7 @@ sudo apt install libgtk-3-dev - Fog LUT Mode 2. - Secondary accumulate buffer (very low priority, not sure if many games use this feature). - Bump mapping. - - Handle and generate Mipmaps. + - Mipmaps for palette textures? - Sort-DMA? - User Tile Clip, only the simplest version is supported. - MMU: Only supported for store queue writes using the pref intruction (used by Ikaruga for example)