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:
120
examples/63-metal-clear.sx
Normal file
120
examples/63-metal-clear.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// issue-0002: impl for built-in types fails with "expected type name after 'for'"
|
// impl Protocol for built-in scalar types (f32, s64, bool, u32, ...) —
|
||||||
//
|
// both static dispatch (`f32.lerp(...)`) and protocol-boxed dispatch via
|
||||||
// `impl Protocol for f32` should work the same as `impl Protocol for MyStruct`.
|
// `#inline` erasure.
|
||||||
// Currently the parser rejects built-in type names (f32, s64, bool, etc.) after `for`.
|
|
||||||
|
|
||||||
Lerpable :: protocol #inline {
|
Lerpable :: protocol #inline {
|
||||||
lerp :: (b: Self, t: f32) -> Self;
|
lerp :: (b: Self, t: f32) -> Self;
|
||||||
@@ -1,12 +1,5 @@
|
|||||||
// issue-0003: Generic struct with protocol #inline constraint generates wrong LLVM types
|
// Generic struct `Animated($T: Lerpable)` monomorphized with a struct type — the
|
||||||
//
|
// `#inline` protocol constraint participates in method dispatch via `self.from.lerp(...)`.
|
||||||
// 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 }
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
#import "modules/math";
|
#import "modules/math";
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
// issue-0004: scalar-to-vector conversion when all optional struct fields are null
|
// Struct with multiple `?f32` fields, all set to `null` simultaneously, passed
|
||||||
//
|
// into a protocol-dispatched method. Exercises the all-null-payload path through
|
||||||
// When a struct has multiple ?f32 fields and ALL are set to null simultaneously,
|
// the boxed call.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
// issue-0005: optional f32 field in struct loses value when earlier optional is null
|
// Optional `?f32` fields in struct literals — exhaustively combine null/value
|
||||||
//
|
// for both fields, through both direct calls and protocol dispatch.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
16
examples/71-int-cmp-in-float-ternary.sx
Normal file
16
examples/71-int-cmp-in-float-ternary.sx
Normal 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);
|
||||||
|
}
|
||||||
@@ -1,13 +1,6 @@
|
|||||||
// issue-0007: protocol value stores dangling pointer to stack local
|
// 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
|
||||||
// When a concrete value is converted to a protocol value inside a function,
|
// heap-copied so dispatch survives the constructing function returning.
|
||||||
// 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)
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
// issue-0008: protocol value created in a function and appended to a list
|
// `List(Protocol)` appended from inside a helper function, dispatched
|
||||||
// still stores a dangling stack pointer (issue-0007 fix incomplete)
|
// repeatedly from `main` after the helper returns. Exercises the heap-copy
|
||||||
//
|
// path for both implicit-erasure-on-append and pre-erased protocol values.
|
||||||
// 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
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -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";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
18
examples/75-push-context-with-arena.sx
Normal file
18
examples/75-push-context-with-arena.sx
Normal 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");
|
||||||
|
}
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
// issue-0010: Closure returning a protocol value generates invalid LLVM IR
|
// Closure whose return type is a (non-`#inline`) protocol value — exercises
|
||||||
//
|
// the indirect-call path where the result is a boxed protocol.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
// issue-0011: Assigning to List(T).items corrupts adjacent memory when T is large
|
// 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
|
||||||
// Writing `list.items = xx 0` overwrites memory beyond the 8-byte items pointer
|
// a sibling field of the enclosing struct is not corrupted.
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
// issue-0013: += on global variables reads initial value instead of current value
|
// `+=` on a global variable loads the current value (not the initializer)
|
||||||
//
|
// before storing — same semantics as the explicit `g = g + 1` form.
|
||||||
// `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`
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
// issue-0015: Global array variables with initializers contain all zeros at runtime.
|
// Global array declared with `: [N]T = .[...]` keeps its initializer values
|
||||||
//
|
// (signed-int element type covers negative-literal handling too).
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
// issue-0018: Dot-shorthand `.{...}` for struct with protocol field causes
|
// Dot-shorthand `.{ child = d }` for a struct whose first field is a protocol
|
||||||
// LLVM verification error when used in List(T).append from 2+ different
|
// value, used as the argument to `List(Container).append` from two distinct
|
||||||
// struct methods.
|
// container types. Exercises the cross-callsite path of dot-shorthand inference.
|
||||||
//
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
26
examples/81-global-struct-defaults.sx
Normal file
26
examples/81-global-struct-defaults.sx
Normal 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);
|
||||||
|
}
|
||||||
42
examples/82-xx-target-in-field-assign.sx
Normal file
42
examples/82-xx-target-in-field-assign.sx
Normal 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);
|
||||||
|
}
|
||||||
16
examples/83-inline-if-return-fallthrough.sx
Normal file
16
examples/83-inline-if-return-fallthrough.sx
Normal 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;
|
||||||
|
}
|
||||||
16
examples/84-global-type-alias.sx
Normal file
16
examples/84-global-type-alias.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
// }
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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");
|
|
||||||
}
|
|
||||||
42
library/modules/gpu/api.sx
Normal file
42
library/modules/gpu/api.sx
Normal 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);
|
||||||
|
}
|
||||||
571
library/modules/gpu/metal.sx
Normal file
571
library/modules/gpu/metal.sx
Normal 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);
|
||||||
|
}
|
||||||
24
library/modules/gpu/types.sx
Normal file
24
library/modules/gpu/types.sx
Normal 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;
|
||||||
|
}
|
||||||
@@ -55,16 +55,26 @@ GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5;
|
|||||||
|
|
||||||
g_uikit_plat : *UIKitPlatform = null;
|
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 {
|
UIKitPlatform :: struct {
|
||||||
window: *void = null; // UIWindow*
|
window: *void = null; // UIWindow*
|
||||||
root_vc: *void = null; // UIViewController*
|
root_vc: *void = null; // UIViewController*
|
||||||
gl_view: *void = null; // SxGLView*
|
gl_view: *void = null; // SxGLView* OR SxMetalView* (depending on gpu_mode)
|
||||||
gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer)
|
gl_layer: *void = null; // CAEAGLLayer* OR CAMetalLayer* (= gl_view.layer)
|
||||||
gl_ctx: *void = null; // EAGLContext*
|
gl_ctx: *void = null; // EAGLContext* (null in metal mode)
|
||||||
display_link: *void = null;
|
display_link: *void = null;
|
||||||
color_renderbuffer: u32 = 0;
|
color_renderbuffer: u32 = 0;
|
||||||
framebuffer: u32 = 0;
|
framebuffer: u32 = 0;
|
||||||
gl_initialized: bool = false;
|
gl_initialized: bool = false;
|
||||||
|
gpu_mode: GpuMode = .gles;
|
||||||
|
|
||||||
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
|
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
|
||||||
text_field: *void = null;
|
text_field: *void = null;
|
||||||
@@ -121,7 +131,13 @@ impl Platform for UIKitPlatform {
|
|||||||
inline if OS == .ios {
|
inline if OS == .ios {
|
||||||
uikit_chdir_to_bundle();
|
uikit_chdir_to_bundle();
|
||||||
uikit_register_classes();
|
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;
|
true;
|
||||||
}
|
}
|
||||||
@@ -159,7 +175,10 @@ impl Platform for UIKitPlatform {
|
|||||||
|
|
||||||
end_frame :: (self: *UIKitPlatform) {
|
end_frame :: (self: *UIKitPlatform) {
|
||||||
inline if OS == .ios {
|
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);
|
objc_registerClassPair(SxAppDelegate);
|
||||||
|
|
||||||
uikit_register_gl_view_class();
|
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
|
// NSNotification callback. The notification's userInfo dict has the
|
||||||
// keyboard's end-frame and animation curve/duration.
|
// keyboard's end-frame and animation curve/duration.
|
||||||
// UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords)
|
// 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);
|
UIWindow := objc_getClass("UIWindow".ptr);
|
||||||
UIViewController := objc_getClass("UIViewController".ptr);
|
UIViewController := objc_getClass("UIViewController".ptr);
|
||||||
SxGLView := objc_getClass("SxGLView".ptr);
|
SxGLView := objc_getClass("SxGLView".ptr);
|
||||||
|
SxMetalView := objc_getClass("SxMetalView".ptr);
|
||||||
EAGLContext := objc_getClass("EAGLContext".ptr);
|
EAGLContext := objc_getClass("EAGLContext".ptr);
|
||||||
CADisplayLink := objc_getClass("CADisplayLink".ptr);
|
CADisplayLink := objc_getClass("CADisplayLink".ptr);
|
||||||
NSRunLoop := objc_getClass("NSRunLoop".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);
|
vc_raw := msg_o(UIViewController, sel_alloc);
|
||||||
plat.root_vc = msg_o(vc_raw, sel_init);
|
plat.root_vc = msg_o(vc_raw, sel_init);
|
||||||
|
|
||||||
// Allocate SxGLView and install it as the VC's view, so the standard
|
// Allocate either SxGLView or SxMetalView based on gpu_mode and install
|
||||||
// ViewController layout pipeline sizes the GL view to the window. Setting
|
// it as the VC's view. The view's +layerClass override gives us the
|
||||||
// it BEFORE setRootViewController avoids the VC lazy-loading a default
|
// right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE
|
||||||
// view first.
|
// setRootViewController avoids the VC lazy-loading a default view.
|
||||||
glv_raw := msg_o(SxGLView, sel_alloc);
|
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);
|
plat.gl_view = msg_o(glv_raw, sel_init);
|
||||||
sel_set_view := sel_registerName("setView:".ptr);
|
sel_set_view := sel_registerName("setView:".ptr);
|
||||||
msg_oo(plat.root_vc, sel_set_view, plat.gl_view);
|
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);
|
plat.gl_layer = msg_o(plat.gl_view, sel_layer);
|
||||||
|
|
||||||
// Mark the layer opaque (no compositor blend) + set the drawable properties
|
// Mark the layer opaque (no compositor blend). Required for EAGL +
|
||||||
// required by EAGLContext.renderbufferStorage:fromDrawable: (color format,
|
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
|
||||||
// non-retained backing). Without this dict the renderbuffer allocation
|
// hurt to be explicit).
|
||||||
// silently fails and the framebuffer reports INCOMPLETE.
|
|
||||||
sel_set_opaque := sel_registerName("setOpaque:".ptr);
|
sel_set_opaque := sel_registerName("setOpaque:".ptr);
|
||||||
msg_obool : (*void, *void, u8) -> void = xx objc_msgSend;
|
msg_obool : (*void, *void, u8) -> void = xx objc_msgSend;
|
||||||
msg_obool(plat.gl_layer, sel_set_opaque, 1);
|
msg_obool(plat.gl_layer, sel_set_opaque, 1);
|
||||||
|
|
||||||
NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr);
|
if plat.gpu_mode == .gles {
|
||||||
NSNumber := objc_getClass("NSNumber".ptr);
|
// EAGL drawable properties dict required by
|
||||||
sel_dictionary := sel_registerName("dictionary".ptr);
|
// EAGLContext.renderbufferStorage:fromDrawable: (color format,
|
||||||
sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr);
|
// non-retained backing). Without this dict the renderbuffer
|
||||||
sel_number_bool := sel_registerName("numberWithBool:".ptr);
|
// allocation silently fails and the framebuffer reports INCOMPLETE.
|
||||||
sel_set_drawable := sel_registerName("setDrawableProperties:".ptr);
|
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;
|
msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend;
|
||||||
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
|
ns_no := msg_oio(NSNumber, sel_number_bool, 0);
|
||||||
|
|
||||||
// The EAGL dict keys/values must be the framework-provided NSString
|
// The EAGL dict keys/values must be the framework-provided NSString
|
||||||
// constants (pointer identity is checked) — dlsym them from OpenGLES.
|
// constants (pointer identity is checked) — dlsym them from OpenGLES.
|
||||||
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
|
retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr);
|
||||||
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
|
colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr);
|
||||||
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
|
rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr);
|
||||||
|
|
||||||
dict := msg_o(NSMutableDictionary, sel_dictionary);
|
dict := msg_o(NSMutableDictionary, sel_dictionary);
|
||||||
msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend;
|
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, ns_no, retained_key);
|
||||||
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
|
msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key);
|
||||||
msg_oo(plat.gl_layer, sel_set_drawable, dict);
|
msg_oo(plat.gl_layer, sel_set_drawable, dict);
|
||||||
|
}
|
||||||
|
|
||||||
// EAGLContext + load_gl were already done in uikit_create_gl_context()
|
// EAGLContext + load_gl were already done in uikit_create_gl_context()
|
||||||
// back when the game's main called plat.init() — so shaders/textures
|
// 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; }
|
if g_uikit_plat == null { return; }
|
||||||
plat := g_uikit_plat;
|
plat := g_uikit_plat;
|
||||||
if plat.gl_initialized { return; }
|
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;
|
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
|
// Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an
|
||||||
// NSSet<UITouch *> + UIEvent. We take the first touch (single-touch model
|
// NSSet<UITouch *> + UIEvent. We take the first touch (single-touch model
|
||||||
// matching the chess game's drag-and-tap UX) and push the resulting
|
// 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);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2848,19 +2848,29 @@ pub const LLVMEmitter = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef {
|
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 n: c_uint = @intCast(agg.len);
|
||||||
const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty);
|
const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty);
|
||||||
defer self.alloc.free(vals);
|
defer self.alloc.free(vals);
|
||||||
for (agg, 0..) |cv, i| {
|
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) {
|
vals[i] = switch (cv) {
|
||||||
.int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1),
|
.int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1),
|
||||||
.float => |v| c.LLVMConstReal(elem_ty, v),
|
.float => |v| c.LLVMConstReal(elem_ty, v),
|
||||||
.boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0),
|
.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),
|
.aggregate => |inner| self.emitConstAggregate(inner, elem_ty),
|
||||||
else => c.LLVMConstNull(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);
|
return c.LLVMConstArray(elem_ty, vals.ptr, n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
107
src/ir/lower.zig
107
src/ir/lower.zig
@@ -447,10 +447,9 @@ pub const Lowering = struct {
|
|||||||
},
|
},
|
||||||
.var_decl => |vd| {
|
.var_decl => |vd| {
|
||||||
// Top-level mutable global (e.g., `context : Context = ---;`)
|
// Top-level mutable global (e.g., `context : Context = ---;`)
|
||||||
const var_ty = if (vd.type_annotation) |ta|
|
// Use self.resolveType so type aliases like `Handle :: u32;` resolve
|
||||||
type_bridge.resolveAstType(ta, &self.module.types)
|
// to their target type (not a synthetic empty struct).
|
||||||
else
|
const var_ty = self.resolveType(vd.type_annotation);
|
||||||
.s64;
|
|
||||||
const name_id = self.module.types.internString(vd.name);
|
const name_id = self.module.types.internString(vd.name);
|
||||||
const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) {
|
const init_val: ?inst_mod.ConstantValue = if (vd.value) |v| switch (v.data) {
|
||||||
.undef_literal => .zeroinit,
|
.undef_literal => .zeroinit,
|
||||||
@@ -459,6 +458,7 @@ pub const Lowering = struct {
|
|||||||
.float_literal => |fl| .{ .float = fl.value },
|
.float_literal => |fl| .{ .float = fl.value },
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
||||||
.array_literal => |al| self.constArrayLiteral(al.elements),
|
.array_literal => |al| self.constArrayLiteral(al.elements),
|
||||||
|
.struct_literal => |sl| self.constStructLiteral(&sl, var_ty),
|
||||||
else => null,
|
else => null,
|
||||||
} else null;
|
} else null;
|
||||||
const gid = self.module.addGlobal(.{
|
const gid = self.module.addGlobal(.{
|
||||||
@@ -479,22 +479,67 @@ pub const Lowering = struct {
|
|||||||
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
|
fn constArrayLiteral(self: *Lowering, elements: []const *const Node) ?inst_mod.ConstantValue {
|
||||||
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
|
const vals = self.alloc.alloc(inst_mod.ConstantValue, elements.len) catch return null;
|
||||||
for (elements, 0..) |elem, i| {
|
for (elements, 0..) |elem, i| {
|
||||||
vals[i] = switch (elem.data) {
|
vals[i] = self.constExprValue(elem) orelse return null;
|
||||||
.int_literal => |il| .{ .int = il.value },
|
}
|
||||||
.bool_literal => |bl| .{ .boolean = bl.value },
|
return .{ .aggregate = vals };
|
||||||
.float_literal => |fl| .{ .float = fl.value },
|
}
|
||||||
.string_literal => |sl| .{ .string = self.module.types.internString(sl.raw) },
|
|
||||||
.unary_op => |uo| switch (uo.op) {
|
/// Try to convert a single AST expression into a compile-time ConstantValue.
|
||||||
.negate => switch (uo.operand.data) {
|
/// Returns null if the expression is not constant-foldable here.
|
||||||
.int_literal => |il| .{ .int = -il.value },
|
fn constExprValue(self: *Lowering, expr: *const Node) ?inst_mod.ConstantValue {
|
||||||
.float_literal => |fl| .{ .float = -fl.value },
|
return switch (expr.data) {
|
||||||
else => return null,
|
.int_literal => |il| .{ .int = il.value },
|
||||||
},
|
.bool_literal => |bl| .{ .boolean = bl.value },
|
||||||
else => return null,
|
.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 => null,
|
||||||
else => return 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 };
|
return .{ .aggregate = vals };
|
||||||
}
|
}
|
||||||
@@ -862,6 +907,12 @@ pub const Lowering = struct {
|
|||||||
fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
|
fn lowerInlineBranch(self: *Lowering, node: *const Node) Ref {
|
||||||
if (node.data == .block) {
|
if (node.data == .block) {
|
||||||
self.lowerBlock(node);
|
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.builder.constInt(0, .void);
|
||||||
}
|
}
|
||||||
return self.lowerExpr(node);
|
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));
|
const elem_ty = self.getElementType(self.inferExprType(asgn.target.data.index_expr.object));
|
||||||
if (elem_ty != .void) self.target_type = elem_ty;
|
if (elem_ty != .void) self.target_type = elem_ty;
|
||||||
} else if (asgn.target.data == .field_access) {
|
} else if (asgn.target.data == .field_access) {
|
||||||
// For obj.field = val, set target_type to the field's type
|
// For obj.field = val, set target_type to the field's type so RHS
|
||||||
// Only for enum literals and struct literals — these need target_type to resolve.
|
// sub-expressions (enum/struct literals, branch arms, xx casts) can
|
||||||
// Avoid setting it for complex RHS expressions (calls, casts) because
|
// resolve against it. Skipped for forms that would forward the type
|
||||||
// resolveCallParamTypes can't override target_type for method-call args.
|
// unchanged into method-call arg slots (`resolveCallParamTypes` can't
|
||||||
|
// override target_type per-arg).
|
||||||
const needs_target = switch (asgn.value.data) {
|
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,
|
.call => |vc| vc.callee.data == .enum_literal,
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
@@ -1266,7 +1318,7 @@ pub const Lowering = struct {
|
|||||||
for (fields, 0..) |f, i| {
|
for (fields, 0..) |f, i| {
|
||||||
if (f.name == field_name_id) {
|
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 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);
|
const coerced = self.coerceToType(val, src_ty, f.ty);
|
||||||
self.storeOrCompound(gep, coerced, asgn.op, f.ty);
|
self.storeOrCompound(gep, coerced, asgn.op, f.ty);
|
||||||
return;
|
return;
|
||||||
@@ -1280,7 +1332,7 @@ pub const Lowering = struct {
|
|||||||
// GEP into union payload area, then into the struct field
|
// 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 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 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);
|
const coerced = self.coerceToType(val, src_ty, sf.ty);
|
||||||
self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty);
|
self.storeOrCompound(field_gep, coerced, asgn.op, sf.ty);
|
||||||
return;
|
return;
|
||||||
@@ -1308,8 +1360,9 @@ pub const Lowering = struct {
|
|||||||
// target type, storing element-sized bytes instead of a pointer.
|
// target type, storing element-sized bytes instead of a pointer.
|
||||||
const gep_ty = self.module.types.ptrTo(field_ty);
|
const gep_ty = self.module.types.ptrTo(field_ty);
|
||||||
const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty);
|
const gep = self.builder.structGepTyped(obj_ptr, field_idx, gep_ty, obj_ty);
|
||||||
// Coerce value to field type
|
// Coerce value to field type — use the lowered value's actual type
|
||||||
const src_ty = self.inferExprType(asgn.value);
|
// (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);
|
const coerced = self.coerceToType(val, src_ty, field_ty);
|
||||||
self.storeOrCompound(gep, coerced, asgn.op, field_ty);
|
self.storeOrCompound(gep, coerced, asgn.op, field_ty);
|
||||||
}
|
}
|
||||||
|
|||||||
1
tests/expected/67-impl-for-builtin.exit
Normal file
1
tests/expected/67-impl-for-builtin.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/67-impl-for-builtin.txt
Normal file
2
tests/expected/67-impl-for-builtin.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
lerp(0, 10, 0.5) = 5.000000
|
||||||
|
lerp(0, 10, 0.25) = 2.500000
|
||||||
1
tests/expected/68-generic-protocol-constraint.exit
Normal file
1
tests/expected/68-generic-protocol-constraint.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
3
tests/expected/68-generic-protocol-constraint.txt
Normal file
3
tests/expected/68-generic-protocol-constraint.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
after set: 100.000000x50.000000
|
||||||
|
mid anim: 150.000000x75.000000
|
||||||
|
end anim: 200.000000x100.000000
|
||||||
1
tests/expected/69-optional-all-null.exit
Normal file
1
tests/expected/69-optional-all-null.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
3
tests/expected/69-optional-all-null.txt
Normal file
3
tests/expected/69-optional-all-null.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
r1 = 150.000000
|
||||||
|
r2 = 150.000000
|
||||||
|
r3 = 200.000000
|
||||||
1
tests/expected/70-optional-roundtrip.exit
Normal file
1
tests/expected/70-optional-roundtrip.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
10
tests/expected/70-optional-roundtrip.txt
Normal file
10
tests/expected/70-optional-roundtrip.txt
Normal 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
|
||||||
1
tests/expected/71-int-cmp-in-float-ternary.exit
Normal file
1
tests/expected/71-int-cmp-in-float-ternary.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/71-int-cmp-in-float-ternary.txt
Normal file
2
tests/expected/71-int-cmp-in-float-ternary.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ok
|
||||||
|
result = 1.000000
|
||||||
1
tests/expected/72-protocol-in-wrapper-struct.exit
Normal file
1
tests/expected/72-protocol-in-wrapper-struct.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
4
tests/expected/72-protocol-in-wrapper-struct.txt
Normal file
4
tests/expected/72-protocol-in-wrapper-struct.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
inside add: 42
|
||||||
|
inside add: 99
|
||||||
|
items[0] = 42 (expected 42)
|
||||||
|
items[1] = 99 (expected 99)
|
||||||
1
tests/expected/73-protocol-list-from-fn.exit
Normal file
1
tests/expected/73-protocol-list-from-fn.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
7
tests/expected/73-protocol-list-from-fn.txt
Normal file
7
tests/expected/73-protocol-list-from-fn.txt
Normal 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 ===
|
||||||
1
tests/expected/74-protocol-dispatch-via-fn-arg.exit
Normal file
1
tests/expected/74-protocol-dispatch-via-fn-arg.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
8
tests/expected/74-protocol-dispatch-via-fn-arg.txt
Normal file
8
tests/expected/74-protocol-dispatch-via-fn-arg.txt
Normal 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 ===
|
||||||
1
tests/expected/75-push-context-with-arena.exit
Normal file
1
tests/expected/75-push-context-with-arena.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/75-push-context-with-arena.txt
Normal file
2
tests/expected/75-push-context-with-arena.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
inside push
|
||||||
|
after push
|
||||||
1
tests/expected/76-closure-returning-protocol.exit
Normal file
1
tests/expected/76-closure-returning-protocol.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/76-closure-returning-protocol.txt
Normal file
2
tests/expected/76-closure-returning-protocol.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
direct call works
|
||||||
|
closure call works
|
||||||
1
tests/expected/77-list-items-assign-big-T.exit
Normal file
1
tests/expected/77-list-items-assign-big-T.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
4
tests/expected/77-list-items-assign-big-T.txt
Normal file
4
tests/expected/77-list-items-assign-big-T.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
size_of BigNode = 40
|
||||||
|
before: sentinel = 3735928559
|
||||||
|
after: sentinel = 3735928559
|
||||||
|
OK
|
||||||
1
tests/expected/78-global-compound-assign.exit
Normal file
1
tests/expected/78-global-compound-assign.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
10
tests/expected/78-global-compound-assign.txt
Normal file
10
tests/expected/78-global-compound-assign.txt
Normal 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
|
||||||
1
tests/expected/79-global-array-init.exit
Normal file
1
tests/expected/79-global-array-init.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/79-global-array-init.txt
Normal file
2
tests/expected/79-global-array-init.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
VALS: -2 -1 42 99
|
||||||
|
PASS
|
||||||
1
tests/expected/80-dot-shorthand-protocol-field.exit
Normal file
1
tests/expected/80-dot-shorthand-protocol-field.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
3
tests/expected/80-dot-shorthand-protocol-field.txt
Normal file
3
tests/expected/80-dot-shorthand-protocol-field.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
StackA: draw=42
|
||||||
|
StackB: draw=25
|
||||||
|
OK
|
||||||
1
tests/expected/81-global-struct-defaults.exit
Normal file
1
tests/expected/81-global-struct-defaults.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
6
tests/expected/81-global-struct-defaults.txt
Normal file
6
tests/expected/81-global-struct-defaults.txt
Normal 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
|
||||||
1
tests/expected/82-xx-target-in-field-assign.exit
Normal file
1
tests/expected/82-xx-target-in-field-assign.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
2
tests/expected/82-xx-target-in-field-assign.txt
Normal file
2
tests/expected/82-xx-target-in-field-assign.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
dpi=2.000000
|
||||||
|
delta=0.500000 fc.a=1.000000
|
||||||
1
tests/expected/83-inline-if-return-fallthrough.exit
Normal file
1
tests/expected/83-inline-if-return-fallthrough.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1
|
||||||
1
tests/expected/83-inline-if-return-fallthrough.txt
Normal file
1
tests/expected/83-inline-if-return-fallthrough.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
tests/expected/84-global-type-alias.exit
Normal file
1
tests/expected/84-global-type-alias.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
1
tests/expected/84-global-type-alias.txt
Normal file
1
tests/expected/84-global-type-alias.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user