Files
sx/library/modules/gpu/gles3.sx
agra b5bf789b7b 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.
2026-05-19 10:09:30 +03:00

333 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// GLES3 backend for the GPU protocol. Android today.
//
// We use opengl.sx's runtime-loaded function-pointer variables for every
// GL call, populated via `load_gl(@eglGetProcAddress)` in `init()`.
// `#foreign` decls for the same names would collide with opengl.sx's
// global variables and silently win — that produces NULL function-
// pointer calls (PC=0 crash) because the variables are uninitialized.
//
// Conventions:
// - One shared VAO with the renderer's fixed 12-float layout
// (pos2 / uv2 / color4 / params4 = 48 bytes, attribute locations 0-3).
// `set_vertex_buffer` rebinds the VBO against this VAO each time.
// - `set_vertex_constants(slot=1, data, 64)` is treated as the 4x4
// projection matrix and uploaded via `glUniformMatrix4fv(uProj)`,
// matching the renderer.sx shader contract. Other slots are no-ops
// for now.
// - `set_texture(slot=0, tex)` binds texture unit 0 and sets
// `uniform sampler2D uTex` to 0. Other slots are no-ops.
// - The shader sources passed to `create_shader(vsrc, fsrc)` must be
// a GLES vertex + fragment pair (NOT the Metal MSL combined source
// the Metal backend takes — caller branches on OS).
#import "modules/std.sx";
#import "modules/compiler.sx";
#import "modules/opengl.sx";
#import "modules/gpu/types.sx";
#import "modules/gpu/api.sx";
// EGL bootstrap helper — used to populate opengl.sx's fn pointers once
// the EGL context is current. We declare only the loader here; EGL
// 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
// load_gl if needed.
// ── State + resource tables ────────────────────────────────────────────
Gles3TextureSlot :: struct {
tex: u32 = 0;
bytes_per_pixel: u32 = 0;
}
Gles3ShaderSlot :: struct {
program: u32 = 0;
proj_loc: s32 = 0 - 1;
tex_loc: s32 = 0 - 1;
}
Gles3Gpu :: struct {
pixel_w: s32 = 0;
pixel_h: s32 = 0;
// The renderer's vertex layout is fixed (see file header). One VAO,
// reused across every set_vertex_buffer.
vao: u32 = 0;
current_shader: u32 = 0;
gl_loaded: bool = false;
shaders: List(Gles3ShaderSlot) = .{};
buffers: List(u32) = .{};
textures: List(Gles3TextureSlot) = .{};
}
// ── GPU impl ───────────────────────────────────────────────────────────
impl GPU for Gles3Gpu {
// EGL is owned by AndroidPlatform; init() here populates the GL
// function pointers (via opengl.sx's load_gl) and sets up the
// single shared VAO. Must be called once the EGL context is
// current (AndroidPlatform.run_frame_loop ensures this before
// invoking the user's per-frame closure). `target` is unused.
init :: (self: *Gles3Gpu, target: *void, pixel_w: s32, pixel_h: s32) -> bool {
inline if OS != .android { return false; }
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
if !self.gl_loaded {
load_gl(@eglGetProcAddress);
self.gl_loaded = true;
}
// 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);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
true;
}
shutdown :: (self: *Gles3Gpu) {
// GLES objects clean up when the EGL context is destroyed (which
// happens when the NativeActivity tears down).
}
resize :: (self: *Gles3Gpu, pixel_w: s32, pixel_h: s32) {
self.pixel_w = pixel_w;
self.pixel_h = pixel_h;
}
// The render-target setup (eglMakeCurrent + glViewport) lives in
// AndroidPlatform.run_frame_loop; begin_frame here just clears the
// active framebuffer. Returns true so callers don't bail out.
begin_frame :: (self: *Gles3Gpu, clear: ClearColor) -> bool {
inline if OS != .android { return false; }
glViewport(0, 0, self.pixel_w, self.pixel_h);
glClearColor(clear.r, clear.g, clear.b, clear.a);
glClear(GL_COLOR_BUFFER_BIT);
true;
}
// eglSwapBuffers happens in AndroidPlatform.end_frame; nothing for us.
end_frame :: (self: *Gles3Gpu, target_time: f64) { }
// ── Resources ────────────────────────────────────────────────────────
create_shader :: (self: *Gles3Gpu, vsrc: string, fsrc: string) -> ShaderHandle {
inline if OS != .android { return 0; }
prog := create_program(xx vsrc, xx fsrc);
if prog == 0 { return 0; }
slot : Gles3ShaderSlot = .{
program = prog,
proj_loc = glGetUniformLocation(prog, "uProj".ptr),
tex_loc = glGetUniformLocation(prog, "uTex".ptr),
};
self.shaders.append(slot);
xx self.shaders.len;
}
create_buffer :: (self: *Gles3Gpu, size_bytes: s64) -> BufferHandle {
inline if OS != .android { return 0; }
if size_bytes <= 0 { return 0; }
b : u32 = 0;
glGenBuffers(1, @b);
glBindBuffer(GL_ARRAY_BUFFER, b);
glBufferData(GL_ARRAY_BUFFER, xx size_bytes, null, GL_DYNAMIC_DRAW);
self.buffers.append(b);
xx self.buffers.len;
}
update_buffer :: (self: *Gles3Gpu, handle: BufferHandle, data: *void, size_bytes: s64) {
inline if OS != .android { return; }
buf := gles3_lookup_buffer(self, handle);
if buf == 0 { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
glBindBuffer(GL_ARRAY_BUFFER, buf);
glBufferSubData(GL_ARRAY_BUFFER, 0, xx size_bytes, data);
}
update_buffer_at :: (self: *Gles3Gpu, handle: BufferHandle, data: *void, size_bytes: s64, byte_offset: s64) {
inline if OS != .android { return; }
buf := gles3_lookup_buffer(self, handle);
if buf == 0 { return; }
if data == null { return; }
if size_bytes <= 0 { return; }
if byte_offset < 0 { return; }
glBindBuffer(GL_ARRAY_BUFFER, buf);
glBufferSubData(GL_ARRAY_BUFFER, xx byte_offset, xx size_bytes, data);
}
create_texture :: (self: *Gles3Gpu, w: s32, h: s32, format: TextureFormat, pixels: *void) -> TextureHandle {
inline if OS != .android { return 0; }
if w <= 0 { return 0; }
if h <= 0 { return 0; }
internal_fmt : s32 = 0;
ext_fmt : u32 = 0;
bpp : u32 = 0;
if format == .rgba8 {
internal_fmt = xx GL_RGBA;
ext_fmt = GL_RGBA;
bpp = 4;
} else {
internal_fmt = xx GL_R8;
ext_fmt = GL_RED;
bpp = 1;
}
t : u32 = 0;
glGenTextures(1, @t);
glBindTexture(GL_TEXTURE_2D, t);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, internal_fmt, w, h, 0, ext_fmt, GL_UNSIGNED_BYTE, pixels);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, xx GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, xx GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, xx GL_CLAMP_TO_EDGE);
slot : Gles3TextureSlot = .{ tex = t, bytes_per_pixel = bpp };
self.textures.append(slot);
xx self.textures.len;
}
update_texture_region :: (self: *Gles3Gpu, handle: TextureHandle, x: s32, y: s32, w: s32, h: s32, pixels: *void) {
inline if OS != .android { return; }
if handle == 0 { return; }
h64 : s64 = xx handle;
if h64 > self.textures.len { return; }
slot := self.textures.items[handle - 1];
if slot.tex == 0 { return; }
if pixels == null { return; }
if w <= 0 { return; }
if h <= 0 { return; }
ext_fmt : u32 = if slot.bytes_per_pixel == 4 then GL_RGBA else GL_RED;
glBindTexture(GL_TEXTURE_2D, slot.tex);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, ext_fmt, GL_UNSIGNED_BYTE, pixels);
}
// glDeleteProgram / glDeleteBuffers aren't in opengl.sx's loader yet.
// For now destroy_* is a no-op on Android — resources free on EGL
// context teardown. Worth adding the loader entries when atlas grow
// starts to matter in practice.
destroy_shader :: (self: *Gles3Gpu, sh: ShaderHandle) { }
destroy_buffer :: (self: *Gles3Gpu, buf: BufferHandle) { }
destroy_texture :: (self: *Gles3Gpu, tex: TextureHandle) {
inline if OS != .android { return; }
if tex == 0 { return; }
h64 : s64 = xx tex;
if h64 > self.textures.len { return; }
t := self.textures.items[tex - 1].tex;
if t == 0 { return; }
glDeleteTextures(1, @t);
self.textures.items[tex - 1].tex = 0;
}
// ── Per-draw state ───────────────────────────────────────────────────
set_shader :: (self: *Gles3Gpu, handle: ShaderHandle) {
inline if OS != .android { return; }
if handle == 0 { return; }
h64 : s64 = xx handle;
if h64 > self.shaders.len { return; }
prog := self.shaders.items[handle - 1].program;
if prog == 0 { return; }
glUseProgram(prog);
self.current_shader = handle;
}
set_vertex_buffer :: (self: *Gles3Gpu, handle: BufferHandle) {
inline if OS != .android { return; }
buf := gles3_lookup_buffer(self, handle);
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) {
inline if OS != .android { return; }
if slot != 0 { return; } // renderer only uses unit 0
if handle == 0 { return; }
h64 : s64 = xx handle;
if h64 > self.textures.len { return; }
t := self.textures.items[handle - 1].tex;
if t == 0 { return; }
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, t);
if self.current_shader != 0 {
cs64 : s64 = xx self.current_shader;
if cs64 <= self.shaders.len {
loc := self.shaders.items[self.current_shader - 1].tex_loc;
if loc >= 0 { glUniform1i(loc, 0); }
}
}
}
// For slot 1 + 64 bytes the renderer is uploading the 4x4 projection
// matrix into `uniform mat4 uProj`. Look up via the current shader's
// cached uniform location.
set_vertex_constants :: (self: *Gles3Gpu, slot: u32, data: *void, size_bytes: s64) {
inline if OS != .android { return; }
if slot != 1 { return; }
if size_bytes != 64 { return; }
if self.current_shader == 0 { return; }
cs64 : s64 = xx self.current_shader;
if cs64 > self.shaders.len { return; }
loc := self.shaders.items[self.current_shader - 1].proj_loc;
if loc < 0 { return; }
glUniformMatrix4fv(loc, 1, 0, xx data);
}
set_scissor :: (self: *Gles3Gpu, x: s32, y: s32, w: s32, h: s32) {
inline if OS != .android { return; }
// 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) {
inline if OS != .android { return; }
glDisable(GL_SCISSOR_TEST);
}
draw_triangles :: (self: *Gles3Gpu, vertex_offset: s32, vertex_count: s32) {
inline if OS != .android { return; }
if vertex_count <= 0 { return; }
glDrawArrays(GL_TRIANGLES, vertex_offset, vertex_count);
}
}
gles3_lookup_buffer :: (self: *Gles3Gpu, handle: u32) -> u32 {
inline if OS != .android { return 0; }
if handle == 0 { return 0; }
h64 : s64 = xx handle;
if h64 > self.buffers.len { return 0; }
self.buffers.items[handle - 1];
}