platform: SDL3 backend (desktop + WASM) + two bug repros
- library/modules/platform/sdl3.sx: SdlPlatform impl wrapping SDL3 init,
GL context, event pump, swap. run_frame_loop owns the loop: while loop
on desktop, emscripten_set_main_loop on WASM. Registers an event-watch
that re-invokes the frame closure during macOS modal resize-drag so
content keeps rendering at the new size. safe_insets / keyboard /
show_keyboard / hide_keyboard are no-ops (these targets have no soft
keyboard).
Two compiler bug repros uncovered during the refactor:
- examples/issue-0020.sx: global `Foo = .{}` zero-initializes, ignoring
struct field defaults. Local `Foo = .{}` correctly applies defaults.
Workaround: set fields explicitly in an init method or heap-allocate
the value.
- examples/issue-0021.sx: an 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. The same expression in a `-> void`
function produces the right value; in a `-> bool` function it
silently produces 0. Bit the SX Chess game's dpi_scale calc inside
`SdlPlatform.init` (returns bool), making all text labels render
invisibly on retina. Workaround: hoist each `xx` cast into its own
f32 local.
Regression gate: 50/50 examples pass, macOS chess game runs at ~2700fps
(close to the pre-refactor 2900 baseline), WASM build still emits a
working .html/.js/.wasm/.data quad.
This commit is contained in:
54
examples/issue-0020.sx
Normal file
54
examples/issue-0020.sx
Normal file
@@ -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");
|
||||
}
|
||||
81
examples/issue-0021.sx
Normal file
81
examples/issue-0021.sx
Normal file
@@ -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");
|
||||
}
|
||||
224
library/modules/platform/sdl3.sx
Normal file
224
library/modules/platform/sdl3.sx
Normal file
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user