diff --git a/examples/issue-0020.sx b/examples/issue-0020.sx new file mode 100644 index 0000000..4dc24fc --- /dev/null +++ b/examples/issue-0020.sx @@ -0,0 +1,54 @@ +// issue-0020: Global `Foo = .{}` zero-initializes, ignoring field defaults +// +// Struct field defaults declared via `field: T = expr;` are honored when the +// struct is constructed at function-local scope, but are silently dropped +// when the struct is declared at module scope with `= .{}`. +// +// Repro: +// +// Foo :: struct { +// running: bool = true; // default +// } +// +// g_foo : Foo = .{}; // global → g_foo.running == false (BUG) +// +// main :: () { +// l_foo : Foo = .{}; // local → l_foo.running == true (correct) +// } +// +// Surface bites: +// +// - In the SX Chess game, the SDL3 platform backend stores a `running: bool +// = true` field on `SdlPlatform`. With `g_plat : SdlPlatform = .{};` the +// main loop's `while self.running { ... }` exits immediately because +// `running` was zero-initialized despite the field default. +// - Workaround: assign defaults explicitly in the type's `init` method, or +// spell every field out at the global construction site: +// g_plat : SdlPlatform = .{ running = true }; +// +// Likely cause: the globals path emits an LLVM ConstantAggregateZero (or +// memset-to-zero) for the initializer, skipping the per-field default-expr +// lowering used for local declarations. +// +// This file is a runnable repro: locals print "running=true", globals print +// "running=false". + +#import "modules/std.sx"; + +Foo :: struct { + running: bool = true; + x: s32 = 42; +} + +g_foo : Foo = .{}; + +main :: () -> void { + out("global running="); + out(if g_foo.running then "true" else "false"); + out("\n"); + + l_foo : Foo = .{}; + out("local running="); + out(if l_foo.running then "true" else "false"); + out("\n"); +} diff --git a/examples/issue-0021.sx b/examples/issue-0021.sx new file mode 100644 index 0000000..0c0aca2 --- /dev/null +++ b/examples/issue-0021.sx @@ -0,0 +1,81 @@ +// issue-0021: enclosing function's return type bleeds into `xx`'s target +// type inside an `if-then-else` expression on the RHS of a struct-field +// assignment +// +// ── Repro ────────────────────────────────────────────────────────────────── +// +// Foo :: struct { pixel_w: s32; dpi: f32; } +// +// calc_void :: (self: *Foo, wf: f32) { +// self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; +// } +// +// calc_bool :: (self: *Foo, wf: f32) -> bool { +// self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; +// true; +// } +// +// With `f.pixel_w = 2880` and `wf = 1440.0`: +// - `calc_void` produces `f.dpi = 2.0` (correct). +// - `calc_bool` produces `f.dpi = 0.0` (wrong). +// +// Only difference: the enclosing function's declared return type. The `xx` +// cast appears to take its target type from the enclosing function's return +// type rather than from the assignment LHS (which is `f32`). When the return +// type is `bool`, the divide is lowered against a non-numeric/zero-valued +// operand. +// +// ── Related, possibly same root cause ─────────────────────────────────────── +// +// In a struct-returning function, this form makes LLVM verification fail with +// `udiv { float, float, i32, i32, float, float }` — the divide is lowered as +// integer division over the function's return-struct shape: +// +// FC :: struct { a: f32; b: f32; c: s32; d: s32; e: f32; f: f32; } +// begin :: (self: *Foo) -> FC { +// if self.last_perf > 0 { +// self.delta_time = xx (current - self.last_perf) / xx freq; +// } +// FC.{ ... }; +// } +// +// ── Workaround ───────────────────────────────────────────────────────────── +// +// Hoist the `xx` cast into its own variable so the divide sees two +// already-typed f32 values: +// +// pw : f32 = xx self.pixel_w; +// self.dpi = if wf > 0.0 then pw / wf else 1.0; +// +// ── Real-world impact ────────────────────────────────────────────────────── +// +// The SX Chess game's dpi_scale calculation took this form inside +// `SdlPlatform.init` (which returns `bool`). On a retina display the +// dpi_scale silently became 0, so the glyph cache rasterized at scale=0 and +// every text label rendered invisibly. + +#import "modules/std.sx"; + +Foo :: struct { pixel_w: s32; dpi: f32; } + +calc_void :: (self: *Foo, wf: f32) { + self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; +} + +calc_bool :: (self: *Foo, wf: f32) -> bool { + self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0; + true; +} + +main :: () -> void { + f : *Foo = xx malloc(size_of(Foo)); + f.pixel_w = 2880; + + f.dpi = 0.0; + calc_void(f, 1440.0); + out("void-return (expect 200): "); out(int_to_string(xx (f.dpi * 100.0))); out("\n"); + + f.dpi = 0.0; + calc_bool(f, 1440.0); + out("bool-return (expect 200): "); out(int_to_string(xx (f.dpi * 100.0))); out("\n"); +} diff --git a/library/modules/platform/sdl3.sx b/library/modules/platform/sdl3.sx new file mode 100644 index 0000000..1ea2e22 --- /dev/null +++ b/library/modules/platform/sdl3.sx @@ -0,0 +1,224 @@ +#import "modules/std.sx"; +#import "modules/allocators.sx"; +#import "modules/compiler.sx"; +#import "modules/opengl.sx"; +#import "modules/sdl3.sx"; +#import "modules/wasm.sx"; +#import "modules/ui/types.sx"; +#import "modules/ui/events.sx"; +#import "modules/platform/types.sx"; +#import "modules/platform/api.sx"; + +g_sdl_plat : *SdlPlatform = null; + +SdlPlatform :: struct { + window: *void = null; + gl_ctx: *void = null; + running: bool = true; + + width: s32 = 0; + height: s32 = 0; + pixel_w: s32 = 0; + pixel_h: s32 = 0; + dpi_scale: f32 = 1.0; + + delta_time: f32 = 0.008; + last_perf: u64 = 0; + + frame_closure: Closure() = ---; + has_frame_closure: bool = false; + + events: List(Event) = .{}; + + stop :: (self: *SdlPlatform) { + self.running = false; + } +} + +impl Platform for SdlPlatform { + init :: (self: *SdlPlatform, title: [:0]u8, w: s32, h: s32) -> bool { + self.running = true; + self.has_frame_closure = false; + self.delta_time = 0.008; + self.dpi_scale = 1.0; + SDL_Init(SDL_INIT_VIDEO); + + inline if OS == { + case .wasm: { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + } + case .ios: { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0); + } + else: { + SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3); + } + } + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24); + + init_w := w; + init_h := h; + inline if OS == .wasm { + init_w = emscripten_run_script_int("window.innerWidth"); + init_h = emscripten_run_script_int("window.innerHeight"); + } else { + display_id := SDL_GetPrimaryDisplay(); + bounds : SDL_Rect = ---; + if SDL_GetDisplayUsableBounds(display_id, @bounds) { + init_w = bounds.w * 3 / 4; + init_h = bounds.h * 3 / 4; + } + } + + self.window = SDL_CreateWindow(title, init_w, init_h, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY); + if self.window == null { return false; } + + self.gl_ctx = SDL_GL_CreateContext(self.window); + SDL_GL_MakeCurrent(self.window, self.gl_ctx); + SDL_GL_SetSwapInterval(0); + + load_gl(@SDL_GL_GetProcAddress); + + SDL_GetWindowSize(self.window, @self.width, @self.height); + SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h); + wf : f32 = xx self.width; + pw : f32 = xx self.pixel_w; + self.dpi_scale = if wf > 0.0 then pw / wf else 1.0; + + glViewport(0, 0, self.pixel_w, self.pixel_h); + + g_sdl_plat = self; + self.last_perf = SDL_GetPerformanceCounter(); + true; + } + + run_frame_loop :: (self: *SdlPlatform, frame_fn: Closure()) { + self.frame_closure = frame_fn; + self.has_frame_closure = true; + g_sdl_plat = self; + + inline if OS != .wasm { + SDL_AddEventWatch(@sdl_event_watch, xx self); + } + + inline if OS == .wasm { + emscripten_set_main_loop(@sdl_wasm_tick, 0, 1); + } else { + while self.running { + frame_fn(); + } + } + } + + poll_events :: (self: *SdlPlatform) -> []Event { + self.events.len = 0; + sdl_event : SDL_Event = .none; + while SDL_PollEvent(@sdl_event) { + if sdl_event == { + case .quit: { self.running = false; } + case .window_resized: (data) { + self.width = data.data1; + self.height = data.data2; + SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h); + } + } + ui_event := translate_sdl_event(@sdl_event); + if ui_event != .none { + self.events.append(ui_event); + } + } + + result : []Event = ---; + result.ptr = self.events.items; + result.len = self.events.len; + result; + } + + begin_frame :: (self: *SdlPlatform) -> FrameContext { + current := SDL_GetPerformanceCounter(); + freq := SDL_GetPerformanceFrequency(); + if freq > 0 and self.last_perf > 0 { + diff : f32 = xx (current - self.last_perf); + fr : f32 = xx freq; + self.delta_time = diff / fr; + } + self.last_perf = current; + + inline if OS == .wasm { + new_w : s32 = 0; + new_h : s32 = 0; + SDL_GetWindowSize(self.window, @new_w, @new_h); + if new_w != self.width or new_h != self.height { + self.width = new_w; + self.height = new_h; + SDL_GetWindowSizeInPixels(self.window, @self.pixel_w, @self.pixel_h); + } + } + + FrameContext.{ + viewport_w = xx self.width, + viewport_h = xx self.height, + pixel_w = self.pixel_w, + pixel_h = self.pixel_h, + dpi_scale = self.dpi_scale, + delta_time = self.delta_time, + }; + } + + end_frame :: (self: *SdlPlatform) { + SDL_GL_SwapWindow(self.window); + } + + safe_insets :: (self: *SdlPlatform) -> EdgeInsets { + EdgeInsets.zero(); + } + + keyboard :: (self: *SdlPlatform) -> KeyboardState { + KeyboardState.zero(); + } + + show_keyboard :: (self: *SdlPlatform) { } + hide_keyboard :: (self: *SdlPlatform) { } + + shutdown :: (self: *SdlPlatform) { + inline if OS != .wasm { + SDL_GL_DestroyContext(self.gl_ctx); + SDL_DestroyWindow(self.window); + SDL_Quit(); + } + } +} + +// SDL fires the watch synchronously when events are added — including during +// macOS's modal resize-drag, when SDL_PollEvent can't run. Re-invoking the +// frame closure here keeps content rendering at the new size during the drag. +sdl_event_watch :: (userdata: *void, event: *SDL_Event) -> bool callconv(.c) { + plat : *SdlPlatform = xx userdata; + if event.* == { + case .window_resized: (data) { + plat.width = data.data1; + plat.height = data.data2; + SDL_GetWindowSizeInPixels(plat.window, @plat.pixel_w, @plat.pixel_h); + if plat.has_frame_closure { + fn := plat.frame_closure; + fn(); + } + } + } + true; +} + +sdl_wasm_tick :: () callconv(.c) { + if g_sdl_plat == null { return; } + if !g_sdl_plat.has_frame_closure { return; } + fn := g_sdl_plat.frame_closure; + fn(); +}