metal: GPU protocol + MetalGPU renders MSL triangle on iOS

Phase 8 step 3a of the Metal renderer port:

- New library/modules/gpu/ with types.sx (handles + ClearColor +
  TextureFormat enum), api.sx (GPU :: protocol { ... } covering the
  lifecycle / per-frame / resource / per-draw surface), and metal.sx
  (MetalGPU backend implementing the protocol against CAMetalLayer).
  Resource handles are 1-based indices into backend List(*void) tables.
  MTL aggregates >16 bytes (MTLRegion, MTLScissorRect) pass via *T to
  match arm64 Apple's indirect-by-reference ABI; MTLClearColor + CGSize
  go through the HFA path as direct fn-pointer casts on objc_msgSend.

- UIKitPlatform got a gpu_mode: GpuMode toggle + sibling SxMetalView
  class registration. In metal mode init skips EAGL context, the
  did_finish_launching IMP skips the EAGL drawable-properties dict,
  layoutSubviews reads the layer's bounds * dpi_scale into pixel_w/h
  instead of allocating a GL renderbuffer, and end_frame is a no-op
  (the MetalGPU owns its own present).

- examples/63-metal-clear.sx verifies the pipeline end-to-end on iOS
  sim — compiles a pass-through MSL shader (packed_float2/packed_float4
  to avoid alignment padding), uploads 3 vertices, draws a colored
  triangle on a dark-blue clear.

Compiler fixes (filed-and-fixed in this branch):

- inline if X { return E; } followed by a fall-through final expression
  no longer emits two terminators into the same basic block. Verified
  by examples/83-inline-if-return-fallthrough.sx.

- Top-level type alias Name :: u32; now resolves correctly as the type
  annotation on a global variable (was treated as ptr {}, breaking
  comparisons + initializers). Verified by examples/84-global-type-alias.sx.

Issue->feature promotion:

- 16 historical examples/issue-NNNN.sx repros now confirmed-fixed and
  renamed to focused feature names (67-82). Each gains a
  tests/expected/*.txt + .exit pair so the regression suite covers them.

- 5 stale issue repros deleted (subsumed by broader tests).

Regression suite: 68 passing, 0 failed. macOS chess builds + runs; wasm
chess builds; iOS sim GLES chess still renders the full board; iOS sim
Metal demo renders the triangle.
This commit is contained in:
agra
2026-05-17 19:36:37 +03:00
parent 2ff24e29cc
commit a938c4f900
66 changed files with 1248 additions and 376 deletions

120
examples/63-metal-clear.sx Normal file
View File

@@ -0,0 +1,120 @@
// iOS-only: bring up UIKitPlatform in Metal mode, clear the screen dark
// blue each frame, then draw a colored triangle via the GPU protocol —
// exercises create_shader (MSL compile + pipeline state), create_buffer
// + update_buffer, set_shader, set_vertex_buffer, and draw_triangles.
// Step 3b will port the UI renderer to use this same surface.
//
// Build for iOS sim:
// /Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim \
// examples/63-metal-clear.sx \
// -o /tmp/MetalClear --bundle /tmp/MetalClear.app \
// --bundle-id co.swipelab.metalclear -F ~/Library/Frameworks
// codesign --force --sign - --timestamp=none /tmp/MetalClear.app
// xcrun simctl install booted /tmp/MetalClear.app
// xcrun simctl launch --terminate-running-process booted co.swipelab.metalclear
//
// This file is iOS-only and not part of the JIT regression suite (no
// tests/expected/63-metal-clear.txt). The test runner skips it on macOS.
#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";
#import "modules/platform/api.sx";
#import "modules/platform/uikit.sx";
#import "modules/gpu/api.sx";
#import "modules/gpu/metal.sx";
#framework "UIKit";
#framework "QuartzCore";
#framework "OpenGLES";
// Pass-through vertex + fragment shader for a colored triangle. Vertex
// layout is { packed_float2 pos; packed_float4 color; } = 24 bytes —
// `packed_*` types have 4-byte alignment so the struct doesn't get
// padded between fields (a plain `float4` would force 16-byte alignment
// and pad the struct out to 32 bytes per vertex). Entry-point names
// (vmain / fmain) match what MetalGPU.create_shader looks up.
TRI_MSL :: #string MSL
#include <metal_stdlib>
using namespace metal;
struct Vertex {
packed_float2 pos;
packed_float4 color;
};
struct RasterizerData {
float4 position [[position]];
float4 color;
};
vertex RasterizerData vmain(uint vid [[vertex_id]],
constant Vertex* vertices [[buffer(0)]]) {
RasterizerData out;
out.position = float4(vertices[vid].pos, 0.0, 1.0);
out.color = float4(vertices[vid].color);
return out;
}
fragment float4 fmain(RasterizerData in [[stage_in]]) {
return in.color;
}
MSL;
TRI_VERTS : [18]f32 = .[
-0.6, -0.4, 1.0, 0.0, 0.0, 1.0,
0.6, -0.4, 0.0, 1.0, 0.0, 1.0,
0.0, 0.6, 0.0, 0.0, 1.0, 1.0,
];
g_plat : *UIKitPlatform = null;
g_gpu : *MetalGPU = null;
g_shader : ShaderHandle = 0;
g_vbuf : BufferHandle = 0;
frame :: () {
if g_plat == null { return; }
if g_gpu == null { return; }
// Lazy-init the GPU on the first frame where the layer is available
// (the layer is created during -[SxAppDelegate didFinishLaunching:]
// which fires AFTER our main() returns into UIApplicationMain).
if g_gpu.layer == null {
if g_plat.gl_layer == null { return; }
if !g_gpu.init(g_plat.gl_layer, g_plat.pixel_w, g_plat.pixel_h) { return; }
}
// Compile shader + upload vertex buffer once.
if g_shader == 0 {
g_shader = g_gpu.create_shader(TRI_MSL, "");
if g_shader == 0 { return; }
}
if g_vbuf == 0 {
g_vbuf = g_gpu.create_buffer(72); // 3 verts × 24 bytes
if g_vbuf == 0 { return; }
g_gpu.update_buffer(g_vbuf, xx @TRI_VERTS, 72);
}
bg : ClearColor = .{ r = 0.07, g = 0.10, b = 0.18, a = 1.0 };
if !g_gpu.begin_frame(bg) { return; }
g_gpu.set_shader(g_shader);
g_gpu.set_vertex_buffer(g_vbuf);
g_gpu.draw_triangles(0, 3);
g_gpu.end_frame(0.0);
}
main :: () -> s32 {
inline if OS != .ios { return 0; }
plat : *UIKitPlatform = xx context.allocator.alloc(size_of(UIKitPlatform));
plat.gpu_mode = .metal;
if !plat.init("Metal Clear", 0, 0) { return 1; }
g_plat = plat;
gpu : *MetalGPU = xx context.allocator.alloc(size_of(MetalGPU));
g_gpu = gpu;
plat.run_frame_loop(closure(frame));
plat.shutdown();
0;
}

View File

@@ -1,7 +1,6 @@
// issue-0002: impl for built-in types fails with "expected type name after 'for'"
//
// `impl Protocol for f32` should work the same as `impl Protocol for MyStruct`.
// Currently the parser rejects built-in type names (f32, s64, bool, etc.) after `for`.
// impl Protocol for built-in scalar types (f32, s64, bool, u32, ...) —
// both static dispatch (`f32.lerp(...)`) and protocol-boxed dispatch via
// `#inline` erasure.
Lerpable :: protocol #inline {
lerp :: (b: Self, t: f32) -> Self;

View File

@@ -1,12 +1,5 @@
// issue-0003: Generic struct with protocol #inline constraint generates wrong LLVM types
//
// When `Animated($T: Lerpable)` is monomorphized with a struct type like `Size`,
// the LLVM IR generates `{}` (empty type) instead of the actual struct layout
// for the `T` parameter in methods like `set_immediate`, `animate_to`, and `lerp`.
//
// Error: "Call parameter type does not match function signature!"
// call void @Animated__0.set_immediate(ptr ..., { float, float } ...)
// expected {} but got { float, float }
// Generic struct `Animated($T: Lerpable)` monomorphized with a struct type — the
// `#inline` protocol constraint participates in method dispatch via `self.from.lerp(...)`.
#import "modules/std.sx";
#import "modules/math";

View File

@@ -1,10 +1,6 @@
// issue-0004: scalar-to-vector conversion when all optional struct fields are null
//
// When a struct has multiple ?f32 fields and ALL are set to null simultaneously,
// passing that struct to a virtual function call triggers:
// "error: scalar-to-vector conversion failed"
//
// Setting at least one field to a concrete value works fine.
// Struct with multiple `?f32` fields, all set to `null` simultaneously, passed
// into a protocol-dispatched method. Exercises the all-null-payload path through
// the boxed call.
#import "modules/std.sx";

View File

@@ -1,8 +1,5 @@
// issue-0005: optional f32 field in struct loses value when earlier optional is null
//
// FIXED: null_literal was double-wrapped as Some in struct literal coercion.
// Root cause: inferExprType(null) returns .void, coerceToType(.void, ?f32)
// tried to wrap the already-null value as Some, corrupting the struct.
// Optional `?f32` fields in struct literals — exhaustively combine null/value
// for both fields, through both direct calls and protocol dispatch.
#import "modules/std.sx";

View File

@@ -0,0 +1,16 @@
// Integer literal `0` on the RHS of an integer comparison stays integer-typed
// even when the comparison is the condition of an `if-then-else` whose result
// type is `f32`. The comparison must not pick up the outer ternary's type.
#import "modules/std.sx";
main :: () -> void {
x : s64 = 42;
// OK: comparison in statement context
if x != 0 { out("ok\n"); }
// BUG: comparison as condition of f32 ternary — `0` inferred as f32
result : f32 = if x != 0 then 1.0 else 2.0;
print("result = {}\n", result);
}

View File

@@ -1,13 +1,6 @@
// issue-0007: protocol value stores dangling pointer to stack local
//
// When a concrete value is converted to a protocol value inside a function,
// and the protocol value is stored in a List (via a wrapper struct), the
// protocol value's data pointer points to the stack-local variable rather
// than a heap-allocated copy. After the function returns, the pointer is
// dangling and method dispatch crashes (SIGSEGV/SIGBUS).
//
// Inside the function: dispatch works (stack local still alive)
// After the function returns: dispatch crashes (stack local gone)
// Protocol value as a field of a wrapper struct, constructed from a stack
// local inside a function and appended to a `List`. The payload must be
// heap-copied so dispatch survives the constructing function returning.
#import "modules/std.sx";

View File

@@ -1,16 +1,6 @@
// issue-0008: protocol value created in a function and appended to a list
// still stores a dangling stack pointer (issue-0007 fix incomplete)
//
// When a concrete value is converted to a protocol value inside a function
// (either implicitly via append or explicitly) and stored in a List, the
// protocol data pointer targets the function's stack frame instead of a
// heap-allocated copy.
//
// After the function returns, the first dispatch may succeed (stack not yet
// overwritten), but subsequent dispatches crash because the stack memory has
// been reused by other calls.
//
// STATUS: open — issue-0007 fix only covers some cases
// `List(Protocol)` appended from inside a helper function, dispatched
// repeatedly from `main` after the helper returns. Exercises the heap-copy
// path for both implicit-erasure-on-append and pre-erased protocol values.
#import "modules/std.sx";

View File

@@ -1,4 +1,6 @@
// Minimal: protocol dispatch on List(Protocol) items from a function
// Protocol dispatch on `List(Protocol)` items where the list pointer is
// passed into another function — verifies the boxed payload survives an
// extra call frame between erasure and dispatch.
#import "modules/std.sx";

View File

@@ -0,0 +1,18 @@
// `push <Context>` where Context's first field is an `#inline` protocol
// (`allocator: Allocator`) and the value being pushed is an `Arena` upcast to
// that protocol. Exercises save/restore of the boxed context across the push.
#import "modules/std.sx";
#import "modules/allocators.sx";
main :: () -> void {
arena : Arena = ---;
arena.create(context.allocator, 4096);
new_ctx := Context.{ allocator = xx @arena, data = context.data };
push new_ctx {
ptr := context.allocator.alloc(128);
out("inside push\n");
}
out("after push\n");
}

View File

@@ -1,11 +1,5 @@
// issue-0010: Closure returning a protocol value generates invalid LLVM IR
//
// LLVM verification failed: Called function must be a pointer!
// %icall = call addrspace(64) i64 %load56()
//
// A Closure() -> MyProtocol where MyProtocol is a protocol (not #inline)
// fails at codegen. Calling the function directly works fine; only the
// closure dispatch path is broken.
// Closure whose return type is a (non-`#inline`) protocol value — exercises
// the indirect-call path where the result is a boxed protocol.
#import "modules/std.sx";

View File

@@ -1,14 +1,6 @@
// issue-0011: Assigning to List(T).items corrupts adjacent memory when T is large
//
// Writing `list.items = xx 0` overwrites memory beyond the 8-byte items pointer
// when the List's element type T is larger than 32 bytes. The corruption spills
// into the struct field that follows the List in memory.
//
// Works correctly when size_of(T) <= 32.
// Fails when size_of(T) > 32 (e.g., 40 bytes).
//
// Likely cause: codegen confuses size_of(T) with size_of([*]T) when generating
// the store instruction for the items field.
// Assigning to `list.items` writes exactly the items-pointer field even when
// the element type `T` is larger than the pointer (40-byte BigNode here), so
// a sibling field of the enclosing struct is not corrupted.
#import "modules/std.sx";

View File

@@ -1,9 +1,5 @@
// issue-0013: += on global variables reads initial value instead of current value
//
// `g_counter += 1` compiles as `store(initial_value + 1)` instead of
// `store(load(g_counter) + 1)`. So it always produces the same result.
//
// Workaround: use `g_counter = g_counter + 1` instead of `g_counter += 1`
// `+=` on a global variable loads the current value (not the initializer)
// before storing — same semantics as the explicit `g = g + 1` form.
#import "modules/std.sx";

View File

@@ -1,10 +1,5 @@
// issue-0015: Global array variables with initializers contain all zeros at runtime.
//
// Expected: VALS[0]=-2, VALS[1]=-1, VALS[2]=42
// Actual: VALS[0]=0, VALS[1]=0, VALS[2]=0
//
// Global arrays declared with `: [N]T = .[...]` syntax get zero-initialized
// instead of receiving their specified values.
// Global array declared with `: [N]T = .[...]` keeps its initializer values
// (signed-int element type covers negative-literal handling too).
#import "modules/std.sx";

View File

@@ -1,16 +1,6 @@
// issue-0018: Dot-shorthand `.{...}` for struct with protocol field causes
// LLVM verification error when used in List(T).append from 2+ different
// struct methods.
//
// Trigger: TWO or more structs each with `List(Container)` calling
// `.append(.{ child = d })` — using dot-shorthand.
//
// Works: Only 1 struct doing it, or using explicit `Container.{ child = d }`.
// Fails: 2+ structs → `Invalid InsertValueInst operands!`
// `%si = insertvalue i64 undef, { ptr, ptr } %load, 0`
//
// Likely a monomorphization issue in `List(T).append` when the dot-shorthand
// type inference is resolved from multiple call sites.
// Dot-shorthand `.{ child = d }` for a struct whose first field is a protocol
// value, used as the argument to `List(Container).append` from two distinct
// container types. Exercises the cross-callsite path of dot-shorthand inference.
#import "modules/std.sx";

View File

@@ -0,0 +1,26 @@
// Global struct initialized with `.{}` (or a partial struct literal) honors
// the struct's per-field defaults, matching the function-local behavior.
#import "modules/std.sx";
Foo :: struct {
running: bool = true;
x: s32 = 42;
name: string = "default";
}
g_empty : Foo = .{};
g_partial : Foo = .{ x = 99 };
g_override : Foo = .{ running = false };
g_reorder : Foo = .{ x = 7, running = false, name = "hi" };
g_positional : Foo = .{ false, 13, "pos" };
main :: () -> void {
l_empty : Foo = .{};
print("local running={} x={} name={}\n", l_empty.running, l_empty.x, l_empty.name);
print("g_empty running={} x={} name={}\n", g_empty.running, g_empty.x, g_empty.name);
print("g_partial running={} x={} name={}\n", g_partial.running, g_partial.x, g_partial.name);
print("g_override running={} x={} name={}\n", g_override.running, g_override.x, g_override.name);
print("g_reorder running={} x={} name={}\n", g_reorder.running, g_reorder.x, g_reorder.name);
print("g_positional running={} x={} name={}\n", g_positional.running, g_positional.x, g_positional.name);
}

View File

@@ -0,0 +1,42 @@
// `xx` cast inside an RHS expression assigned to a struct field takes its
// target type from the field, not from the enclosing function's return type.
// Covers if-then-else RHS and binary-op RHS variants.
#import "modules/std.sx";
Foo :: struct {
pixel_w: s32;
dpi: f32;
last_perf: s64;
delta_time: f32;
}
FC :: struct { a: f32; b: f32; c: s32; d: s32; e: f32; f: f32; }
// If-then-else RHS in a function whose return type is not f32.
calc_bool :: (self: *Foo, wf: f32) -> bool {
self.dpi = if wf > 0.0 then xx self.pixel_w / wf else 1.0;
true;
}
// Binary-op RHS in a struct-returning function. The xx casts must target f32,
// not the FC return-struct shape.
begin :: (self: *Foo, current: s64, freq: s64) -> FC {
if self.last_perf > 0 {
self.delta_time = xx (current - self.last_perf) / xx freq;
}
FC.{ a = 1.0, b = 2.0, c = 3, d = 4, e = 5.0, f = 6.0 };
}
main :: () -> void {
f : *Foo = xx malloc(size_of(Foo));
f.pixel_w = 2880;
f.dpi = 0.0;
f.last_perf = 1000;
f.delta_time = 0.0;
_ := calc_bool(f, 1440.0);
print("dpi={}\n", f.dpi);
fc := begin(f, 1500, 1000);
print("delta={} fc.a={}\n", f.delta_time, fc.a);
}

View File

@@ -0,0 +1,16 @@
// `inline if COND { return E; }` followed by a fall-through final expression:
// when COND evaluates true at comptime, the body is spliced unconditionally
// and the trailing expression must NOT also be emitted into the same LLVM
// block (only one terminator per block).
#import "modules/std.sx";
#import "modules/compiler.sx";
do_it :: () -> bool {
inline if OS != .ios { return false; }
true;
}
main :: () -> s32 {
if do_it() then 0 else 1;
}

View File

@@ -0,0 +1,16 @@
// Top-level type alias `Handle :: u32;` resolves to its target type in every
// position — function signatures, type annotations on globals, and initializer
// literal coercion.
#import "modules/std.sx";
Handle :: u32;
ok :: () -> Handle { 0; }
g : Handle = 0;
main :: () -> s32 {
g = ok();
if g == 0 then 0 else 1;
}

View File

@@ -1,21 +0,0 @@
// issue-0006: literal `0` in integer comparison inferred as float inside f32 ternary
//
// When `s64 != 0` is used as the condition of a ternary whose result type is f32,
// the literal `0` in the comparison leaks the ternary's f32 type instead of matching
// the LHS s64 type. This generates invalid LLVM IR:
// %icmp = icmp ne i64 %load, float 0.000000e+00
//
// The same comparison works fine in a regular if-statement.
#import "modules/std.sx";
main :: () -> void {
x : s64 = 42;
// OK: comparison in statement context
if x != 0 { out("ok\n"); }
// BUG: comparison as condition of f32 ternary — `0` inferred as f32
result : f32 = if x != 0 then 1.0 else 2.0;
print("result = {}\n", result);
}

View File

@@ -1,23 +0,0 @@
// issue-0009: `push` with Context containing inline protocol triggers LLVM verification error
//
// LLVM verification failed: Invalid InsertValueInst operands!
// %si24 = insertvalue { ptr, ptr, ptr } undef, { ptr, ptr, ptr } %si21, 0
//
// Context contains `allocator: Allocator` where `Allocator :: protocol #inline`.
// push saves/restores context as a value, but LLVM lowering mishandles the struct
// when the first field is an inline protocol cast from a different impl (Arena).
#import "modules/std.sx";
#import "modules/allocators.sx";
main :: () -> void {
arena : Arena = ---;
arena.create(context.allocator, 4096);
new_ctx := Context.{ allocator = xx @arena, data = context.data };
push new_ctx {
ptr := context.allocator.alloc(128);
out("inside push\n");
}
out("after push\n");
}

View File

@@ -1,42 +0,0 @@
// issue-0019: #import c symbols are globally visible instead of scoped to the importing file
//
// When a file uses `#import c { #include "foo.h"; #source "foo.c"; }`,
// the C symbols become available to ALL files in the compilation unit,
// not just the file that imported them.
//
// This means a file can call C functions without importing the module
// that declares them, as long as some other file in the project does.
//
// Expected: C symbols from `#import c` should only be visible in files
// that directly (or transitively via SX #import) import the module.
//
// Repro:
// - a.sx: `#import c { #include "some_lib.h"; #source "some_lib.c"; };`
// - b.sx: does NOT import a.sx, but calls some_lib_function() — compiles successfully
//
// In the game project:
// - main.sx imports modules/stb_truetype.sx (which has #import c for kbts/stbtt)
// - ui/glyph_cache.sx does NOT import modules/stb_truetype.sx
// - ui/glyph_cache.sx calls kbts_ShapeRun, stbtt_InitFont, etc. — compiles fine
// - If main.sx removed the import, glyph_cache.sx would break
// Minimal repro structure (two files):
// --- module_with_c.sx ---
// #import c {
// #include "vendors/some_lib.h";
// #source "vendors/some_lib.c";
// };
//
// uses_c :: () -> s32 {
// some_lib_function();
// }
// --- main.sx (this file) ---
// #import "module_with_c.sx";
//
// main :: () -> s32 {
// // This should fail because we never imported the C module directly,
// // but currently it compiles:
// some_lib_function();
// }

View File

@@ -1,54 +0,0 @@
// 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");
}

View File

@@ -1,81 +0,0 @@
// 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");
}