From 5c41e9c180c0806e7467e8d3ff7d97f96e0b880f Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 19 May 2026 09:32:09 +0300 Subject: [PATCH] =?UTF-8?q?gpu:=20Gles3Gpu=20=E2=80=94=20GLES3=20implement?= =?UTF-8?q?ation=20of=20the=20GPU=20protocol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`. --- library/modules/gpu/gles3.sx | 332 +++++++++++++++++++++++++++++++++ library/modules/ui/renderer.sx | 10 +- 2 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 library/modules/gpu/gles3.sx diff --git a/library/modules/gpu/gles3.sx b/library/modules/gpu/gles3.sx new file mode 100644 index 0000000..1f7dfc4 --- /dev/null +++ b/library/modules/gpu/gles3.sx @@ -0,0 +1,332 @@ +// 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; + } + + // One shared VAO with the renderer's 12-float layout. Bound here + // once; set_vertex_buffer just rebinds the VBO against it. + 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); + + 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; } + // 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 = .{ + 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); + } + + 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; } + 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); + } + + 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]; +} diff --git a/library/modules/ui/renderer.sx b/library/modules/ui/renderer.sx index 5ee1921..3d6c185 100755 --- a/library/modules/ui/renderer.sx +++ b/library/modules/ui/renderer.sx @@ -62,7 +62,15 @@ UIRenderer :: struct { // draw reads from its own slice and can outlive earlier in- // flight draws without corruption. metal_buf_size := buf_size * 4; - self.mtl_shader = self.gpu.create_shader(UI_MSL_SRC, ""); + // Backend-specific shader sources: Metal takes a combined MSL + // library with `vmain` + `fmain` entry points; GLES3 takes a + // separate vertex / fragment pair. Caller selects via OS gate. + inline if OS == .android { + self.mtl_shader = self.gpu.create_shader(UI_VERT_SRC_ES, UI_FRAG_SRC_ES); + } + inline if OS == .ios { + self.mtl_shader = self.gpu.create_shader(UI_MSL_SRC, ""); + } self.mtl_vbuf = self.gpu.create_buffer(metal_buf_size); self.mtl_buf_capacity = metal_buf_size; white_px : [4]u8 = .[255, 255, 255, 255];