bundling: Android APK pipeline moved into sx; android.sx state-on-plat
Week 7 of /Users/agra/.claude/plans/lets-plan-to-move-splendid-pumpkin.md
plus the android.sx refactor + three sx-compiler fixes hit along the way
to get chess on Pixel 7 Pro responding to touch end-to-end.
library/modules/platform/bundle.sx now covers the Android APK shape
alongside macOS / iOS-sim / iOS-device. `android_bundle_main` discovers
the SDK ($ANDROID_HOME / $ANDROID_SDK_ROOT / $HOME/Library/Android/sdk),
picks the highest-versioned build-tools + platforms via
`process.run("ls .. | sort -V | tail -1")`, stages
`<apk>.stage/lib/arm64-v8a/<libfoo.so>`, synthesizes
AndroidManifest.xml (NativeActivity vs `#jni_main` Activity branch),
writes each `#jni_main` decl's Java source under
`<stage>/java/<pkg>/<Cls>.java`, runs javac --release 11 + d8 to
produce classes.dex, aapt2-links the unaligned APK, appends lib/ +
classes.dex + each registered asset tree via zip, zipalign + ensure
debug keystore via keytool + apksigner sign.
Compiler-side accessors (src/ir/compiler_hooks.zig + library/modules/compiler.sx):
- is_android predicate.
- set_manifest_path / manifest_path + set_keystore_path / keystore_path.
- jni_main_count / jni_main_foreign_path_at(i) /
jni_main_java_source_at(i) surface the `#jni_main` emissions that
the Zig createApk previously consumed directly.
- main.zig wires manifest_path, keystore_path, and the per-decl
(foreign_path, java_source) parallel slices into BuildConfig before
invoking the post-link callback.
CLI `--apk <path>` keeps working as a transitional alias: it now feeds
bundle_path so the existing auto-`post_link_module = "platform.bundle"`
shim fires the same way as `--bundle`. main.zig no longer calls
target.createApk directly.
Deletions in src/target.zig: createApk, compileJniMainSources,
buildJniMainManifest, buildAndroidManifest, ensureDebugKeystore,
libNameFromSoBasename, plus helpers splitForeignPath / discoverJavac /
discoverAndroidSdk / findHighestSubdir / runProcess / runProcessIn
(~400 lines). git grep returns only the obituary comment.
library/modules/platform/android.sx refactor (chess Android dependency):
- Module-level globals retired (g_app_window, g_egl_*, g_viewport_*,
g_dpi_scale, g_should_stop, g_render_thread*, g_user_main_fn,
g_touch_*) → AndroidPlatform struct fields.
- All sx_android_* helpers take `plat: *AndroidPlatform` as first arg.
Render thread receives plat via pthread_create's arg.
- New `logical_w: f32 = 0.0` field. Consumers set it before init() to
define the design width in points; `recompute_scale` derives
`dpi_scale = pixel_w / logical_w` (or 1.0 if unset). Called on
init / set_viewport / egl_init. drain_touches divides incoming
physical pixel coords by dpi_scale so chess sees logical-space
positions matching its layout. Touch lands on the right squares.
Three sx-compiler bugs hit + fixed along the way:
1. Top-level `inline if OS == .X { decls }` body decls were silently
dropped because scanDecls/lowerDecls had no .if_expr arm. New
`flattenComptimeConditionals` pre-pass in src/imports.zig
(threaded via ComptimeContext from core.zig) hoists matching arms
recursively. Regression at examples/124-inline-if-hoist-toplevel.sx.
2. Parser rejected `#import` / `#framework` inside inline-if bodies
because parseStmt in src/parser.zig only had arms for `#insert`.
Added the missing arms. Regression at
examples/123-inline-if-import-in-body.sx (landed earlier).
3. JNI `Call<T>Method` switches in src/ir/emit_llvm.zig (instance /
nonvirtual / static) were missing `.f32` rows — jfloat returns
(e.g. MotionEvent.getX/getY) fell into the silent-undef else arm.
Chess's sx_android_push_touch(plat, getAction(), getX(), getY())
delivered garbage f32 coords to the touch ring, so taps landed
nowhere recognisable. Added `.f32 => Jni.Call{Static,Nonvirtual,}FloatMethod`
rows to all three switches; lifted unsupported-type detection
from emit_llvm into lowerForeignMethodCall with proper
source-spanned diagnostics (`isJniReturnTypeSupported`). Regressions
at examples/ffi-jni-call-10-jfloat-return.sx,
examples/ffi-jni-class-09-multi-float-args.sx,
examples/ffi-jni-call-11-unsupported-return-diag.sx.
Stale-snapshot drift in tests/expected/ffi-objc-call-03-selector-sharing.ir
and ffi-objc-call-06-sret-return.ir picks up the new BuildOptions
accessor extern decls (is_android, set_manifest_path,
set_keystore_path, jni_main_count, jni_main_foreign_path_at,
jni_main_java_source_at). Verified diff is dead-decl-only.
Chess on Pixel 7 Pro: tap on e2 white pawn -> yellow selection +
green dots on legal e3/e4 targets; tap on e4 -> board updates with
1. e4, "Black to move" + "1. e4" in info panel.
zig build && zig build test && bash tests/run_examples.sh -> 145/145
green. bash tests/cross_compile.sh -> 7/7 green.
This commit is contained in:
@@ -19,6 +19,13 @@
|
||||
// (action,x,y) tuple onto a mutex-guarded queue. `poll_events`
|
||||
// drains the queue into the platform's standard `Event` shape.
|
||||
//
|
||||
// State model: every piece of mutable Android-backend state lives on
|
||||
// `AndroidPlatform` (the EGL handles, the ANativeWindow, the render
|
||||
// thread, the touch ring, the frame closure, the user main fn, the
|
||||
// touch mutex). Module-level globals are out — they shadow consumer
|
||||
// globals on `#import` and produced an integer/float-shadowing render
|
||||
// regression on chess before we eliminated them.
|
||||
//
|
||||
// Vulkan-compatible: same ANativeWindow drives `vkCreate*SurfaceKHR`
|
||||
// without changing the lifecycle.
|
||||
|
||||
@@ -117,44 +124,75 @@ eglDestroyContext :: (d: *void, ctx: *void) -> u32 #foreign;
|
||||
eglDestroySurface :: (d: *void, surface: *void) -> u32 #foreign;
|
||||
eglTerminate :: (d: *void) -> u32 #foreign;
|
||||
|
||||
// ── Module-level state ──────────────────────────────────────────────────
|
||||
// ── Touch ring ──────────────────────────────────────────────────────────
|
||||
|
||||
g_activity : *void = null; // global ref to the SxApp jobject (saved env can't outlive scope)
|
||||
g_app_window : *void = null; // ANativeWindow from surfaceCreated
|
||||
g_egl_display : *void = null;
|
||||
g_egl_context : *void = null;
|
||||
g_egl_surface : *void = null;
|
||||
g_egl_config : *void = null;
|
||||
g_viewport_w : s32 = 0;
|
||||
g_viewport_h : s32 = 0;
|
||||
// Defaults to 1.0 until a proper density query lands. Chess's pipeline
|
||||
// uses `viewport_w/h` as the layout space and `dpi_scale` to scale
|
||||
// rendering; mismatches cause layout drift / shrinking.
|
||||
g_dpi_scale : f32 = 1.0;
|
||||
g_should_stop : bool = false;
|
||||
g_render_thread_started : bool = false;
|
||||
g_render_thread : u64 = 0;
|
||||
g_frame_fn : Closure() = ---;
|
||||
g_frame_fn_set : bool = false;
|
||||
|
||||
// Touch event queue. Single-producer (Java UI thread) / single-consumer
|
||||
// (render thread); a small ring buffer guarded by a pthread mutex is
|
||||
// enough — chess only generates touches on user interaction so contention
|
||||
// is rare.
|
||||
TouchEvent :: struct {
|
||||
action: s32;
|
||||
x: f32;
|
||||
y: f32;
|
||||
}
|
||||
|
||||
g_touch_queue : [64]TouchEvent = ---;
|
||||
g_touch_head : u32 = 0;
|
||||
g_touch_tail : u32 = 0;
|
||||
// pthread_mutex_t is 40 bytes on bionic (NDK 26+); over-size to 64 for safety.
|
||||
g_touch_mutex_storage : [64]u8 = ---;
|
||||
g_touch_mutex_inited : bool = false;
|
||||
// ── AndroidPlatform ─────────────────────────────────────────────────────
|
||||
//
|
||||
// Every per-instance piece of state — EGL handles, ANativeWindow, render
|
||||
// thread, touch ring + mutex, frame closure, user main fn — lives here.
|
||||
// No module-level globals: a previous shape had `g_viewport_w : s32` at
|
||||
// module scope, which silently shadowed chess's own
|
||||
// `g_viewport_w : f32` on `#import` and caused the renderer to receive
|
||||
// a logical width cast to s32 instead of the physical pixel width.
|
||||
//
|
||||
// `logical_w` is the consumer's design width in points (e.g. chess sets
|
||||
// 414 to match an iPhone 12 layout). `begin_frame` derives `dpi_scale`
|
||||
// from `pixel_w / logical_w` so the renderer sees the design canvas
|
||||
// regardless of physical density. Touch coords are divided by the same
|
||||
// scale before delivery so layout-side hit-testing matches.
|
||||
|
||||
// ── #jni_main Activity ──────────────────────────────────────────────────
|
||||
AndroidPlatform :: struct {
|
||||
title: [:0]u8 = "";
|
||||
width: s32 = 0;
|
||||
height: s32 = 0;
|
||||
|
||||
// Set by consumer code BEFORE `init` if a fixed design width is
|
||||
// wanted (chess uses 414). When 0, the platform reports
|
||||
// viewport = pixel (1:1 logical/physical) — same as the legacy
|
||||
// hardcoded `dpi_scale = 1.0` behaviour.
|
||||
logical_w: f32 = 0.0;
|
||||
|
||||
// ANativeWindow + EGL state (set up by sx_android_attach_window +
|
||||
// egl_init on the render thread).
|
||||
app_window: *void = null;
|
||||
egl_display: *void = null;
|
||||
egl_context: *void = null;
|
||||
egl_surface: *void = null;
|
||||
egl_config: *void = null;
|
||||
|
||||
// Pixel-size from ANativeWindow_get{Width,Height}; derived dpi_scale
|
||||
// (pixel_w / logical_w when logical_w > 0, else 1.0).
|
||||
pixel_w: s32 = 0;
|
||||
pixel_h: s32 = 0;
|
||||
dpi_scale: f32 = 1.0;
|
||||
|
||||
// Render thread lifecycle. `user_main_fn` is the consumer's `main`
|
||||
// entry, invoked once EGL is current. `should_stop` is checked in
|
||||
// the frame loop; `stop()` flips it.
|
||||
render_thread: u64 = 0;
|
||||
render_thread_started: bool = false;
|
||||
user_main_fn: () -> void = ---;
|
||||
should_stop: bool = false;
|
||||
frame_closure: ?Closure() = null;
|
||||
|
||||
events: List(Event) = .{};
|
||||
|
||||
// Touch ring: single-producer (Java UI thread via sx_android_push_touch)
|
||||
// / single-consumer (render thread via sx_android_drain_touches).
|
||||
// pthread_mutex_t is 40 bytes on bionic (NDK 26+); over-size to 64
|
||||
// for safety. `touch_mutex_inited` tracks lazy init on first use.
|
||||
touch_queue: [64]TouchEvent = ---;
|
||||
touch_head: u32 = 0;
|
||||
touch_tail: u32 = 0;
|
||||
touch_mutex_storage: [64]u8 = ---;
|
||||
touch_mutex_inited: bool = false;
|
||||
}
|
||||
|
||||
// ── User-facing helpers for the consumer's `#jni_main` Activity ────────
|
||||
//
|
||||
@@ -164,11 +202,12 @@ g_touch_mutex_inited : bool = false;
|
||||
// those methods call:
|
||||
//
|
||||
// - `sx_android_forward_assets(env, activity)` from onCreate.
|
||||
// - `sx_android_attach_window(env, holder)` from surfaceCreated.
|
||||
// - `sx_android_detach_window()` from surfaceDestroyed.
|
||||
// - `sx_android_set_viewport(w, h)` from surfaceChanged.
|
||||
// - `sx_android_start_render_thread(main_fn)` once the surface is up.
|
||||
// - `sx_android_push_touch(action, x, y)` from onTouchEvent.
|
||||
// - `sx_android_attach_window(plat, env, holder)` from surfaceCreated.
|
||||
// - `sx_android_detach_window(plat)` from surfaceDestroyed.
|
||||
// - `sx_android_set_viewport(plat, w, h)` from surfaceChanged.
|
||||
// - `sx_android_start_render_thread(plat, main_fn)` once the surface
|
||||
// is up.
|
||||
// - `sx_android_push_touch(plat, action, x, y)` from onTouchEvent.
|
||||
|
||||
// Extract the AAssetManager from the Activity and install it into the
|
||||
// C file_utils so `read_file_bytes` can route through `AAssetManager_open`.
|
||||
@@ -181,67 +220,69 @@ sx_android_forward_assets :: (env: *void, activity: *JContext) {
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the ANativeWindow from a SurfaceHolder. Call this from your
|
||||
// Activity's `surfaceCreated`. The window stays valid until
|
||||
// `sx_android_detach_window` runs (typically in `surfaceDestroyed`).
|
||||
sx_android_attach_window :: (env: *void, holder: *SurfaceHolder) {
|
||||
// Extract the ANativeWindow from a SurfaceHolder and stash it on `plat`.
|
||||
// Call this from your Activity's `surfaceCreated`. The window stays
|
||||
// valid until `sx_android_detach_window` runs (typically in
|
||||
// `surfaceDestroyed`).
|
||||
sx_android_attach_window :: (plat: *AndroidPlatform, env: *void, holder: *SurfaceHolder) {
|
||||
#jni_env(env) {
|
||||
surface := holder.getSurface();
|
||||
g_app_window = ANativeWindow_fromSurface(env, xx surface);
|
||||
plat.app_window = ANativeWindow_fromSurface(env, xx surface);
|
||||
}
|
||||
}
|
||||
|
||||
sx_android_detach_window :: () {
|
||||
if g_app_window != null {
|
||||
ANativeWindow_release(g_app_window);
|
||||
g_app_window = null;
|
||||
sx_android_detach_window :: (plat: *AndroidPlatform) {
|
||||
if plat.app_window != null {
|
||||
ANativeWindow_release(plat.app_window);
|
||||
plat.app_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
sx_android_set_viewport :: (w: s32, h: s32) {
|
||||
g_viewport_w = w;
|
||||
g_viewport_h = h;
|
||||
sx_android_set_viewport :: (plat: *AndroidPlatform, w: s32, h: s32) {
|
||||
plat.pixel_w = w;
|
||||
plat.pixel_h = h;
|
||||
sx_android_recompute_scale(plat);
|
||||
}
|
||||
|
||||
// Start the render thread that brings up EGL on `g_app_window` and calls
|
||||
// the user-supplied `entry_fn` (typically the user's `main`). Safe to
|
||||
// call once after `sx_android_attach_window` has set the window.
|
||||
sx_android_start_render_thread :: (entry_fn: () -> void) {
|
||||
if g_render_thread_started { return; }
|
||||
g_user_main_fn = entry_fn;
|
||||
pthread_create(@g_render_thread, null, sx_android_render_thread_entry, null);
|
||||
g_render_thread_started = true;
|
||||
// Start the render thread that brings up EGL on `plat.app_window` and
|
||||
// calls `entry_fn` (typically the consumer's `main`). Safe to call once
|
||||
// after `sx_android_attach_window` has set the window.
|
||||
sx_android_start_render_thread :: (plat: *AndroidPlatform, entry_fn: () -> void) {
|
||||
if plat.render_thread_started { return; }
|
||||
plat.user_main_fn = entry_fn;
|
||||
pthread_create(@plat.render_thread, null, sx_android_render_thread_entry, xx plat);
|
||||
plat.render_thread_started = true;
|
||||
}
|
||||
|
||||
g_user_main_fn : () -> void = null;
|
||||
|
||||
sx_android_render_thread_entry :: (arg: *void) -> *void {
|
||||
while g_app_window == null and !g_should_stop {
|
||||
plat : *AndroidPlatform = xx arg;
|
||||
while plat.app_window == null and !plat.should_stop {
|
||||
usleep(1000);
|
||||
}
|
||||
if g_should_stop { return null; }
|
||||
if plat.should_stop { return null; }
|
||||
|
||||
if !sx_android_egl_init() {
|
||||
if !sx_android_egl_init(plat) {
|
||||
__android_log_print(6, "sxapp".ptr, "EGL bootstrap failed\n".ptr);
|
||||
return null;
|
||||
}
|
||||
|
||||
if g_user_main_fn != null {
|
||||
g_user_main_fn();
|
||||
if plat.user_main_fn != null {
|
||||
fn := plat.user_main_fn;
|
||||
fn();
|
||||
}
|
||||
null;
|
||||
}
|
||||
|
||||
// Bring up EGL on g_app_window. Sets g_egl_display / g_egl_context /
|
||||
// g_egl_surface and makes the context current. Returns false on any
|
||||
// failure — caller bails on the render thread.
|
||||
sx_android_egl_init :: () -> bool {
|
||||
g_egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if g_egl_display == EGL_NO_DISPLAY { return false; }
|
||||
// Bring up EGL on `plat.app_window`. Sets the egl_* fields and makes
|
||||
// the context current. Returns false on any failure — caller bails on
|
||||
// the render thread.
|
||||
sx_android_egl_init :: (plat: *AndroidPlatform) -> bool {
|
||||
plat.egl_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
|
||||
if plat.egl_display == EGL_NO_DISPLAY { return false; }
|
||||
|
||||
major : s32 = 0;
|
||||
minor : s32 = 0;
|
||||
if eglInitialize(g_egl_display, @major, @minor) == EGL_FALSE { return false; }
|
||||
if eglInitialize(plat.egl_display, @major, @minor) == EGL_FALSE { return false; }
|
||||
|
||||
cfg_attrs : [13]s32 = .{
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
|
||||
@@ -253,50 +294,65 @@ sx_android_egl_init :: () -> bool {
|
||||
EGL_NONE,
|
||||
};
|
||||
num_cfg : s32 = 0;
|
||||
if eglChooseConfig(g_egl_display, @cfg_attrs[0], @g_egl_config, 1, @num_cfg) == EGL_FALSE { return false; }
|
||||
if eglChooseConfig(plat.egl_display, @cfg_attrs[0], @plat.egl_config, 1, @num_cfg) == EGL_FALSE { return false; }
|
||||
if num_cfg < 1 { return false; }
|
||||
|
||||
visual_id : s32 = 0;
|
||||
eglGetConfigAttrib(g_egl_display, g_egl_config, EGL_NATIVE_VISUAL_ID, @visual_id);
|
||||
ANativeWindow_setBuffersGeometry(g_app_window, 0, 0, visual_id);
|
||||
eglGetConfigAttrib(plat.egl_display, plat.egl_config, EGL_NATIVE_VISUAL_ID, @visual_id);
|
||||
ANativeWindow_setBuffersGeometry(plat.app_window, 0, 0, visual_id);
|
||||
|
||||
ctx_attrs : [3]s32 = .{ EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
|
||||
g_egl_context = eglCreateContext(g_egl_display, g_egl_config, EGL_NO_CONTEXT, @ctx_attrs[0]);
|
||||
if g_egl_context == EGL_NO_CONTEXT { return false; }
|
||||
plat.egl_context = eglCreateContext(plat.egl_display, plat.egl_config, EGL_NO_CONTEXT, @ctx_attrs[0]);
|
||||
if plat.egl_context == EGL_NO_CONTEXT { return false; }
|
||||
|
||||
g_egl_surface = eglCreateWindowSurface(g_egl_display, g_egl_config, g_app_window, null);
|
||||
if g_egl_surface == EGL_NO_SURFACE { return false; }
|
||||
plat.egl_surface = eglCreateWindowSurface(plat.egl_display, plat.egl_config, plat.app_window, null);
|
||||
if plat.egl_surface == EGL_NO_SURFACE { return false; }
|
||||
|
||||
if eglMakeCurrent(g_egl_display, g_egl_surface, g_egl_surface, g_egl_context) == EGL_FALSE { return false; }
|
||||
if eglMakeCurrent(plat.egl_display, plat.egl_surface, plat.egl_surface, plat.egl_context) == EGL_FALSE { return false; }
|
||||
|
||||
g_viewport_w = ANativeWindow_getWidth(g_app_window);
|
||||
g_viewport_h = ANativeWindow_getHeight(g_app_window);
|
||||
plat.pixel_w = ANativeWindow_getWidth(plat.app_window);
|
||||
plat.pixel_h = ANativeWindow_getHeight(plat.app_window);
|
||||
sx_android_recompute_scale(plat);
|
||||
true;
|
||||
}
|
||||
|
||||
// Recompute `dpi_scale` from `pixel_w / logical_w`. Called on viewport
|
||||
// changes and after EGL bringup populates pixel_w/h. Falls back to 1.0
|
||||
// when consumer didn't set logical_w (1:1 logical/physical mode).
|
||||
sx_android_recompute_scale :: (plat: *AndroidPlatform) {
|
||||
if plat.logical_w > 0.0 and plat.pixel_w > 0 {
|
||||
plat.dpi_scale = xx plat.pixel_w / plat.logical_w;
|
||||
} else {
|
||||
plat.dpi_scale = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Touch event queue ───────────────────────────────────────────────────
|
||||
|
||||
sx_android_push_touch :: (action: s32, x: f32, y: f32) {
|
||||
sx_android_ensure_touch_mutex();
|
||||
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
|
||||
next := (g_touch_tail + 1) % 64;
|
||||
if next != g_touch_head { // drop on full
|
||||
g_touch_queue[g_touch_tail] = TouchEvent.{ action = action, x = x, y = y };
|
||||
g_touch_tail = next;
|
||||
sx_android_push_touch :: (plat: *AndroidPlatform, action: s32, x: f32, y: f32) {
|
||||
sx_android_ensure_touch_mutex(plat);
|
||||
pthread_mutex_lock(xx @plat.touch_mutex_storage[0]);
|
||||
next := (plat.touch_tail + 1) % 64;
|
||||
if next != plat.touch_head { // drop on full
|
||||
plat.touch_queue[plat.touch_tail] = TouchEvent.{ action = action, x = x, y = y };
|
||||
plat.touch_tail = next;
|
||||
}
|
||||
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
|
||||
pthread_mutex_unlock(xx @plat.touch_mutex_storage[0]);
|
||||
}
|
||||
|
||||
sx_android_drain_touches :: (out: *List(Event)) {
|
||||
sx_android_ensure_touch_mutex();
|
||||
pthread_mutex_lock(xx @g_touch_mutex_storage[0]);
|
||||
while g_touch_head != g_touch_tail {
|
||||
t := g_touch_queue[g_touch_head];
|
||||
g_touch_head = (g_touch_head + 1) % 64;
|
||||
sx_android_drain_touches :: (plat: *AndroidPlatform, out: *List(Event)) {
|
||||
sx_android_ensure_touch_mutex(plat);
|
||||
pthread_mutex_lock(xx @plat.touch_mutex_storage[0]);
|
||||
inv : f32 = if plat.dpi_scale > 0.0 then 1.0 / plat.dpi_scale else 1.0;
|
||||
while plat.touch_head != plat.touch_tail {
|
||||
t := plat.touch_queue[plat.touch_head];
|
||||
plat.touch_head = (plat.touch_head + 1) % 64;
|
||||
// MotionEvent actions: 0=DOWN, 1=UP, 2=MOVE. Map onto chess's
|
||||
// existing mouse Event variants — touch becomes a left-button
|
||||
// mouse on the same screen coords; delta unused on Android.
|
||||
pos : Point = .{ x = t.x, y = t.y };
|
||||
// mouse on the same screen coords. Coords come in as physical
|
||||
// pixels; divide by dpi_scale so layout-side hit-testing
|
||||
// matches its own logical-coord frames.
|
||||
pos : Point = .{ x = t.x * inv, y = t.y * inv };
|
||||
if t.action == 0 {
|
||||
out.append(.mouse_down(.{ position = pos, button = .left }));
|
||||
} else if t.action == 1 {
|
||||
@@ -305,59 +361,75 @@ sx_android_drain_touches :: (out: *List(Event)) {
|
||||
out.append(.mouse_moved(.{ position = pos, delta = .{ x = 0, y = 0 } }));
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(xx @g_touch_mutex_storage[0]);
|
||||
pthread_mutex_unlock(xx @plat.touch_mutex_storage[0]);
|
||||
}
|
||||
|
||||
sx_android_ensure_touch_mutex :: () {
|
||||
if g_touch_mutex_inited { return; }
|
||||
pthread_mutex_init(xx @g_touch_mutex_storage[0], null);
|
||||
g_touch_mutex_inited = true;
|
||||
sx_android_ensure_touch_mutex :: (plat: *AndroidPlatform) {
|
||||
if plat.touch_mutex_inited { return; }
|
||||
pthread_mutex_init(xx @plat.touch_mutex_storage[0], null);
|
||||
plat.touch_mutex_inited = true;
|
||||
}
|
||||
|
||||
// ── AndroidPlatform ─────────────────────────────────────────────────────
|
||||
|
||||
AndroidPlatform :: struct {
|
||||
title: [:0]u8 = "";
|
||||
width: s32 = 0;
|
||||
height: s32 = 0;
|
||||
events: List(Event) = .{};
|
||||
}
|
||||
// ── Platform impl ───────────────────────────────────────────────────────
|
||||
|
||||
impl Platform for AndroidPlatform {
|
||||
init :: (self: *AndroidPlatform, title: [:0]u8, w: s32, h: s32) -> bool {
|
||||
self.title = title;
|
||||
self.width = w;
|
||||
self.height = h;
|
||||
sx_android_recompute_scale(self);
|
||||
true;
|
||||
}
|
||||
|
||||
// NOTE: method order must match the `Platform` protocol declaration
|
||||
// in modules/platform/api.sx. The vtable is built in impl source
|
||||
// order; mismatched order silently routes calls to the wrong method
|
||||
// (chess on Android lost touch entirely because poll_events sat in
|
||||
// begin_frame's slot, etc.).
|
||||
|
||||
run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) {
|
||||
self.frame_closure = frame_fn;
|
||||
// `frame_fn` is expected to call `g_plat.end_frame()` which does
|
||||
// the `eglSwapBuffers` — don't swap again here or the back buffer
|
||||
// is presented twice per render, alternating with the previous
|
||||
// frame's contents → visible flicker.
|
||||
while !self.should_stop {
|
||||
frame_fn();
|
||||
usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
poll_events :: (self: *AndroidPlatform) -> []Event {
|
||||
self.events.len = 0;
|
||||
sx_android_drain_touches(self, @self.events);
|
||||
result : []Event = ---;
|
||||
result.ptr = self.events.items;
|
||||
result.len = self.events.len;
|
||||
result;
|
||||
}
|
||||
|
||||
begin_frame :: (self: *AndroidPlatform) -> FrameContext {
|
||||
// viewport_* is the logical canvas the renderer + layout see —
|
||||
// pixel_w/h divided by dpi_scale. With logical_w unset (1.0
|
||||
// scale) viewport == pixel.
|
||||
inv : f32 = if self.dpi_scale > 0.0 then 1.0 / self.dpi_scale else 1.0;
|
||||
FrameContext.{
|
||||
viewport_w = xx g_viewport_w,
|
||||
viewport_h = xx g_viewport_h,
|
||||
pixel_w = g_viewport_w,
|
||||
pixel_h = g_viewport_h,
|
||||
dpi_scale = g_dpi_scale,
|
||||
viewport_w = xx self.pixel_w * inv,
|
||||
viewport_h = xx self.pixel_h * inv,
|
||||
pixel_w = self.pixel_w,
|
||||
pixel_h = self.pixel_h,
|
||||
dpi_scale = self.dpi_scale,
|
||||
delta_time = 0.016,
|
||||
target_present_time = 0.0,
|
||||
};
|
||||
}
|
||||
|
||||
end_frame :: (self: *AndroidPlatform) {
|
||||
if g_egl_display != null and g_egl_surface != null {
|
||||
eglSwapBuffers(g_egl_display, g_egl_surface);
|
||||
if self.egl_display != null and self.egl_surface != null {
|
||||
eglSwapBuffers(self.egl_display, self.egl_surface);
|
||||
}
|
||||
}
|
||||
|
||||
poll_events :: (self: *AndroidPlatform) -> []Event {
|
||||
self.events.len = 0;
|
||||
sx_android_drain_touches(@self.events);
|
||||
result : []Event = ---;
|
||||
result.ptr = self.events.items;
|
||||
result.len = self.events.len;
|
||||
result;
|
||||
}
|
||||
|
||||
safe_insets :: (self: *AndroidPlatform) -> EdgeInsets {
|
||||
EdgeInsets.{};
|
||||
}
|
||||
@@ -368,30 +440,17 @@ impl Platform for AndroidPlatform {
|
||||
show_keyboard :: (self: *AndroidPlatform) { }
|
||||
hide_keyboard :: (self: *AndroidPlatform) { }
|
||||
|
||||
run_frame_loop :: (self: *AndroidPlatform, frame_fn: Closure()) {
|
||||
g_frame_fn = frame_fn;
|
||||
g_frame_fn_set = true;
|
||||
// `frame_fn` is expected to call `g_plat.end_frame()` which does
|
||||
// the `eglSwapBuffers` — don't swap again here or the back buffer
|
||||
// is presented twice per render, alternating with the previous
|
||||
// frame's contents → visible flicker.
|
||||
while !g_should_stop {
|
||||
frame_fn();
|
||||
usleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
stop :: (self: *AndroidPlatform) {
|
||||
g_should_stop = true;
|
||||
self.should_stop = true;
|
||||
}
|
||||
|
||||
shutdown :: (self: *AndroidPlatform) {
|
||||
if g_egl_display != null {
|
||||
eglMakeCurrent(g_egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if g_egl_surface != null { eglDestroySurface(g_egl_display, g_egl_surface); g_egl_surface = null; }
|
||||
if g_egl_context != null { eglDestroyContext(g_egl_display, g_egl_context); g_egl_context = null; }
|
||||
eglTerminate(g_egl_display);
|
||||
g_egl_display = null;
|
||||
if self.egl_display != null {
|
||||
eglMakeCurrent(self.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
if self.egl_surface != null { eglDestroySurface(self.egl_display, self.egl_surface); self.egl_surface = null; }
|
||||
if self.egl_context != null { eglDestroyContext(self.egl_display, self.egl_context); self.egl_context = null; }
|
||||
eglTerminate(self.egl_display);
|
||||
self.egl_display = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user