Repository navigation
Conversation
nv2a_vertex_shader_to_glsl and nv2a_pixel_shader_to_glsl take a struct nv2a_dialect, which gl_initialize fills from the context, in place of their #ifdef HALO_ANDROID: the #version line, ES's precision qualifiers, the LOD bias each lookup passes where samplers have none (ES), and the clip space conversion glClipControl does on the desktop. The GLSL is the same as before, byte for byte, for every vertex program in the game and 715 pixel shader keys, in each of the three dialects. The first of five steps that move the renderer's OpenGL calls behind a backend interface. Co-Authored-By: Claude Opus 5.5 <[email protected]>
gpu.h is the interface between the Direct3D device and the code that drives the GPU for it, in fixed-width types only, and gpu_gl.c is its OpenGL backend. This step gives it textures, render targets included, and samplers: xbox_textures.c makes, uploads, reads back and drops its textures through gpu_texture_*, the device makes render targets and the water's mip composites the same way, and it describes each texture stage as a struct gpu_stage, which gpu_gl.c binds with its sampler objects. gpu_initialize probes the context and reports what the device needs to know in a struct gpu_capabilities. The GL state cache, the framebuffers and the multisampled renderbuffers move to gpu_gl.c with them. Until the next steps move the device's draws, clears and presentation behind gpu.h, gpu_gl.h declares what the device still uses directly. The GL calls are the same but for a few redundant ones that go: a texture is bound again for each level it uploads, and sampler objects are cached by the sampler state alone, so the high-res HUD's no longer has an identical twin. The GLSL is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The device now describes each draw in full as a struct gpu_draw (gpu.h): its targets and samples a pixel, its shaders, viewport and scissor, depth, stencil, blending and rasterization in neutral terms, its texture stages, its vertex streams and attributes, and its index buffer and primitive. gpu_draw applies the whole packet; nothing carries over from the draw before it but what the GL state cache skips. With it go the vertex streams and the vertex constants. The mirror of the Xbox's contiguous memory writes gpu_buffers, and what a draw streams goes through gpu_stream into the backend's frame buffers. The 192 vertex constants and their change serials, log and checkpoint are a struct gpu_constant_store, whose uploads stay incremental, and the other uniforms are a struct gpu_uniforms with a serial of its own. Shaders are compiled through gpu_shader_create; the programs, their uniform locations, the vertex array objects, the sampler binding and the Intel flush are gpu_gl.c's. A draw whose shaders lit for each pixel do not link is drawn again lit for each vertex, as before. The GL calls are the same but for these: a draw's textures are bound after its uniforms rather than before its targets, an indexed draw with no base vertex calls glDrawElements rather than glDrawElementsBaseVertex with 0, and the desktop finds a draw's vertex array by comparing its layout with the previous draw's rather than keeping one on each vertex declaration. The GLSL is unchanged. Clears, visibility tests and presentation still make GL calls of their own, through gpu_gl.h, until the next step. Co-Authored-By: Claude Opus 5.5 <[email protected]>
gpu_clear takes the targets, what to clear and the rectangles in the targets' pixels; the device still clips the Xbox's rectangles to the viewport and scales them. gpu_present resolves the back buffer, letterboxes it into the window and starts the next frame, which on Android includes copying the visibility counters into the frame's snapshot and taking the next stream buffers once their fence has passed. Screenshots read the back buffer with gpu_texture_read, and the anti-aliasing passes run through gpu_anti_alias_prepare and gpu_anti_alias. The queries, the desktop's mapped result buffer and Android's atomic counters move to gpu_gl.c behind gpu_visibility_begin, gpu_visibility_end and gpu_visibility_result. The device keeps what the game sees: whether a slot has a test, the target's area each count is divided by, and the latest count known, so that an exact count is still turned into the game's pixels and Android's "any sample passed" into a large count, as before. The device makes no GL calls now except to reset the backend's cache once at startup. The GL calls are the same but for the order of a few that do not depend on each other. The GLSL is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Direct3D device reaches the GPU through gpu.h alone: it no longer includes gl.h or gpu_gl.h, its types and names lose GL (gl_ready is gpu_ready, a render target's gl_width and gl_height are its pixel_width and pixel_height), and it leaves resetting the GL state cache to the backend. The last GL calls outside the backend move behind gpu.h too: the high-res HUD's and the menus' PNG textures and the high-res text's atlas are made, filled and dropped through gpu_texture_*, the atlas taking its new glyphs' rows with gpu_texture_upload_rows. The anti-aliasing passes become gpu_gl_post.c, part of the OpenGL backend. gpu_gl.c and gpu_gl_post.c are now the only files that make GL calls, but for sdl_platform.c's log line naming the context it created. The comments and READMEs that named d3d8_gl.c or xgpu_post.c name the new files. The GL calls are unchanged but for a pixel store setting the high-res text's atlas makes once when it is created. The GLSL is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
steverice
marked this pull request as ready for review
October 8, 2026 23:01
Collaborator
|
We arent currently planning to add support for more rendering backends. It adds more moving parts and things that could break. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This splits the renderer in two: the Xbox Direct3D device, which keeps the game's state and translates it, and a GPU backend, which owns every GPU object and makes every GL call.
d3d8_gl.cbecomesd3d8_device.cand makes no GL calls; the GL code moves intogpu_gl.c, behind a new interface,gpu.h. OpenGL stays the only backend, its output is unchanged in every check below, and each of the five commits builds on its own.Note
This code was written by AI. It was tested as described below; I have not reviewed every line myself.
Why now
innerhat is bringing a native Metal renderer for the Mac into this repository. Without a seam, that is a second copy of the device kept in step with
d3d8_gl.cby hand, and every renderer change here has to be made twice. Withgpu.h, a Metal backend implements the interface and the device stays shared: a fix to how Xbox state is translated reaches OpenGL and Metal at once.What changed since this PR was opened
This PR was first cut from
73dc01d0. Main has since changed the renderer in ways the split now carries:web_chunk_get), its vertex array cache and index cache, its texture swizzle and S3TC limits, the feedback-loop copy and the meters' blend applied in the shader (source_blend);HALO_ANDROIDsplit intoHALO_GLESand friends, and the ES base-vertex extensions;Begin;Each of these lives where the split puts it: GL state, buffers and the web's GL caches in
gpu_gl.c; game-facing decisions (which index arrays to keep, when a target is stale, what a draw samples) in the device.gpu.hgrew to match:gpu_buffer_createtakesGPU_BUFFER_INDICES(WebGL fixes a buffer's use on first bind),gpu_stream_alignedkeeps ES attribute pointers stable for immediate draws,gpu_drawhasindex_sizefor 32-bit indices,gpu_stagehassampled, andgpu_visibility_endreports whether a count was kept.The series was rebased commit by commit rather than rewritten, so the five steps are the same as before.
Altogether 36 files, +4,456 −3,221 (with renames detected). Each commit message lists the GL calls whose order or count changes; none changes what is drawn.
Evidence that GL is unchanged
The GLSL, byte for byte. A harness compiles
nv2a_vsh.candnv2a_psh.cfrom a commit and writes every shader for all 67 vertex programs (three packed-attribute masks, plus the per-pixel lit variants) and 2,117 pixel shader keys (117 recorded from play, 2,000 synthetic), each with the key's flags varied: 21,386 shaders for desktop GL 4.5 and for ES 3.0, and 25,620 for ES 3.1. The output matches main's at every commit.Every commit builds with main's own
configure.pyand ninja rules:ninja linux(32-bit x86 against the Debian 11 system root, with LTO and PGO off), the Android guest imagebuild/android/halo_guest.elf(against a stand-in NDK of LLVM and Khronos's GLES headers), andninja webwith emsdk 6.0.10. The tip adds no warning but macOS's file-name-case note for the new file.The web build runs the same.
tools/ci_build.py web release, the page'snode --testsuite andsmoke.mjspass at main and at the tip. In Chromium on Apple-silicon hardware WebGL, with the menus' and Blood Gulch's maps, the main menu frame is identical pixel for pixel at main and at the tip, the network test game on Blood Gulch draws at both, anddebug.gl_debugreports the same single GL error at both (0502at frame 5, before this series).Upstream's tests. The asset-free
pytestset gives the same results at main and at the tip; on macOS the harness tests that need a 32-bit x86 toolchain fail at both.Windows builds and runs the same.
python configure.pyandninja windowsfrom a clean tree build main and every commit with no warnings (LLVM 23.1, Visual Studio Build Tools 2022's x86 libraries, Windows SDK 10.0.26100). On an AMD Radeon RX 7600M XT (OpenGL 4.5 core, driver 26.6.4), with Blood Gulch frozen on one tick through a saved core state (game_speed 0,core_save_name,core_load_name_at_startup), main and the tip render the same frames pixel for pixel, at 30 and at 240 frames a second; the frozen main menu differs only inside the selected item's pulsing highlight, as two runs of the same build do. A debug context (HALO_GL_DEBUG=1) logs no error or warning at either, and all 46 runs exited cleanly.Not checked: running the Linux and Android builds; neither could run here.
What it costs
On the web build, which takes the ES path: three alternating runs each of main and the tip in a Blood Gulch network test game at a steady 120 frames a second, measuring the CPU all of the browser's processes used over the last 80 seconds of each 2-minute run. Main used 0.715, 0.695 and 0.690 CPU seconds per second (mean 0.700), the tip 0.691, 0.690 and 0.700 (mean 0.694): no difference beyond the runs' own spread. On Windows with desktop GL, in a live Blood Gulch scene with the player idle: uncapped, main drew 1,395 frames a second (sd 19) and the tip 1,382 (sd 15), 1.28 and 1.30 ms of CPU a frame; with vsync at 240 Hz, six alternating pairs used 0.53 and 0.57 cores, and the three pairs launched tip first came out equal (0.54 and 0.54). Both within the runs' spread.
The packet is built once per draw from state the device already reads, and the backend's GL state cache still skips redundant calls, so the GL traffic is the same as before.
Not in this PR
No Metal backend, no Apple platform code, nothing that only a fork uses. If the split is wanted, the Mac backend can follow on top of it.