NSLog's fmt, addObserver's name, UIApplicationMain's principal-class, CADisplayLink's run-loop mode, and metal's newLibraryWithSource/newFunctionWithName string args are retyped *NSString, so their call sites read xx "..." instead of ns_string("...".ptr). ns_string is now used only by impl Into(*NSString) for string.
The chess panel-text regression (text vanished after the first move on
macOS) had a single root cause: GlyphCache's entries List, hash table,
and shaped_buf grew through `context.allocator` — which during render
is the per-frame arena. On the next arena reset the backing died, and
subsequent glyph lookups read garbage / wrote into freshly-allocated
view-tree memory.
Fix is shaped as the user proposed: `List(T)`'s mutations take an
optional trailing `alloc: Allocator = context.allocator` argument. No
allocator stored on the container, no init ceremony, every existing
`list.append(item)` callsite keeps working unchanged. Long-lived
owners now write `list.append(item, self.parent_allocator)` and the
arena-leak bug becomes impossible to write accidentally.
Default-arg substitution previously only fired for identifier callees
(`expandCallDefaults` at lower.zig:7978). Extended to the generic
struct-method dispatch path (`list.append(...)` lands here) via a new
`appendDefaultArgs` helper that lowers fd.params[i].default_expr in
the caller's scope and appends to the lowered args slice.
Long-lived owners updated to capture `parent_allocator: Allocator` at
init and use it for every internal growth:
- GlyphCache (the chess bug) — entries, shaped_buf, hash_keys,
hash_vals, atlas bitmap.
- DockInteraction — drops the existing `push Context` workaround in
`ensure_capacity` for the explicit-arg form.
- StateStore — entries list + per-entry data buffer.
- Gles3Gpu, MetalGPU — shaders, buffers, textures (atlas-grow during
render would otherwise leak resources into the frame arena).
Also kept: an operator-precedence fix in pipeline.sx
(`(self.frame_index & 1) == 0` instead of
`self.frame_index & 1 == 0`, which parses as
`self.frame_index & (1 == 0)` = always 0). That was a stealth
single-arena-only bug that masked the GlyphCache one for a long time.
Docs:
- specs.md §11 documents `param: T = expr` default parameter values.
The parser already supported it — formalised in the spec now.
- current/CHECKPOINT-MEM.md logs the change.
- CLAUDE.md REJECTED PATTERNS gains a "Long-lived containers growing
through context.allocator" section with the `parent_allocator`
capture template and the list of existing examples to mirror.
155/155 example tests pass — zero-diff against snapshots since every
existing callsite still resolves to `context.allocator`.
Verify-step uncovered three categories of regressions where sx code
calls into the platform's C ABI through fn-pointer types or as a
registered callback. Every site now declares the right convention.
C-side calls INTO sx → callconv(.c) on the sx function:
- platform/android.sx: sx_android_render_thread_entry is the start
routine pthread_create invokes — pthread treats it as a C function.
Also annotate the pthread_create signature so the start-routine fn-
pointer field rejects mismatching sx fns at compile time.
sx code calling typed fn-pointers cast from C symbols → callconv(.c)
on the fn-pointer type:
- opengl.sx: 55 GL fn-ptr globals + load_gl's proc-loader param. GL
trampolines are macOS/iOS/Android system code.
- std/objc.sx: the two typed `objc_msgSend` casts.
- gpu/metal.sx: ~40 typed `objc_msgSend` casts across Metal command
encoder / device / pipeline construction.
The block invoke trampolines (objc_block.sx) call back INTO sx (the
closure trampoline). The typed fn-ptr there stays default-conv so
ctx prepends correctly. Compiler change: a callconv(.c) sx function
now binds `current_ctx_ref` to `&__sx_default_context` at entry (used
to be gated by `isExportedEntryName`). C-callable sx callbacks like
the block invokes don't get their own __sx_ctx param but their bodies
still need a real Context to forward to the closure they delegate to.
Tests: 152/152 example suite + chess green on all 3 platforms.
Screenshots at /tmp/sx-game-{macos,iossim,android}.png.
Deletes the entire NativeActivity / native_app_glue / ALooper stack
that the previous Android entry path was built around:
- `examples/99-android-egl-clear.sx` — the demo of the legacy path.
- `library/modules/platform/android.sx` — `AndroidPlatform.init`,
`run_frame_loop`, `sx_android_bootstrap`, `g_android_app`, plus
the ALooper / AInputEvent / ANativeActivity / AConfiguration
foreign decls that fed them. The JNI helpers (`sx_load_javavm_fn`,
`sx_android_get_env`, `sx_query_safe_insets_jni`, the
`ANATIVEACTIVITY_*` offsets) were tied to the ANativeActivity*
delivered to `android_main` — they're stale now that the OS hands
sx code a Java Activity directly via `onCreate(JNIEnv*, jobject)`.
- `library/vendors/sx_android_jni/sx_android_jni.c` — the input-
handler installer (`sx_android_install_input_handler`), which
poked NDK app-pointer field offsets that no longer exist.
`library/modules/platform/android_jni.sx` (the `Activity`/`Window`/
`View`/`WindowInsets` `#jni_class` registry used for safe-insets
dispatch) survives — it's standalone declarative bindings useful from
any `#jni_main` onCreate body. Docstring updated to drop the
"imported from android.sx" framing.
131 host / 4 cross / zig build test all green. End-to-end smoke APK
still produces the expected JNI-mangled symbol +
SxApp-extends-Activity dex.
External consumers (chess) will need to migrate their entry from the
`AndroidPlatform.run_frame_loop` model to the `#jni_main` model
(Java-side Activity drives lifecycle; onSurfaceChanged / Choreographer
drive frames via JNI callbacks). That migration is downstream work.
Four Android UX wins landing together; all verified end-to-end on a
Pixel 7 Pro (board fills width, info-panel text renders, status bar
inset honored, tap-to-select + tap-to-move plays 1. e4).
- AndroidPlatform.init reads density via AConfiguration_getDensity
(app->config at offset 32) and sets dpi_scale = density / 160. The
hardcoded 1.0 had been making every logical unit equal one physical
pixel; ChessBoardView's 520-default size_that_fits fallback then
rendered at ~half the framebuffer width on the device, and glyphs
rasterized at literal 11-13 physical pixels were essentially invisible
on a 2340-tall display.
- gles3.sx set_scissor un-stubbed; with dpi_scale right the renderer
feeds in valid pixel bounds and the Y-flip math lands inside the
framebuffer.
- New library/vendors/sx_android_jni/sx_android_jni.c walks
activity -> window -> decorView -> rootWindowInsets via JNI and
publishes the system-bar insets. safe_insets() lazy-queries the
first call after EGL is up (decor view isn't attached at bootstrap).
- sx_android_install_input_handler sets app->onInputEvent; sx-side
sx_android_input_event translates AMotionEvent DOWN/MOVE/UP/CANCEL
into existing mouse_down/mouse_moved/mouse_up Events so the chess
board's tap-to-select + ScrollView drag path Just Works. Coordinates
divided by dpi_scale so layout-side hit tests match. poll_events
drains its slice after returning (mirrors the SDL pattern).
- src/imports.zig now routes #import c { #source / #include } paths
through the same chain as #import (importing dir -> CWD -> stdlib
search paths). Lets library-owned C helpers like the JNI bridge
live in sx/library/vendors/ without forcing consumers to vendor a
copy. Existing CWD-relative consumer layouts (chess's vendors/...)
still resolve first, so no regression.
86/86 regression tests pass.
platform/android.sx: `sx_android_bootstrap(app)` now also reads the
ANativeActivity's `assetManager` (offset 64) and `internalDataPath`
(offset 32) into module globals so consumers can route file I/O
through the APK's bundled `assets/` tree.
target.zig (`createApk`): also zips the project's `./assets/`
directory into the APK alongside `lib/<arch>/`. Resolves relative
to the user's CWD at invoke time — matches the convention chess
uses (assets/ next to main.sx).
gles3.sx: scissor is currently a no-op on Android. The renderer's
ScrollView clip_push path feeds bounds that land outside the
framebuffer (clipping everything off-screen). With scissor disabled
the chess board + pieces render correctly. TODO recorded in the
file to fix the bounds path properly.
Mirror of metal.sx, talks to GLES3 via opengl.sx's runtime-loaded
fn-pointer variables. EGL bootstrap is owned by AndroidPlatform; this
module just calls `load_gl(@eglGetProcAddress)` once during `init` to
populate the pointers, then drives raw draw/state from there.
The renderer's vertex layout (12 floats: pos2/uv2/color4/params4 = 48
bytes, attribute locations 0-3) is hardcoded in a single shared VAO
the Gles3Gpu owns — `set_vertex_buffer` rebinds the active VBO against
it. `set_vertex_constants(slot=1, data, 64)` is treated as the 4x4
projection matrix; `set_texture(slot=0, ...)` binds texture unit 0 and
sets `uniform sampler2D uTex` — both match renderer.sx's shader
contract.
A subtle gotcha caught + recorded in the file header: declaring the
same GL name as a `#foreign` function while opengl.sx also declares it
as an fn-pointer global silently lets the global win, and calling
through the uninitialized variable jumps to PC=0. Solution: don't
re-declare; use opengl.sx's pointers and `load_gl` them.
renderer.sx: the GPU-protocol shader-source branch now passes
(UI_VERT_SRC_ES, UI_FRAG_SRC_ES) on Android (separate vert+frag) vs.
the combined MSL library on iOS. Both gated with `inline if OS == X`.
Three new method signatures on the GPU protocol. Metal backend sends
`release` to the MTLTexture/Buffer/RenderPipelineState and nulls the
slot in its backing List so the handle becomes inert; handles are not
re-used. glyph_cache.grow() now destroys the old atlas before
allocating its replacement, eliminating the per-grow leak the file's
comment had been flagging since Session 62.
Two small cleanups in the Metal text path on top of the buffer-offset
fix from cc71d95:
- Drop the SDF-style `smoothstep(0.5 ± ew, alpha)` from the text mode
branch in UI_MSL_SRC. The glyph atlas stores alpha coverage straight
from stbtt_MakeGlyphBitmap, not signed distance, so the smoothstep
was thinning anti-aliased strokes by mapping mid-coverage values
(0.3–0.7) toward 0/1. Use the sampled value directly as alpha.
- Drop the 16-byte alignment pad on `mtl_buf_offset` in `flush()`. Each
batch's upload_size is already a multiple of UI_VERTEX_BYTES (48), so
the running offset stays vertex-aligned without the extra rounding.
- After `font.shape_text` + `font.flush` in `render_text`, re-bind
`font.texture_id`. If the atlas grew during shaping, the GPU texture
handle changed; without this rebind the next flush samples the old
(smaller) atlas which doesn't have the newly-rasterized glyphs.
- Use explicit s64-pointer arithmetic in `metal_update_buffer_at_ios`
so a future regression in `[*]u8` indexing can't quietly miscompile
the per-flush write offset.
Text at small sizes still renders dim on dark backgrounds — most glyph
pixels sit in 0.1–0.5 coverage and the linear blend doesn't push them
to bright values — tracked separately as the faint-text follow-up.
UIRenderer.flush wrote to mtl_vbuf at byte offset 0 on every flush.
Metal records draw commands but reads the buffer at GPU execution time,
so a frame with multiple flushes ended up rendering whatever the LAST
writer left in the buffer for every draw. Chess UI hit this hard:
each of the 32 pieces in the initial position triggers two bind_texture
flushes (atlas -> pieces -> atlas), so ~64 mid-frame flushes silently
rendered the final info-panel batch over the board and the sprites.
New GPU protocol method update_buffer_at(buf, data, size, byte_offset);
Metal impl writes at offset via [*]u8 arithmetic on [buf contents].
UIRenderer tracks mtl_buf_offset (reset in begin, advanced per flush,
aligned to 16B, wraps on overflow) and draws each batch with
vertex_off = byte_off / UI_VERTEX_BYTES. Metal buffer over-allocated
4x the per-flush max (~3 MB) for headroom. GL path untouched —
glBufferData already orphans the storage.
71/71 regression tests pass. Metal-clear example, macOS GL chess, and
WASM chess all still build.
Three stacked compiler bugs were causing iOS-sim chess to crash inside
[MTLTexture replaceRegion:...]. Fixing them lets every replaceRegion call
site succeed (1×1 RGBA8, 1MB R8 atlas, 440×440 chess pieces).
Path B for callconv(.c) fn-pointer casts:
- FunctionInfo now carries call_conv: CallConv (TypeInfo.CallConv) so
function-type interning distinguishes sx-CC from C-CC. Inst.zig's
Function.CallingConvention aliases the same enum.
- Parser accepts an optional `callconv(.c)` suffix on fn-pointer type
spellings (factored into parseOptionalCallConv() shared with parseFnDecl
and parseLambda).
- resolveFunctionType passes the parsed CC through functionTypeCC().
- .call_indirect reads fp.call_conv == .c and applies the C-ABI
alloca+materialize for >16B aggregate args (Path A's behaviour at .call).
Apple ARM64 ABI (drop LLVM byval):
- Side-by-side asm diff vs clang's emission for the equivalent C call site
showed LLVM's `byval` attribute lowers Apple-arm64 byval on the stack,
while clang passes the struct via a pointer in the next int register
(x2 for replaceRegion:). The runtime objc_msgSend dispatch path expects
clang's convention.
- Dropped the byval attribute from the function-signature emission and
from both call sites (.call and .call_indirect). The materialize-into-
alloca + pass-plain-ptr pattern stays — the call site now matches
clang's `mov x2, sp` exactly.
- Path A's sx-to-sx case continues to work since both ends use plain ptr
(caller does alloca+store+pass, callee loads from the ptr in prologue).
Protocol dispatch (emitProtocolDispatch):
- Untargeted `null` lowers as const_null with type .void (per
target_type orelse .void). The "wrap-value-in-alloca-pass-pointer"
branch alloca'd a void slot, which LLVM's IRBuilder asserts on —
EXC_BREAKPOINT in getTypeSizeInBits, manifesting as exit 133 / SIGTRAP
when building the chess game. Fixed by re-emitting as
constNull(void_ptr) when arg_ty == .void && expected_ty == void_ptr.
- is_pointer_ty only recognized .pointer, so [*]T (many_pointer) was
alloca-wrapped — the heap pixels pointer from stbi_load was stored
into a stack slot and the slot's address was passed as the *void arg.
Fixed by extending the check to `.pointer or .many_pointer`.
metal.sx call sites + lifecycle guards:
- msg_replace (replaceRegion:, MTLRegion = 48B) and the two setScissorRect:
sites (MTLScissorRect = 32B) now spell their fn-pointer types with
by-value params + callconv(.c) — the *MTLRegion/@local workaround is
gone.
- metal_begin_frame_ios bails before nextDrawable when pixel_w/h are 0
(drawableSize 0×0 makes nextDrawable abort via XPC).
- metal_init_ios only sets drawableSize when dims are positive.
- begin_frame's encoder/cmd_buffer failure paths now clear self.drawable
so a partial failure doesn't leak a drawable back into the pool.
Examples + tests:
- examples/86-callconv-c-fnptr-large-aggregate.sx — new, covers Path B
with C-CC fn-ptr cast.
- examples/87-fnptr-cast-large-aggregate.sx — renamed from issue-0025.sx,
covers Path B with default sx-CC (the negative case).
- examples/85-cc-c-large-aggregate.sx — from Session 60, covers Path A.
- examples/issue-0014.sx, issue-0024.sx, issue-0025.sx — removed
(resolved earlier this work).
71 regression tests pass, 0 failed. Chess game builds clean for iOS sim
and reaches its frame loop without aborting. Runtime: chess UI still
doesn't render — remaining issue is in the UIKit lifecycle / CAMetalLayer
setup (legacy-app vs scene-API hybrid), not a compiler bug. See
current/CHECKPOINT.md "Next step" for the diagnosis + options.
Step 3b code is wired across UIRenderer + GlyphCache + UIPipeline +
chess game (gpu_mode = .metal on iOS, MetalGPU bound via the GPU
protocol). macOS GL chess, iOS-sim GLES chess, and iOS-sim Metal
triangle (63-metal-clear.sx) all still render.
iOS-sim Metal chess crashes inside replaceRegion uploading the 1MB
font atlas. Bisecting that crash exposed several sx-language issues
where mid-bisect tracers (NSLog inside if/else branch bodies) didn't
produce output, blocking further investigation.
Filing each finding as examples/issue-NNNN.sx rather than working
around piecemeal:
Bugs:
- 0024 NSLog/foreign-call inside if/else body not producing output
- 0025 C-ABI param coercion incomplete for composites >16B
(combined direct-call abiCoerceParamType TODO + call_indirect
path that doesn't apply C-ABI coercion at all)
- 0026 replaceRegion 1MB upload crash (likely downstream of 0025)
Features needed for step 4 + cleanup:
- 0027 Obj-C block bridge (^{...}) for animateWithDuration:
- 0028 Optional protocol box (?GPU = null) replaces T = ---; has_T: bool
- 0029 destroy_texture/buffer/shader on GPU protocol
- 0030 extern cross-file globals
Library-side: renderer.sx + glyph_cache.sx + pipeline.sx gain a
`gpu: GPU = ---; has_gpu: bool` field pair + branches that route every
GL touchpoint through the protocol when has_gpu. glyph_cache.init
saves/restores those fields around its memset. pipeline.set_gpu()
propagates to renderer + font. Renderer's MSL shader source added as
UI_MSL_SRC using packed_float2/packed_float4 to keep the 12-float
interleaved vertex layout tight (48 bytes).
metal.sx: dual-phase init (init(null, 0, 0) for eager device+queue,
re-init with the layer once UIKit installs the SxMetalView).
setStorageMode:.shared on every texture descriptor to ensure CPU-
writable atlas pixels on Apple Silicon iOS-sim.
Regression suite: 68 passing, 0 failed. WASM chess build currently
broken under step 3b state (silent compiler crash); documented in
CHECKPOINT.md, likely fallout from one of the filed issues (probably
0028 — the verbose protocol-box pattern). Step 3b resumes after
0024-0030 land.
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.