Skip to content

aarch64-linux-android: obj.field reads of numeric fields return garbage #128

@proggeramlug

Description

@proggeramlug

Summary

On --target android (aarch64-linux-android), reads of numeric fields from a plain object return incorrect values. The bug affects any obj.field read of a numeric field — not just reads feeding FFI arguments. Comparisons between two fields return the wrong boolean, arithmetic using fields produces NaN, and FFI calls that take obj.field as an f64 parameter receive NaN.

All other targets tested (native macOS, iOS, tvOS) emit correct code for the same source.

Perry version: 0.5.146
NDK: 30.0.14904198, arch aarch64-linux-android24
Device: Pixel 4a, Adreno 618, Android 14, Vulkan backend

Minimal reproducer

repro.ts:

interface Rect { x: number; y: number; width: number; height: number; }

function overlapsPrim(
  ax: number, ay: number, aw: number, ah: number,
  bx: number, by: number, bw: number, bh: number,
): boolean {
  return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}

function overlapsObj(a: Rect, b: Rect): boolean {
  return a.x < b.x + b.width
      && a.x + a.width  > b.x
      && a.y < b.y + b.height
      && a.y + a.height > b.y;
}

const A: Rect = { x: 10.0, y: 20.0, width: 100.0, height: 50.0 };
const B: Rect = { x: 50.0, y: 30.0, width: 20.0,  height: 20.0 };

const prim = overlapsPrim(A.x, A.y, A.width, A.height, B.x, B.y, B.width, B.height);
const obj  = overlapsObj(A, B);

console.log("prim =", prim);
console.log("obj  =", obj);
console.log("A.x =", A.x, "A.y =", A.y, "A.width =", A.width, "A.height =", A.height);
if (prim !== obj) console.log("BUG: overlapsPrim and overlapsObj disagree");

Host build (works)

$ perry compile repro.ts -o repro && ./repro
prim = true
obj  = true
A.x = 10 A.y = 20 A.width = 100 A.height = 50

Android build (broken)

$ ANDROID_NDK_HOME=... perry compile --target android repro.ts -o repro_android

The android output is a shared-object-style ELF intended to be loaded via System.loadLibrary from an Android activity (running the ELF via adb shell SIGILLs due to Pixel seccomp — unrelated). Loaded inside a proper JNI context (via Bloom Engine's Android activity harness), the expected logs never print with correct values:

  • A.x, A.y, A.width, A.height print as NaN
  • overlapsObj returns false for clearly overlapping rects
  • overlapsPrim (which only uses primitive args) returns true as expected
  • BUG: …disagree fires

Observed impact in a production app

Bloom Jump (2D platformer, src/main.ts built with perry compile --target android) hit this bug in every form, over time:

Pattern Symptom on Android Workaround
drawText(text, x, y, size, color: Color)bloom_draw_text(…, color.r, color.g, color.b, color.a) Text invisible (glyph quads dropped when any channel is NaN) drawTextRgba(text, x, y, size, r, g, b, a) taking primitives
loadMusic(path) returning { handle: number }, then bloom_play_music(music.handle) Music silently never plays (NaN as usize == 0) loadMusicRaw/playMusicRaw returning/taking a raw number
beginMode2D(camera: Camera2D)bloom_begin_mode_2d(camera.offset.x, camera.target.x, camera.zoom, …) Entire scene invisible inside the camera mode; background + HUD drawn outside it work beginMode2DRaw(ox, oy, tx, ty, rot, zoom) taking primitives
const ATLAS_ID = loadTexture("atlas").id; at module scope All textured quads invisible (ID is NaN → native rejects draw) Call bloom_load_texture(path) FFI directly, store the returned number
Pure-TS checkCollisionRecs(a: Rect, b: Rect) (no FFI involved) Always returns false — player can't collect coins, enemies never hurt/get stomped Flat [x,y,w,h] arrays + primitive-arg aabbOverlap(ax,ay,…)

The last row is the most telling: there is no FFI in the call path at all. The miscompilation is at the obj.field read itself.

Why I think it's codegen, not source

  • Same TS compiles and runs correctly on every non-android target (macOS, iOS, tvOS produced from the same main.ts).
  • Replacing any obj.field read with an equivalent flat-array arr[idx] read or primitive local variable immediately fixes the symptom at that call site.
  • The source patterns above are idiomatic TypeScript and match how Perry itself writes engine wrappers (e.g. engine/src/shapes/index.ts:checkCollisionRecs).

What would help

  • A fix in the aarch64-linux-android codegen for obj.field (numeric-field) reads.
  • In the meantime, even a diagnostic that flags when obj.field reads flow through the affected code path would save users from silent failure — the production symptoms (invisible text, silent audio, invisible rendering, broken collision) all fail silently with no runtime error.

Happy to run further experiments on request. Bloom Jump's workarounds live at:

  • engine/src/text/index.tsdrawTextRgba
  • engine/src/audio/index.tsloadMusicRaw family
  • engine/src/core/index.tsbeginMode2DRaw
  • engine/src/textures/index.tsdrawTextureProRaw
  • jump/src/main.ts — uses all of the above + flat-array collision rects

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions