gpu: Gles3Gpu — GLES3 implementation of the GPU protocol
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`.
This commit is contained in:
332
library/modules/gpu/gles3.sx
Normal file
332
library/modules/gpu/gles3.sx
Normal file
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user