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

View File

@@ -0,0 +1,42 @@
#import "modules/std.sx";
#import "modules/gpu/types.sx";
// GPU is the rendering-API abstraction. Concrete backends live as siblings
// of this file: `metal.sx` (iOS, eventually macOS), `vulkan.sx` (Linux/
// Android, plus macOS via MoltenVK), `webgpu.sx` (wasm). The SDL-backed
// GL renderer used by the desktop+wasm path stays as-is until those
// backends land.
GPU :: protocol {
// Bind the GPU to a backend-specific render target (e.g. a
// CAMetalLayer on iOS). pixel_w/pixel_h are the drawable's pixel
// dimensions; call resize when they change.
init :: (target: *void, pixel_w: s32, pixel_h: s32) -> bool;
shutdown :: ();
resize :: (pixel_w: s32, pixel_h: s32);
begin_frame :: (clear: ClearColor) -> bool;
// target_time is the host clock time at which the drawable should be
// presented (units match the platform's CADisplayLink.targetTimestamp
// on Apple). Metal forwards it to presentDrawable:atTime: to cap the
// pipeline at one frame so the inset slide lands on the same vsync as
// UIKit's keyboard view. GL backends ignore it.
end_frame :: (target_time: f64);
create_shader :: (vsrc: string, fsrc: string) -> ShaderHandle;
create_buffer :: (size_bytes: s64) -> BufferHandle;
update_buffer :: (buf: BufferHandle, data: *void, size_bytes: s64);
create_texture :: (w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle;
update_texture_region :: (tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void);
set_shader :: (sh: ShaderHandle);
set_vertex_buffer :: (buf: BufferHandle);
set_texture :: (slot: u32, tex: TextureHandle);
set_vertex_constants :: (slot: u32, data: *void, size_bytes: s64);
set_scissor :: (x: s32, y: s32, w: s32, h: s32);
disable_scissor :: ();
draw_triangles :: (vertex_offset: s32, vertex_count: s32);
}

View File

@@ -0,0 +1,571 @@
// Metal backend for the GPU protocol. iOS-only for now; macOS later.
//
// Linking is per-target via the consumer's build.sx:
// opts.add_framework("Metal")
// opts.add_framework("QuartzCore") // CAMetalLayer lives here
// `#framework "Metal"` below adds it to iOS-target link lines automatically;
// non-iOS targets don't reach the Metal-touching code paths.
#import "modules/std.sx";
#import "modules/std/objc.sx";
#import "modules/compiler.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
#framework "Metal";
// MTLCreateSystemDefaultDevice lives in the Metal framework as a plain C
// function. Returns id<MTLDevice> retained +1; we leak it for now since
// the device lives for the whole process.
MTLCreateSystemDefaultDevice :: () -> *void #foreign;
// Pixel formats.
MTL_PIXEL_FORMAT_BGRA8_UNORM :u64: 80;
MTL_PIXEL_FORMAT_RGBA8_UNORM :u64: 70;
MTL_PIXEL_FORMAT_R8_UNORM :u64: 10;
// MTLLoadAction / MTLStoreAction.
MTL_LOAD_ACTION_CLEAR :u64: 2;
MTL_STORE_ACTION_STORE :u64: 1;
// MTLPrimitiveType.
MTL_PRIMITIVE_TYPE_TRIANGLE :u64: 3;
// MTLBlendFactor — the subset used for normal alpha blending.
MTL_BLEND_FACTOR_SRC_ALPHA :u64: 4;
MTL_BLEND_FACTOR_ONE_MINUS_SRC_A :u64: 5;
// CGSize is a 2-element f64 HFA. arm64 Apple ABI puts it in d0,d1 — direct
// fn-pointer cast on objc_msgSend with a CGSize arg does the right thing.
CGSize :: struct { width: f64; height: f64; }
// MTLClearColor is a 4-element f64 HFA. Same ABI story — passes in d0..d3.
MTLClearColor :: struct {
red: f64;
green: f64;
blue: f64;
alpha: f64;
}
// MTLOrigin / MTLSize / MTLRegion / MTLScissorRect — integer aggregates.
// MTLRegion is 48 bytes and MTLScissorRect is 32 bytes; arm64 Apple ABI
// passes >16-byte composites by reference (address in the next register).
// We declare the call shapes with `*MTLRegion` etc., construct a local on
// the stack, and pass `@local` — the machine state matches what the Obj-C
// method expects.
MTLOrigin :: struct { x: u64; y: u64; z: u64; }
MTLSize :: struct { width: u64; height: u64; depth: u64; }
MTLRegion :: struct { origin: MTLOrigin; size: MTLSize; }
MTLScissorRect :: struct { x: u64; y: u64; width: u64; height: u64; }
// Pixel sub-format storage for textures. Tracks the bytes-per-pixel for the
// upload path (replaceRegion needs bytesPerRow which is bpp × width).
TextureSlot :: struct {
tex: *void = null;
bytes_per_pixel: u32 = 0;
}
MetalGPU :: struct {
device: *void = null; // id<MTLDevice>
queue: *void = null; // id<MTLCommandQueue>
layer: *void = null; // CAMetalLayer*
pixel_w: s32 = 0;
pixel_h: s32 = 0;
// Per-frame transients. Live only between begin_frame and end_frame.
drawable: *void = null; // id<CAMetalDrawable>
cmd_buffer: *void = null; // id<MTLCommandBuffer>
encoder: *void = null; // id<MTLRenderCommandEncoder>
// Resource tables. Handles are 1-based indices (0 = invalid).
shaders: List(*void) = .{}; // MTLRenderPipelineState*
buffers: List(*void) = .{}; // MTLBuffer*
textures: List(TextureSlot) = .{};
}
impl GPU for MetalGPU {
init :: (self: *MetalGPU, target: *void, pixel_w: s32, pixel_h: s32) -> bool {
inline if OS != .ios { return false; }
self.layer = target;
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
metal_init_ios(self);
}
shutdown :: (self: *MetalGPU) {
// Metal objects clean up at process exit on iOS. A real shutdown
// would send `release` to queue + device.
}
resize :: (self: *MetalGPU, pixel_w: s32, pixel_h: s32) {
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
inline if OS == .ios {
metal_resize_ios(self);
}
}
begin_frame :: (self: *MetalGPU, clear: ClearColor) -> bool {
inline if OS != .ios { return false; }
metal_begin_frame_ios(self, clear);
}
end_frame :: (self: *MetalGPU, target_time: f64) {
inline if OS == .ios {
metal_end_frame_ios(self, target_time);
}
}
// ── Resources ────────────────────────────────────────────────────────
// Handle = 1-based index into the backing List (0 = invalid). The bulk
// of each method lives in an iOS-only helper for readability — the impl
// method just guards non-iOS and delegates.
create_shader :: (self: *MetalGPU, vsrc: string, fsrc: string) -> ShaderHandle {
inline if OS != .ios { return 0; }
metal_create_shader_ios(self, vsrc);
}
create_buffer :: (self: *MetalGPU, size_bytes: s64) -> BufferHandle {
inline if OS != .ios { return 0; }
metal_create_buffer_ios(self, size_bytes);
}
update_buffer :: (self: *MetalGPU, buf: BufferHandle, data: *void, size_bytes: s64) {
inline if OS == .ios {
metal_update_buffer_ios(self, buf, data, size_bytes);
}
}
create_texture :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle {
inline if OS != .ios { return 0; }
metal_create_texture_ios(self, w, h, format, pixels);
}
update_texture_region :: (self: *MetalGPU, tex: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
inline if OS == .ios {
metal_update_texture_region_ios(self, tex, x, y, w, h, pixels);
}
}
// ── Per-draw state ───────────────────────────────────────────────────
// All operate on `self.encoder`, which is live only between begin_frame
// and end_frame. Calling these outside that window is a silent no-op.
set_shader :: (self: *MetalGPU, sh: ShaderHandle) {
inline if OS == .ios {
metal_set_shader_ios(self, sh);
}
}
set_vertex_buffer :: (self: *MetalGPU, buf: BufferHandle) {
inline if OS == .ios {
metal_set_vertex_buffer_ios(self, buf);
}
}
set_texture :: (self: *MetalGPU, slot: u32, tex: TextureHandle) {
inline if OS == .ios {
metal_set_texture_ios(self, slot, tex);
}
}
set_vertex_constants :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) {
inline if OS == .ios {
metal_set_vertex_constants_ios(self, slot, data, size_bytes);
}
}
set_scissor :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) {
inline if OS == .ios {
metal_set_scissor_ios(self, x, y, w, h);
}
}
disable_scissor :: (self: *MetalGPU) {
inline if OS == .ios {
metal_disable_scissor_ios(self);
}
}
draw_triangles :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) {
inline if OS == .ios {
metal_draw_triangles_ios(self, vertex_offset, vertex_count);
}
}
}
// ───────────────────────────────────────────────────────────────────────────
// iOS-only helpers — only reachable from `inline if OS == .ios` call sites,
// so non-iOS builds never reference the unresolved Metal symbols below.
// ───────────────────────────────────────────────────────────────────────────
metal_init_ios :: (self: *MetalGPU) -> bool {
inline if OS != .ios { return false; }
if self.layer == null { return false; }
self.device = MTLCreateSystemDefaultDevice();
if self.device == null { return false; }
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void = xx objc_msgSend;
msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend;
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo(self.layer, sel_registerName("setDevice:".ptr), self.device);
msg_ou(self.layer, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM);
msg_ob(self.layer, sel_registerName("setFramebufferOnly:".ptr), 1);
size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h };
msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size);
self.queue = msg_o(self.device, sel_registerName("newCommandQueue".ptr));
if self.queue == null { return false; }
true;
}
metal_resize_ios :: (self: *MetalGPU) {
inline if OS != .ios { return; }
if self.layer == null { return; }
msg_osize : (*void, *void, CGSize) -> void = xx objc_msgSend;
size := CGSize.{ width = xx self.pixel_w, height = xx self.pixel_h };
msg_osize(self.layer, sel_registerName("setDrawableSize:".ptr), size);
}
metal_begin_frame_ios :: (self: *MetalGPU, clear: ClearColor) -> bool {
inline if OS != .ios { return false; }
if self.layer == null { return false; }
if self.queue == null { return false; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_oo_ret : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ouret : (*void, *void, u64) -> *void = xx objc_msgSend;
msg_oclear : (*void, *void, MTLClearColor) -> void = xx objc_msgSend;
// drawable = [layer nextDrawable]
self.drawable = msg_o(self.layer, sel_registerName("nextDrawable".ptr));
if self.drawable == null { return false; }
// tex = [drawable texture]
drawable_texture := msg_o(self.drawable, sel_registerName("texture".ptr));
// pass = [MTLRenderPassDescriptor renderPassDescriptor] (autoreleased)
MTLRenderPassDescriptor := objc_getClass("MTLRenderPassDescriptor".ptr);
pass := msg_o(MTLRenderPassDescriptor, sel_registerName("renderPassDescriptor".ptr));
// color0 = pass.colorAttachments[0]
attachments := msg_o(pass, sel_registerName("colorAttachments".ptr));
color0 := msg_ouret(attachments, sel_registerName("objectAtIndexedSubscript:".ptr), 0);
msg_oo(color0, sel_registerName("setTexture:".ptr), drawable_texture);
msg_ou(color0, sel_registerName("setLoadAction:".ptr), MTL_LOAD_ACTION_CLEAR);
msg_ou(color0, sel_registerName("setStoreAction:".ptr), MTL_STORE_ACTION_STORE);
mtl_clear := MTLClearColor.{
red = xx clear.r,
green = xx clear.g,
blue = xx clear.b,
alpha = xx clear.a,
};
msg_oclear(color0, sel_registerName("setClearColor:".ptr), mtl_clear);
// cmd = [queue commandBuffer] (autoreleased)
self.cmd_buffer = msg_o(self.queue, sel_registerName("commandBuffer".ptr));
if self.cmd_buffer == null { return false; }
// encoder = [cmd renderCommandEncoderWithDescriptor:pass] (autoreleased)
self.encoder = msg_oo_ret(self.cmd_buffer,
sel_registerName("renderCommandEncoderWithDescriptor:".ptr), pass);
if self.encoder == null { return false; }
true;
}
metal_end_frame_ios :: (self: *MetalGPU, target_time: f64) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if self.cmd_buffer == null { return; }
if self.drawable == null { return; }
msg_v : (*void, *void) -> void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_ood : (*void, *void, *void, f64) -> void = xx objc_msgSend;
msg_v(self.encoder, sel_registerName("endEncoding".ptr));
// target_time > 0 → presentDrawable:atTime: (lockstep path).
// target_time == 0 → fall back to presentDrawable: (immediate).
if target_time > 0.0 {
msg_ood(self.cmd_buffer, sel_registerName("presentDrawable:atTime:".ptr),
self.drawable, target_time);
} else {
msg_oo(self.cmd_buffer, sel_registerName("presentDrawable:".ptr), self.drawable);
}
msg_v(self.cmd_buffer, sel_registerName("commit".ptr));
self.encoder = null;
self.cmd_buffer = null;
self.drawable = null;
}
// ── Shader (MSL pipeline state) ──────────────────────────────────────────
// Compile the MSL source, look up the conventional entry points `vmain`
// (vertex) and `fmain` (fragment), and produce an `MTLRenderPipelineState`
// targeted at the layer's BGRA8 surface with standard alpha blending.
// The fsrc parameter is ignored — Metal's library is one MSL file with
// both functions; pass the combined source as vsrc.
metal_create_shader_ios :: (self: *MetalGPU, src: string) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_oo : (*void, *void, *void) -> void = xx objc_msgSend;
msg_oo_r : (*void, *void, *void) -> *void = xx objc_msgSend;
msg_ou : (*void, *void, u64) -> void = xx objc_msgSend;
msg_ouret: (*void, *void, u64) -> *void = xx objc_msgSend;
msg_ob : (*void, *void, u8) -> void = xx objc_msgSend;
// [device newLibraryWithSource:src options:nil error:&err]
msg_lib : (*void, *void, *void, *void, **void) -> *void = xx objc_msgSend;
src_ns := ns_string(src.ptr);
err : *void = null;
library := msg_lib(self.device,
sel_registerName("newLibraryWithSource:options:error:".ptr),
src_ns, xx 0, @err);
if library == null {
NSLog(ns_string("[metal] MSL compile failed\n".ptr));
return 0;
}
vfn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr),
ns_string("vmain".ptr));
ffn := msg_oo_r(library, sel_registerName("newFunctionWithName:".ptr),
ns_string("fmain".ptr));
if vfn == null { NSLog(ns_string("[metal] missing vmain in MSL\n".ptr)); return 0; }
if ffn == null { NSLog(ns_string("[metal] missing fmain in MSL\n".ptr)); return 0; }
MTLRenderPipelineDescriptor := objc_getClass("MTLRenderPipelineDescriptor".ptr);
desc := msg_o(MTLRenderPipelineDescriptor, sel_registerName("alloc".ptr));
desc = msg_o(desc, sel_registerName("init".ptr));
msg_oo(desc, sel_registerName("setVertexFunction:".ptr), vfn);
msg_oo(desc, sel_registerName("setFragmentFunction:".ptr), ffn);
// colorAttachments[0]: pixel format + alpha blending.
atts := msg_o(desc, sel_registerName("colorAttachments".ptr));
att0 := msg_ouret(atts, sel_registerName("objectAtIndexedSubscript:".ptr), 0);
msg_ou(att0, sel_registerName("setPixelFormat:".ptr), MTL_PIXEL_FORMAT_BGRA8_UNORM);
msg_ob(att0, sel_registerName("setBlendingEnabled:".ptr), 1);
msg_ou(att0, sel_registerName("setSourceRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA);
msg_ou(att0, sel_registerName("setDestinationRGBBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A);
msg_ou(att0, sel_registerName("setSourceAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_SRC_ALPHA);
msg_ou(att0, sel_registerName("setDestinationAlphaBlendFactor:".ptr), MTL_BLEND_FACTOR_ONE_MINUS_SRC_A);
msg_pipe : (*void, *void, *void, **void) -> *void = xx objc_msgSend;
err2 : *void = null;
state := msg_pipe(self.device,
sel_registerName("newRenderPipelineStateWithDescriptor:error:".ptr),
desc, @err2);
if state == null {
NSLog(ns_string("[metal] pipeline state creation failed\n".ptr));
return 0;
}
self.shaders.append(state);
xx self.shaders.len;
}
// ── Buffers ──────────────────────────────────────────────────────────────
// Shared-memory MTLBuffer (CPU + GPU visible on UMA hardware). `contents`
// returns the mapped pointer for memcpy uploads.
metal_create_buffer_ios :: (self: *MetalGPU, size_bytes: s64) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
if size_bytes <= 0 { return 0; }
// MTLResourceStorageModeShared is the default (option value 0).
msg_buf : (*void, *void, u64, u64) -> *void = xx objc_msgSend;
buf := msg_buf(self.device,
sel_registerName("newBufferWithLength:options:".ptr),
xx size_bytes, 0);
if buf == null { return 0; }
self.buffers.append(buf);
xx self.buffers.len;
}
metal_update_buffer_ios :: (self: *MetalGPU, handle: u32, data: *void, size_bytes: s64) {
inline if OS != .ios { return; }
buf := metal_lookup_buffer(self, handle);
if buf == null { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
msg_o : (*void, *void) -> *void = xx objc_msgSend;
dst := msg_o(buf, sel_registerName("contents".ptr));
if dst == null { return; }
memcpy(dst, data, size_bytes);
}
metal_lookup_buffer :: (self: *MetalGPU, handle: u32) -> *void {
inline if OS != .ios { return null; }
if handle == 0 { return null; }
h64 : s64 = xx handle;
if h64 > self.buffers.len { return null; }
self.buffers.items[handle - 1];
}
metal_lookup_shader :: (self: *MetalGPU, handle: u32) -> *void {
inline if OS != .ios { return null; }
if handle == 0 { return null; }
h64 : s64 = xx handle;
if h64 > self.shaders.len { return null; }
self.shaders.items[handle - 1];
}
// ── Textures ─────────────────────────────────────────────────────────────
metal_create_texture_ios :: (self: *MetalGPU, w: s32, h: s32, format: TextureFormat, pixels: *void) -> u32 {
inline if OS != .ios { return 0; }
if self.device == null { return 0; }
if w <= 0 { return 0; }
if h <= 0 { return 0; }
pixel_format : u64 = 0;
bytes_per_pixel : u32 = 0;
if format == .rgba8 {
pixel_format = MTL_PIXEL_FORMAT_RGBA8_UNORM;
bytes_per_pixel = 4;
} else {
pixel_format = MTL_PIXEL_FORMAT_R8_UNORM;
bytes_per_pixel = 1;
}
// [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:width:height:mipmapped:]
MTLTextureDescriptor := objc_getClass("MTLTextureDescriptor".ptr);
msg_desc : (*void, *void, u64, u64, u64, u8) -> *void = xx objc_msgSend;
desc := msg_desc(MTLTextureDescriptor,
sel_registerName("texture2DDescriptorWithPixelFormat:width:height:mipmapped:".ptr),
pixel_format, xx w, xx h, 0);
if desc == null { return 0; }
msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend;
tex := msg_oo(self.device, sel_registerName("newTextureWithDescriptor:".ptr), desc);
if tex == null { return 0; }
slot : TextureSlot = .{ tex = tex, bytes_per_pixel = bytes_per_pixel };
self.textures.append(slot);
if pixels != null {
handle : u32 = xx self.textures.len;
metal_update_texture_region_ios(self, handle, 0, 0, w, h, pixels);
}
xx self.textures.len;
}
metal_update_texture_region_ios :: (self: *MetalGPU, handle: u32, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
inline if OS != .ios { return; }
if handle == 0 { return; }
h64 : s64 = xx handle;
if h64 > self.textures.len { return; }
slot := self.textures.items[handle - 1];
if slot.tex == null { return; }
if pixels == null { return; }
if w <= 0 { return; }
if h <= 0 { return; }
region : MTLRegion = .{
origin = .{ x = xx x, y = xx y, z = 0 },
size = .{ width = xx w, height = xx h, depth = 1 },
};
bytes_per_row : u64 = xx (slot.bytes_per_pixel * cast(u32) w);
// [tex replaceRegion:region mipmapLevel:0 withBytes:pixels bytesPerRow:bytes_per_row]
msg_replace : (*void, *void, *MTLRegion, u64, *void, u64) -> void = xx objc_msgSend;
msg_replace(slot.tex,
sel_registerName("replaceRegion:mipmapLevel:withBytes:bytesPerRow:".ptr),
@region, 0, pixels, bytes_per_row);
}
// ── Per-draw state ───────────────────────────────────────────────────────
metal_set_shader_ios :: (self: *MetalGPU, sh: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
state := metal_lookup_shader(self, sh);
if state == null { return; }
msg : (*void, *void, *void) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setRenderPipelineState:".ptr), state);
}
metal_set_vertex_buffer_ios :: (self: *MetalGPU, h: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
buf := metal_lookup_buffer(self, h);
if buf == null { return; }
// [encoder setVertexBuffer:buf offset:0 atIndex:0]
msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBuffer:offset:atIndex:".ptr), buf, 0, 0);
}
metal_set_texture_ios :: (self: *MetalGPU, slot: u32, h: u32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if h == 0 { return; }
h64 : s64 = xx h;
if h64 > self.textures.len { return; }
tex := self.textures.items[h - 1].tex;
if tex == null { return; }
// [encoder setFragmentTexture:tex atIndex:slot]
msg : (*void, *void, *void, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setFragmentTexture:atIndex:".ptr), tex, xx slot);
}
metal_set_vertex_constants_ios :: (self: *MetalGPU, slot: u32, data: *void, size_bytes: s64) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
// [encoder setVertexBytes:data length:size_bytes atIndex:slot]
msg : (*void, *void, *void, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setVertexBytes:length:atIndex:".ptr),
data, xx size_bytes, xx slot);
}
metal_set_scissor_ios :: (self: *MetalGPU, x: s32, y: s32, w: s32, h: s32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
rect : MTLScissorRect = .{ x = xx x, y = xx y, width = xx w, height = xx h };
// [encoder setScissorRect:rect] (MTLScissorRect is 32 bytes → indirect)
msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect);
}
metal_disable_scissor_ios :: (self: *MetalGPU) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
// Metal has no "disable scissor" — set the rect to cover the full
// drawable so subsequent draws aren't clipped.
rect : MTLScissorRect = .{ x = 0, y = 0, width = xx self.pixel_w, height = xx self.pixel_h };
msg : (*void, *void, *MTLScissorRect) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("setScissorRect:".ptr), @rect);
}
metal_draw_triangles_ios :: (self: *MetalGPU, vertex_offset: s32, vertex_count: s32) {
inline if OS != .ios { return; }
if self.encoder == null { return; }
if vertex_count <= 0 { return; }
// [encoder drawPrimitives:.triangle vertexStart:offset vertexCount:count]
msg : (*void, *void, u64, u64, u64) -> void = xx objc_msgSend;
msg(self.encoder, sel_registerName("drawPrimitives:vertexStart:vertexCount:".ptr),
MTL_PRIMITIVE_TYPE_TRIANGLE, xx vertex_offset, xx vertex_count);
}

View File

@@ -0,0 +1,24 @@
#import "modules/std.sx";
// Opaque GPU resource handles. Backends decide what the integer means
// (GL: object name; Metal: 1-based index into a backend-owned table of
// retained MTL* objects). Zero is reserved for "no handle".
ShaderHandle :: u32;
BufferHandle :: u32;
TextureHandle :: u32;
ClearColor :: struct {
r: f32;
g: f32;
b: f32;
a: f32;
black :: () -> ClearColor => .{ r = 0.0, g = 0.0, b = 0.0, a = 1.0 };
}
// Texture pixel format. The UI renderer only needs rgba8 (images, the
// 1×1 white solid-rect texture) and r8 (font glyph atlas alpha).
TextureFormat :: enum {
rgba8;
r8;
}

View File

@@ -55,16 +55,26 @@ GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5;
g_uikit_plat : *UIKitPlatform = null;
// Which GPU API the UIKit backend wires up. `.gles` keeps the existing
// CAEAGLLayer + EAGLContext + renderbuffer path; `.metal` swaps the view
// for a CAMetalLayer-backed one and leaves rendering to MetalGPU. Set
// before calling `init`; default `.gles` preserves prior behavior.
GpuMode :: enum {
gles;
metal;
}
UIKitPlatform :: struct {
window: *void = null; // UIWindow*
root_vc: *void = null; // UIViewController*
gl_view: *void = null; // SxGLView*
gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer)
gl_ctx: *void = null; // EAGLContext*
gl_view: *void = null; // SxGLView* OR SxMetalView* (depending on gpu_mode)
gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer)
gl_ctx: *void = null; // EAGLContext* (null in metal mode)
display_link: *void = null;
color_renderbuffer: u32 = 0;
framebuffer: u32 = 0;
gl_initialized: bool = false;
gpu_mode: GpuMode = .gles;
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
text_field: *void = null;
@@ -121,7 +131,13 @@ impl Platform for UIKitPlatform {
inline if OS == .ios {
uikit_chdir_to_bundle();
uikit_register_classes();
uikit_create_gl_context(self);
if self.gpu_mode == .gles {
uikit_create_gl_context(self);
} else {
// Metal mode: skip EAGL. dpi_scale still needs to be known
// before the window exists so callers can size resources.
uikit_read_screen_scale(self);
}
}
true;
}
@@ -159,7 +175,10 @@ impl Platform for UIKitPlatform {
end_frame :: (self: *UIKitPlatform) {
inline if OS == .ios {
uikit_present_renderbuffer(self);
if self.gpu_mode == .gles {
uikit_present_renderbuffer(self);
}
// Metal mode: caller's gpu.end_frame() handles present.
}
}
@@ -274,9 +293,25 @@ uikit_register_classes :: () {
objc_registerClassPair(SxAppDelegate);
uikit_register_gl_view_class();
uikit_register_metal_view_class();
}
}
// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the
// metal-mode init path which doesn't go through uikit_create_gl_context
// (that's where the gles path picks the scale up).
uikit_read_screen_scale :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
UIScreen := objc_getClass("UIScreen".ptr);
sel_main_screen := sel_registerName("mainScreen".ptr);
sel_native_scale := sel_registerName("nativeScale".ptr);
msg_o : (*void, *void) -> *void = xx objc_msgSend;
msg_d : (*void, *void) -> f64 = xx objc_msgSend;
screen := msg_o(UIScreen, sel_main_screen);
scale_d : f64 = msg_d(screen, sel_native_scale);
plat.dpi_scale = xx scale_d;
}
// NSNotification callback. The notification's userInfo dict has the
// keyboard's end-frame and animation curve/duration.
// UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords)
@@ -390,6 +425,7 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
UIWindow := objc_getClass("UIWindow".ptr);
UIViewController := objc_getClass("UIViewController".ptr);
SxGLView := objc_getClass("SxGLView".ptr);
SxMetalView := objc_getClass("SxMetalView".ptr);
EAGLContext := objc_getClass("EAGLContext".ptr);
CADisplayLink := objc_getClass("CADisplayLink".ptr);
NSRunLoop := objc_getClass("NSRunLoop".ptr);
@@ -445,11 +481,12 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
vc_raw := msg_o(UIViewController, sel_alloc);
plat.root_vc = msg_o(vc_raw, sel_init);
// Allocate SxGLView and install it as the VC's view, so the standard
// ViewController layout pipeline sizes the GL view to the window. Setting
// it BEFORE setRootViewController avoids the VC lazy-loading a default
// view first.
glv_raw := msg_o(SxGLView, sel_alloc);
// Allocate either SxGLView or SxMetalView based on gpu_mode and install
// it as the VC's view. The view's +layerClass override gives us the
// right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE
// setRootViewController avoids the VC lazy-loading a default view.
view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView;
glv_raw := msg_o(view_class, sel_alloc);
plat.gl_view = msg_o(glv_raw, sel_init);
sel_set_view := sel_registerName("setView:".ptr);
msg_oo(plat.root_vc, sel_set_view, plat.gl_view);
@@ -458,35 +495,40 @@ uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
plat.gl_layer = msg_o(plat.gl_view, sel_layer);
// Mark the layer opaque (no compositor blend) + set the drawable properties
// required by EAGLContext.renderbufferStorage:fromDrawable: (color format,
// non-retained backing). Without this dict the renderbuffer allocation
// silently fails and the framebuffer reports INCOMPLETE.
// Mark the layer opaque (no compositor blend). Required for EAGL +
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
// hurt to be explicit).
sel_set_opaque := sel_registerName("setOpaque:".ptr);
msg_obool : (*void, *void, u8) -> void = xx objc_msgSend;
msg_obool(plat.gl_layer, sel_set_opaque, 1);
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
NSNumber := objc_getClass("NSNumber".ptr);
sel_dictionary := sel_registerName("dictionary".ptr);
sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr);
sel_number_bool := sel_registerName("numberWithBool:".ptr);
sel_set_drawable := sel_registerName("setDrawableProperties:".ptr);
if plat.gpu_mode == .gles {
// EAGL drawable properties dict required by
// EAGLContext.renderbufferStorage:fromDrawable: (color format,
// non-retained backing). Without this dict the renderbuffer
// allocation silently fails and the framebuffer reports INCOMPLETE.
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
NSNumber := objc_getClass("NSNumber".ptr);
sel_dictionary := sel_registerName("dictionary".ptr);
sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr);
sel_number_bool := sel_registerName("numberWithBool:".ptr);
sel_set_drawable := sel_registerName("setDrawableProperties:".ptr);
msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend;
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend;
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
// The EAGL dict keys/values must be the framework-provided NSString
// constants (pointer identity is checked) — dlsym them from OpenGLES.
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
// The EAGL dict keys/values must be the framework-provided NSString
// constants (pointer identity is checked) — dlsym them from OpenGLES.
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
dict := msg_o(NSMutableDictionary, sel_dictionary);
msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend;
msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key);
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
msg_oo(plat.gl_layer, sel_set_drawable, dict);
dict := msg_o(NSMutableDictionary, sel_dictionary);
msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend;
msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key);
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
msg_oo(plat.gl_layer, sel_set_drawable, dict);
}
// EAGLContext + load_gl were already done in uikit_create_gl_context()
// back when the game's main called plat.init() — so shaders/textures
@@ -615,10 +657,38 @@ uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) {
if g_uikit_plat == null { return; }
plat := g_uikit_plat;
if plat.gl_initialized { return; }
uikit_setup_renderbuffer(plat);
if plat.gpu_mode == .gles {
uikit_setup_renderbuffer(plat);
} else {
uikit_compute_layer_pixel_size(plat);
}
plat.gl_initialized = true;
}
// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the
// drawable is in pixels". Reads the layer's bounds in points and scales to
// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init
// based on these dims.
uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
if plat.gl_view == null { return; }
sel_bounds := sel_registerName("bounds".ptr);
msg_rect : (*void, *void) -> CGRect = xx objc_msgSend;
b := msg_rect(plat.gl_view, sel_bounds);
w_pts : f64 = b.width;
h_pts : f64 = b.height;
plat.viewport_w = xx w_pts;
plat.viewport_h = xx h_pts;
scale64 : f64 = xx plat.dpi_scale;
pw : f64 = w_pts * scale64;
ph : f64 = h_pts * scale64;
plat.pixel_w = xx pw;
plat.pixel_h = xx ph;
}
// Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an
// NSSet<UITouch *> + UIEvent. We take the first touch (single-touch model
// matching the chess game's drag-and-tap UX) and push the resulting
@@ -698,3 +768,45 @@ uikit_register_gl_view_class :: () {
objc_registerClassPair(SxGLView);
}
}
// +layerClass IMP for SxMetalView. Class method, signature "#@:".
uikit_metal_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) {
objc_getClass("CAMetalLayer".ptr);
}
// SxMetalView reuses the same tick/layout/touch IMPs as SxGLView. The IMPs
// already branch on `plat.gpu_mode` for the GL-specific bits (renderbuffer
// setup, etc.), so a single set of IMPs serves both view classes.
uikit_register_metal_view_class :: () {
inline if OS == .ios {
UIView := objc_getClass("UIView".ptr);
SxMetalView := objc_allocateClassPair(UIView, "SxMetalView".ptr, 0);
metaclass := object_getClass(SxMetalView);
class_addMethod(metaclass,
sel_registerName("layerClass".ptr),
xx uikit_metal_view_layer_class, "#@:".ptr);
class_addMethod(SxMetalView,
sel_registerName("sxTick:".ptr),
xx uikit_gl_view_tick, "v@:@".ptr);
class_addMethod(SxMetalView,
sel_registerName("layoutSubviews".ptr),
xx uikit_gl_view_layout, "v@:".ptr);
class_addMethod(SxMetalView,
sel_registerName("touchesBegan:withEvent:".ptr),
xx uikit_gl_view_touches_began, "v@:@@".ptr);
class_addMethod(SxMetalView,
sel_registerName("touchesMoved:withEvent:".ptr),
xx uikit_gl_view_touches_moved, "v@:@@".ptr);
class_addMethod(SxMetalView,
sel_registerName("touchesEnded:withEvent:".ptr),
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
class_addMethod(SxMetalView,
sel_registerName("touchesCancelled:withEvent:".ptr),
xx uikit_gl_view_touches_ended, "v@:@@".ptr);
objc_registerClassPair(SxMetalView);
}
}

View File

@@ -2848,19 +2848,29 @@ pub const LLVMEmitter = struct {
}
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
const elem_ty = c.LLVMGetElementType(llvm_ty);
const kind = c.LLVMGetTypeKind(llvm_ty);
const is_struct = kind == c.LLVMStructTypeKind;
const n: c_uint = @intCast(agg.len);
const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty);
defer self.alloc.free(vals);
for (agg, 0..) |cv, i| {
const elem_ty = if (is_struct)
c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i))
else
c.LLVMGetElementType(llvm_ty);
vals[i] = switch (cv) {
.int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1),
.float => |v| c.LLVMConstReal(elem_ty, v),
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
else => c.LLVMConstNull(elem_ty),
};
}
if (is_struct) {
return c.LLVMConstNamedStruct(llvm_ty, vals.ptr, n);
}
const elem_ty = c.LLVMGetElementType(llvm_ty);
return c.LLVMConstArray(elem_ty, vals.ptr, n);
}

View File

@@ -447,10 +447,9 @@ pub const Lowering = struct {
},
.var_decl => |vd| {
// Top-level mutable global (e.g., `context : Context = ---;`)
const var_ty = if (vd.type_annotation) |ta|
type_bridge.resolveAstType(ta, &self.module.types)
else
.s64;
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
// to their target type (not a synthetic empty struct).
const var_ty = self.resolveType(vd.type_annotation);
const name_id = self.module.types.internString(vd.name);
const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) {
.undef_literal => .zeroinit,
@@ -459,6 +458,7 @@ pub const Lowering = struct {
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.array_literal => |al| self.constArrayLiteral(al.elements),
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty),
else => null,
} else null;
const gid = self.module.addGlobal(.{
@@ -479,22 +479,67 @@ pub const Lowering = struct {
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
for (elements, 0..) |elem, i| {
vals[i] = switch (elem.data) {
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.unary_op => |uo| switch (uo.op) {
.negate => switch (uo.operand.data) {
.int_literal => |il| .{ .int = -il.value },
.float_literal => |fl| .{ .float = -fl.value },
else => return null,
},
else => return null,
vals[i] = self.constExprValue(elem) orelse return null;
}
return .{ .aggregate = vals };
}
/// Try to convert a single AST expression into a compile-time ConstantValue.
/// Returns null if the expression is not constant-foldable here.
fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue {
return switch (expr.data) {
.int_literal => |il| .{ .int = il.value },
.bool_literal => |bl| .{ .boolean = bl.value },
.float_literal => |fl| .{ .float = fl.value },
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
.undef_literal => .zeroinit,
.unary_op => |uo| switch (uo.op) {
.negate => switch (uo.operand.data) {
.int_literal => |il| .{ .int = -il.value },
.float_literal => |fl| .{ .float = -fl.value },
else => null,
},
.array_literal => |al| self.constArrayLiteral(al.elements) orelse return null,
else => return null,
else => null,
},
.array_literal => |al| self.constArrayLiteral(al.elements),
else => null,
};
}
/// Try to convert a struct literal into a compile-time ConstantValue.aggregate of the
/// struct's fields in declaration order, filling missing fields from the struct's
/// field defaults. Returns null if any value is not constant-foldable.
fn constStructLiteral(self: *Lowering, sl: *const ast.StructLiteral, ty: TypeId) ?inst_mod.ConstantValue {
if (ty.isBuiltin()) return null;
const ti = self.module.types.get(ty);
if (ti != .@"struct") return null;
const struct_fields = ti.@"struct".fields;
const struct_name = self.module.types.getString(ti.@"struct".name);
const field_defaults: []const ?*const Node = self.struct_defaults_map.get(struct_name) orelse &.{};
const has_names = sl.field_inits.len > 0 and sl.field_inits[0].name != null;
const vals = self.alloc.alloc(inst_mod.ConstantValue, struct_fields.len) catch return null;
for (struct_fields, 0..) |sf, fi| {
const sf_name = self.module.types.getString(sf.name);
const init_expr: ?*const Node = blk: {
if (has_names) {
for (sl.field_inits) |init_pair| {
if (init_pair.name) |n| {
if (std.mem.eql(u8, n, sf_name)) break :blk init_pair.value;
}
}
} else if (fi < sl.field_inits.len) {
break :blk sl.field_inits[fi].value;
}
if (fi < field_defaults.len) break :blk field_defaults[fi];
break :blk null;
};
if (init_expr) |e| {
vals[fi] = self.constExprValue(e) orelse return null;
} else {
vals[fi] = .zeroinit;
}
}
return .{ .aggregate = vals };
}
@@ -862,6 +907,12 @@ pub const Lowering = struct {
fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
if (node.data == .block) {
self.lowerBlock(node);
// A `return` inside the branch terminates the current LLVM block; propagate
// that up so the enclosing block lowering stops emitting fall-through.
if (self.currentBlockHasTerminator()) {
self.block_terminated = true;
return .none;
}
return self.builder.constInt(0, .void);
}
return self.lowerExpr(node);
@@ -1152,12 +1203,13 @@ pub const Lowering = struct {
const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object));
if (elem_ty != .void) self.target_type = elem_ty;
} else if (asgn.target.data == .field_access) {
// For obj.field = val, set target_type to the field's type
// Only for enum literals and struct literals — these need target_type to resolve.
// Avoid setting it for complex RHS expressions (calls, casts) because
// resolveCallParamTypes can't override target_type for method-call args.
// For obj.field = val, set target_type to the field's type so RHS
// sub-expressions (enum/struct literals, branch arms, xx casts) can
// resolve against it. Skipped for forms that would forward the type
// unchanged into method-call arg slots (`resolveCallParamTypes` can't
// override target_type per-arg).
const needs_target = switch (asgn.value.data) {
.enum_literal, .struct_literal => true,
.enum_literal, .struct_literal, .if_expr, .match_expr, .block, .unary_op, .binary_op => true,
.call => |vc| vc.callee.data == .enum_literal,
else => false,
};
@@ -1266,7 +1318,7 @@ pub const Lowering = struct {
for (fields, 0..) |f, i| {
if (f.name == field_name_id) {
const gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
const src_ty = self.inferExprType(asgn.value);
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, f.ty);
self.storeOrCompound(gep, coerced, asgn.op, f.ty);
return;
@@ -1280,7 +1332,7 @@ pub const Lowering = struct {
// GEP into union payload area, then into the struct field
const union_gep = self.builder.emit(.{ .union_gep = .{ .base = obj_ptr, .field_index = @intCast(i), .base_type = obj_ty } }, self.module.types.ptrTo(f.ty));
const field_gep = self.builder.structGepTyped(union_gep, @intCast(si), sf.ty, f.ty);
const src_ty = self.inferExprType(asgn.value);
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, sf.ty);
self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty);
return;
@@ -1308,8 +1360,9 @@ pub const Lowering = struct {
// target type, storing element-sized bytes instead of a pointer.
const gep_ty = self.module.types.ptrTo(field_ty);
const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty);
// Coerce value to field type
const src_ty = self.inferExprType(asgn.value);
// Coerce value to field type — use the lowered value's actual type
// (not inferExprType, which can re-read target_type after restore).
const src_ty = self.builder.getRefType(val);
const coerced = self.coerceToType(val, src_ty, field_ty);
self.storeOrCompound(gep, coerced, asgn.op, field_ty);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
lerp(0, 10, 0.5) = 5.000000
lerp(0, 10, 0.25) = 2.500000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
after set: 100.000000x50.000000
mid anim: 150.000000x75.000000
end anim: 200.000000x100.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
r1 = 150.000000
r2 = 150.000000
r3 = 200.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,10 @@
=== Direct calls ===
d1 = 150.000000
d2 = 150.000000
d3 = 200.000000
d4 = 110.000000
=== Protocol dispatch ===
r1 = 150.000000
r2 = 150.000000
r3 = 200.000000
r4 = 110.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
ok
result = 1.000000

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
inside add: 42
inside add: 99
items[0] = 42 (expected 42)
items[1] = 99 (expected 99)

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,7 @@
=== Created in main ===
first: 42 (expected 42)
second: 42 (expected 42)
=== Created in add() ===
first: 99 (expected 99)
second: 99 (expected 99)
=== OK ===

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,8 @@
=== Direct 1 ===
r1 = 42
=== Direct 2 ===
r2 = 42
=== From function ===
dispatch_fn: about to dispatch
dispatch_fn: result = 42
=== OK ===

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
inside push
after push

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
direct call works
closure call works

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,4 @@
size_of BigNode = 40
before: sentinel = 3735928559
after: sentinel = 3735928559
OK

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,10 @@
--- Test 1: += (broken) ---
Expected: 1, 2, 3
counter=1
counter=2
counter=3
--- Test 2: = x + 1 (works) ---
Expected: 2, 3, 4
counter=4
counter=5
counter=6

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
VALS: -2 -1 42 99
PASS

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,3 @@
StackA: draw=42
StackB: draw=25
OK

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,6 @@
local running=true x=42 name=default
g_empty running=true x=42 name=default
g_partial running=true x=99 name=default
g_override running=false x=42 name=default
g_reorder running=false x=7 name=hi
g_positional running=false x=13 name=pos

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
dpi=2.000000
delta=0.500000 fc.a=1.000000

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@