android: AAssetManager bootstrap + APK asset bundling + scissor TODO

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.
This commit is contained in:
agra
2026-05-19 10:09:30 +03:00
parent 5c41e9c180
commit b5bf789b7b
3 changed files with 66 additions and 25 deletions

View File

@@ -31,6 +31,7 @@
// surface/context creation lives in platform/android.sx.
eglGetProcAddress :: (name: [*]u8) -> *void #foreign;
// Functions absent from opengl.sx (it was authored against the SDL
// desktop subset). We don't currently call these — destroy_* is best
// effort here; future work can wire `glDeleteProgram` etc. through
@@ -83,23 +84,11 @@ impl GPU for Gles3Gpu {
self.gl_loaded = true;
}
// One shared VAO with the renderer's 12-float layout. Bound here
// once; set_vertex_buffer just rebinds the VBO against it.
// Create the VAO. Attribute layout is deferred to set_vertex_buffer
// because `glVertexAttribPointer` captures the currently-bound
// GL_ARRAY_BUFFER into the VAO state — we don't know which VBO
// the renderer wants until it tells us via set_vertex_buffer.
glGenVertexArrays(1, @self.vao);
glBindVertexArray(self.vao);
// pos (2 floats)
glVertexAttribPointer(0, 2, GL_FLOAT, 0, 48, xx 0);
glEnableVertexAttribArray(0);
// uv (2 floats)
glVertexAttribPointer(1, 2, GL_FLOAT, 0, 48, xx 8);
glEnableVertexAttribArray(1);
// color (4 floats)
glVertexAttribPointer(2, 4, GL_FLOAT, 0, 48, xx 16);
glEnableVertexAttribArray(2);
// params (4 floats)
glVertexAttribPointer(3, 4, GL_FLOAT, 0, 48, xx 32);
glEnableVertexAttribArray(3);
glBindVertexArray(0);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -135,10 +124,7 @@ impl GPU for Gles3Gpu {
create_shader :: (self: *Gles3Gpu, vsrc: string, fsrc: string) -> ShaderHandle {
inline if OS != .android { return 0; }
// opengl.sx::create_program takes [:0]u8 (NUL-terminated). The
// string literals from the renderer (#string GLSL blocks) are
// already NUL-terminated by the sx string-pool emitter, so the
// cast is safe.
prog := create_program(xx vsrc, xx fsrc);
if prog == 0 { return 0; }
slot : Gles3ShaderSlot = .{
@@ -267,6 +253,18 @@ impl GPU for Gles3Gpu {
if buf == 0 { return; }
glBindVertexArray(self.vao);
glBindBuffer(GL_ARRAY_BUFFER, buf);
// glVertexAttribPointer captures the currently-bound
// GL_ARRAY_BUFFER into the VAO state. Reconfigure each frame
// so the VAO knows about the live buffer; otherwise drawing
// sources attribs from the zero-buffer (invisible geometry).
glVertexAttribPointer(0, 2, GL_FLOAT, 0, 48, xx 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 2, GL_FLOAT, 0, 48, xx 8);
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 4, GL_FLOAT, 0, 48, xx 16);
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 4, GL_FLOAT, 0, 48, xx 32);
glEnableVertexAttribArray(3);
}
set_texture :: (self: *Gles3Gpu, slot: u32, handle: TextureHandle) {
@@ -305,10 +303,12 @@ impl GPU for Gles3Gpu {
set_scissor :: (self: *Gles3Gpu, x: s32, y: s32, w: s32, h: s32) {
inline if OS != .android { return; }
glEnable(GL_SCISSOR_TEST);
// GL scissor origin is bottom-left; renderer.sx feeds top-left
// pixel coordinates, so flip Y.
glScissor(x, self.pixel_h - (y + h), w, h);
// TODO: re-enable once we figure out why the renderer passes
// a 0×0 clip rect on Android (chess's ScrollView path). The
// bounds the renderer feeds us land outside the framebuffer
// and clip everything off-screen.
// glEnable(GL_SCISSOR_TEST);
// glScissor(x, self.pixel_h - (y + h), w, h);
}
disable_scissor :: (self: *Gles3Gpu) {

View File

@@ -85,14 +85,45 @@ TimeSpec :: struct { sec: s64; nsec: s64; }
g_android_app : *void = null;
g_android_plat : *AndroidPlatform = null;
// `app->activity` (ANativeActivity*) at byte 24.
// `activity->assetManager` at byte 64 inside ANativeActivity.
APP_OFF_activity :s64: 24;
ACTIVITY_OFF_assetManager :s64: 64;
ACTIVITY_OFF_internalData :s64: 32;
// AAssetManager handle the user's `android_main` stashes via the
// bootstrap. Consumers that want to read bundled APK assets (font.ttf,
// piece sprites, level data, ...) read this and feed it into their own
// file-IO shim — e.g. chess wires it through `vendors/file_utils.c`
// which routes `read_file_bytes` through AAssetManager_open on Android.
g_android_asset_manager : *void = null;
g_android_internal_path : *u8 = null;
// ── Bootstrap (called by user's `android_main`) ────────────────────────
// Stashes the NDK app pointer the OS handed to `android_main(app)` so
// the rest of the platform module can find it. Single responsibility —
// the user's `android_main` calls this once, then calls their own
// `main()` to enter the normal cross-platform setup flow.
//
// Also extracts `AAssetManager*` and `internalDataPath` from the
// ANativeActivity that the app pointer carries — these are the two
// pieces consumers need to touch APK assets / per-app storage on
// Android. Reachable as `g_android_asset_manager` / `g_android_internal_path`.
sx_android_bootstrap :: (app: *void) {
g_android_app = app;
inline if OS == .android {
g_android_app = app;
base : s64 = xx app;
activity_pp : **void = xx (base + APP_OFF_activity);
activity_ptr := activity_pp.*;
if activity_ptr != null {
act_base : s64 = xx activity_ptr;
mgr_pp : **void = xx (act_base + ACTIVITY_OFF_assetManager);
g_android_asset_manager = mgr_pp.*;
path_pp : **u8 = xx (act_base + ACTIVITY_OFF_internalData);
g_android_internal_path = path_pp.*;
}
}
}
// ── Helpers ────────────────────────────────────────────────────────────

View File

@@ -331,6 +331,16 @@ pub fn createApk(allocator: std.mem.Allocator, io: std.Io, so_path: []const u8,
// and zip is on every macOS/Linux host by default.
try runProcessIn(allocator, io, stage, &.{ "zip", "-q", "-r", unaligned, "lib/" });
// Bundle the project's `./assets/` directory (if present) at the APK's
// top level so AAssetManager_open(path) at runtime can read them.
// Resolves relative to the user's CWD at invocation time — matches the
// convention chess uses (assets/ next to main.sx).
if (std.Io.Dir.openDir(.cwd(), io, "assets", .{})) |dir_handle| {
var dh = dir_handle;
dh.close(io);
try runProcess(allocator, io, &.{ "zip", "-q", "-r", unaligned, "assets/" });
} else |_| {}
// zipalign → aligned apk.
const aligned = try std.fmt.allocPrint(allocator, "{s}.aligned", .{apk_path});
try runProcess(allocator, io, &.{ zipalign, "-f", "4", unaligned, aligned });