This report summarizes the local Dreamcast rendering/tooling fork work since the recorded clone baselines. It is written for maintainers of the Dreamcast raylib/GLdc ecosystem who need to understand what changed, why it changed, and which parts are broadly reusable versus downstream integration glue.
The short version: the forks move the Dreamcast path away from many tiny immediate-mode helper submissions and toward fewer, larger, state-coherent array submissions that GLdc can process through its fastest paths. A small offline mesh format (.dcmesh) was added to pre-stripify static model geometry. GLdc was instrumented and tuned around the same fast path assumptions. SH4ZAM support exists, but the TODO plan correctly treats it as incomplete and in need of a staged follow-up.
The Dreamcast rendering stack used here is:
raylib helpers -> rlgl -> GLdc -> KallistiOS -> PowerVR2/CLX2
The main problem addressed by these forks is that stock raylib helper code is very portable, but on the Dreamcast GL 1.1 backend it tends to produce small immediate-style draws:
rlSetTexture(id) -> rlBegin(RL_QUADS) -> vertices -> rlEnd() -> rlSetTexture(0)
That is harmless on modern backends with a strong internal batcher, but expensive on GLdc. Each tiny helper draw can become a full GLdc submission: dirty-state checks, possible polygon header rebuild, transform load, vertex gather/transform/copy, near-plane clipping, and PVR list buffering. The local work therefore attacks the cost at three layers:
raylib-dc: capture simple rlgl immediate traffic on Dreamcast and submit it as largerglDrawArrays()batches, with deferred texture unbinds and strict flush boundaries for correctness.GLdc: add counters to prove where time goes, specialize the common position+UV+color array layout, reduce clipping/submission overhead, and expose a few KOS/PVR-specific APIs required by the downstream renderer.dcmesh: precompute triangle strips and a Dreamcast-friendly 24-byte vertex layout offline, then let raylib-dc transparently route sidecar-backed model meshes through a strip path.
This aligns with the DCDocs rendering research: the highest-confidence optimization is not rewriting application code, but adding a Dreamcast-specific rlgl layer that batches immediate helper traffic and feeds GLdc's array fast paths. DCDocs also records a measured saving of up to roughly 3 ms/frame for helper-heavy scenes after this style of batching.
Remote:
https://github.com/ninjadynamics/raylib.git
Clone baseline from reflog:
65cfa062e5207f0105b9b41f2ec17967c5299cd4
Subject: Update custom build
Commit date: 2026-04-21
Clone event: 2026-04-27
Current local head:
ba16cf013
Subject: Update initialization log timestamp in InitWindow function
Branch: master, tracking origin/master
Diff size from clone baseline:
10 files changed, 8358 insertions(+), 6885 deletions(-) raw diff
10 files changed, 1503 insertions(+), 30 deletions(-) with whitespace ignored
The whitespace-ignored number is more representative. rmodels.c has a very large raw textual diff, but the meaningful changes are small and concentrated in model load/unload/upload/draw hook points.
Commits after the clone baseline:
| Commit | Date | Subject |
|---|---|---|
b929d610a |
2026-04-28 | Gainz! |
c266422de |
2026-04-29 | Add dcmesh path |
229e60c00 |
2026-04-29 | Implement UploadMesh hook to sync DCMesh data and update geometry handling |
469a16dd5 |
2026-04-29 | Add SH4ZAM support for accelerated math operations and logging |
07d827a2e |
2026-05-04 | Improve performance |
8ce7244e6 |
2026-05-15 | Add alignment attribute for Matrix struct on SH4 architecture and update initialization log timestamp |
ff1e97984 |
2026-05-23 | Update initialization log timestamp in InitWindow function |
b6aeab2ee |
2026-05-24 | Add GLdc mipmap generation support for Dreamcast |
7cadb4e69 |
2026-06-12 | Refactor Matrix struct alignment and improve unaligned loading for SH4 architecture |
744eb2a10 |
2026-06-12 | Update initialization log timestamp in InitWindow function and add re-upload guard for mesh in UploadMesh |
a7e1b4a50 |
2026-06-15 | Update initialization log timestamp in InitWindow function |
ba16cf013 |
2026-06-15 | Update initialization log timestamp in InitWindow function |
Changed files:
| File | Main reason |
|---|---|
src/rlgl_dc_batch.h |
New Dreamcast immediate-mode batcher and deferred texture unbind layer. |
src/rlgl.h |
Dreamcast hooks for the batcher, flush triggers, texture state coalescing, GLdc mipmap call path. |
src/dc_mesh.c |
New Dreamcast runtime loader/drawer for .dcmesh sidecar strip data. |
src/dc_mesh.h |
Public DCMesh runtime API. |
src/dcmesh.h |
Shared .dcmesh file/runtime format. |
src/rmodels.c |
Transparent LoadModel/UnloadModel/UploadMesh/DrawMesh hooks for DCMesh. |
src/raymath.h |
Optional SH4ZAM acceleration paths and alignment-safe matrix handling. |
src/rcore.c |
Local canary/logging for modified raylib and SH4ZAM status. |
src/Makefile |
Builds dc_mesh.o; adds USE_SH4ZAM flag/link option. |
src/CMakeLists.txt |
Adds DCMesh sources/headers to CMake build metadata. |
Remote:
https://github.com/ninjadynamics/GLdc.git
Clone baseline from reflog:
ba10615722b21bd60f527676f0218d6adc6599fd
Subject: Allow compiling containers on Xbox
Commit date: 2026-02-15
Clone event: 2026-04-27
Current local head:
e99062a
Subject: Add deferred fog functionality
Branch: master, tracking origin/master
Diff size from clone baseline:
12 files changed, 578 insertions(+), 19 deletions(-)
Commits after the clone baseline:
| Commit | Date | Subject |
|---|---|---|
1872a8f |
2026-04-28 | Gainz! |
4ffd9f7 |
2026-04-28 | Enhance GLdc performance instrumentation with expanded statistics and improved clipping metrics |
dc028cc |
2026-04-29 | Add SH4ZAM support for Dreamcast math optimizations |
6f1b285 |
2026-05-01 | Refactor GPU initialization and enhance vertex processing for improved performance |
37fb5f3 |
2026-05-04 | Improve init |
38fe083 |
2026-05-05 | Refactor blending logic in apply_poly_header |
fab3bd2 |
2026-05-23 | Update GLdc welcome message with latest Git revision date |
2051005 |
2026-06-12 | Update welcome message with latest Git revision and add out-of-memory check in texture conversion |
a25cf62 |
2026-06-15 | Update welcome message with latest Git revision date |
c301cfb |
2026-06-15 | Fix punch-through srd/dst blend mode |
78d6044 |
2026-06-15 | Update welcome message with latest Git revision time |
06e825a |
2026-06-16 | Add render-to-texture functionality and update welcome message with latest Git revision |
e99062a |
2026-06-17 | Add deferred fog functionality |
Changed files:
| File | Main reason |
|---|---|
GL/gldc_stats.h |
New optional stats structure and macros. |
GL/gldc_stats.c |
New reset/get/print implementation for stats. |
GL/draw.c |
Counters, position+UV+color fast path, punch-through blend fix, draw/submit accounting. |
GL/immediate.c |
Counters for immediate begin/end/vertex traffic. |
GL/state.c |
Counter for dirty-state events. |
GL/texture.c |
Texture bind counter, render-to-texture data accessor, conversion OOM guard. |
GL/flush.c |
Startup canary, glKosFlushToTexture(), list clear after texture flush. |
GL/platforms/sh4.c |
Deferred fog application, render-to-texture scene begin, clipping/submission optimizations and counters. |
GL/matrix.c |
Limited SH4ZAM use for normal-matrix inverse/transpose and 32-byte-aligned matrix storage. |
GL/platform.h |
Declares SceneBeginToTexture(). |
include/GL/glkos.h |
Public APIs for deferred fog, flush-to-texture, and texture data lookup. |
CMakeLists.txt |
Adds USE_SH4ZAM option and links sh4zam for Dreamcast builds. |
Remote:
git@github.com:ninjadynamics/dcmesh.git
Initial baseline:
86bc187ed054b5835dac2772f495ff5b95f21da8
Subject: Initial commit
Commit date: 2026-04-29
Current local head:
a6adf79
Subject: Fix path references in gen_meshes script for asset locations
Branch: master, tracking origin/master
Diff size from initial baseline:
4 files changed, 68 insertions(+), 18 deletions(-)
Commits after the initial baseline:
| Commit | Date | Subject |
|---|---|---|
ff7c8f5 |
2026-04-29 | Add .gitattributes file to manage line endings |
ac32d43 |
2026-04-29 | Enhance DCMesh: add vertex mapping, update version, and include node transformations |
849609d |
2026-04-29 | Fix comment: update converter name in dcmesh.h documentation |
95c65c9 |
2026-04-29 | Fix comments: update file name references from glb_to_dcmesh to dcmesh in dcmesh.c |
6fd5601 |
2026-06-17 | Add logo.glb processing to gen_meshes script |
a6adf79 |
2026-06-17 | Fix path references in gen_meshes script for asset locations |
Changed files:
| File | Main reason |
|---|---|
.gitattributes |
Normalizes line endings. |
src/dcmesh.h |
Format version 2, vertex_map, documentation updates. |
src/dcmesh.c |
Node transform baking, vertex map output, format v2 writing. |
gen_meshes |
Convenience script for generating downstream .dcmesh assets. |
The local changes line up with the DCDocs analysis of raylib/GLdc performance on Dreamcast:
- DCDocs overview: https://github.com/ninjadynamics/DCDocs/tree/main
- Dreamcast rlgl batcher notes: https://raw.githubusercontent.com/ninjadynamics/DCDocs/main/raylib_dreamcast_batcher.md
- GLdc performance notes: https://raw.githubusercontent.com/ninjadynamics/DCDocs/main/gldc_performance_modifications.md
- Rendering implementation plan: https://raw.githubusercontent.com/ninjadynamics/DCDocs/main/deep-research-implementation.md
Those docs describe the same architectural bottleneck visible in the code: raylib helper traffic is too granular for a Dreamcast OpenGL 1.1 backend, while GLdc already has useful array fast paths if rlgl feeds it bigger, cleaner submissions.
New file:
src/rlgl_dc_batch.h
Integrated into:
src/rlgl.h
The batcher is Dreamcast-only and guarded by PLATFORM_DREAMCAST. It captures rlgl immediate-mode drawing for:
RL_QUADSRL_TRIANGLES
It intentionally does not capture lines, points, or unusual attribute layouts. Unsupported modes flush pending batched geometry and fall through to the existing GL path.
The captured vertex format is:
| Field | Type | Size |
|---|---|---|
| position | 3 x float |
12 bytes |
| texcoord | 2 x float |
8 bytes |
| color | BGRA bytes | 4 bytes |
Total: 24 bytes per vertex.
That format is chosen because it matches the GLdc fast path expectations for client arrays:
- vertex: 3 x
GL_FLOAT - UV: 2 x
GL_FLOAT - color:
GL_BGRAxGL_UNSIGNED_BYTE - normals disabled
Capacity is 4080 vertices, with a 120 vertex overflow margin. 4080 is divisible by both 3 and 4, so capacity flushes land on clean triangle/quad boundaries. The overflow margin catches unusually large single begin/end blocks without immediately corrupting primitive grouping.
The OpenGL 1.1 path now includes:
<GL/glext.h>for Dreamcast, so GLdc'sglGenerateMipmap()and related declarations are visible.rlgl_dc_batch.hunderPLATFORM_DREAMCAST.- Matrix mutation flush triggers before
rlFrustum,rlOrtho,rlPushMatrix,rlPopMatrix,rlLoadIdentity,rlTranslatef,rlRotatef,rlScalef, andrlMultMatrixf. rlBegin()capture for quads/triangles.rlEnd()no-op behavior for captured draws, keeping the batch open.rlVertex2i,rlVertex2f, andrlVertex3fappend into the batch when active.rlTexCoord2fandrlColor*update the batcher's current attributes.rlNormal3fis ignored for captured fast-path geometry.rlSetTexture()uses batcher-aware texture tracking on Dreamcast.- Direct array draws (
rlDrawVertexArray,rlDrawVertexArrayElements) flush the batch first to preserve ordering. - Blend, depth, depth-mask, cull, scissor, color-mask, and related state changes flush the batch first.
- Frame-end path flushes outstanding batch data before the frame is handed off.
raylib helpers often clean up after themselves with rlSetTexture(0). On desktop this is ordinary hygiene. On Dreamcast it dirties GLdc state and can force extra header work.
The batcher changes the Dreamcast meaning of rlSetTexture(0) for captured traffic:
rlSetTexture(0)records a pending unbind instead of immediately disabling texturing.- If the next captured draw uses the same texture, the pending unbind is cancelled.
- If the next draw uses another texture or needs an untextured state, the old batch is flushed and the state transition is applied at a correctness boundary.
This preserves raylib-visible draw ordering while eliminating redundant bind/unbind churn in common helper paths.
The batcher flushes when a later operation would make already-buffered vertices render with the wrong state:
- texture changes
- draw mode changes
- matrix changes
- GL state changes that affect GLdc polygon headers
- direct array draws
- frame boundaries
- non-captured primitive modes
- capacity boundaries
The conservative choice is to flush on matrix mutation rather than pre-transform vertices. That is not the maximum possible batching, but it is much safer because GLdc loads transform state once per submission. Future work can consider pre-transforming vertices only if profiling says matrix flushes remain a real bottleneck.
The batcher is the largest raylib-dc architectural change because it addresses the mismatch between raylib helper style and Dreamcast backend cost. It keeps the high-level raylib API unchanged and leaves PC/web/other backends untouched. It also preserves GLdc as the renderer, rather than replacing it with a separate application-specific renderer.
The intended win is multiplicative with GLdc's changes:
- rlgl emits fewer GLdc submissions.
- deferred unbinds reduce dirty-state events and header churn.
- GLdc's P+UV+color path processes each remaining large submission more cheaply.
New files:
src/dc_mesh.csrc/dc_mesh.hsrc/dcmesh.h
Hooked into:
src/rmodels.csrc/Makefilesrc/CMakeLists.txt
The DCMesh runtime looks for a .dcmesh sidecar next to a loaded .glb/model file. If found, it loads pre-expanded triangle-strip vertices and links each raylib Mesh to the corresponding DCMesh submesh.
The integration is intentionally transparent:
LoadModel()uploads normal raylib mesh data as before, then attempts to load the.dcmeshsidecar.UnloadModel()frees DCMesh registry data before normal model cleanup.DrawMesh()checks whether a mesh has DCMesh strip data; if so, it routes throughdcMeshDraw().UploadMesh()checks whether the mesh is DCMesh-backed; if so, it syncs raylib vertex/color arrays into DCMesh strip vertices and returns without doing the normal upload body.
On the Dreamcast GL 1.1 path, raylib's Mesh.vaoId is not used for a real VAO. The fork uses it as a tagged DCMesh registry handle:
0xDC000000 | (registry_index << 8) | submesh_index
This avoids adding fields to raylib's public Mesh structure. It is pragmatic, but it is a portability-sensitive choice: if a future Dreamcast backend uses real VAOs or another meaning for vaoId, this would need a different association mechanism.
dcMeshDraw() resolves the tagged vaoId, selects the corresponding submesh, and calls a strip draw path:
- binds the material's diffuse texture
- enables vertex, UV, and color client arrays
- disables normals
- points the arrays at the interleaved DCMesh vertex buffer
- pushes the model transform
- submits each precomputed strip with
glDrawArrays(GL_TRIANGLE_STRIP, first_vertex, vertex_count) - restores client state and blend state
This avoids runtime index parsing and avoids rebuilding strips on the Dreamcast. It also feeds GLdc a narrow, predictable layout.
The DCMesh runtime has ENABLE_PATCH_E, defaulting to on for Dreamcast. In this local code, "Patch E" means: if a DCMesh submesh is known opaque and uses a fully opaque diffuse material, temporarily disable blending so GLdc routes it to the opaque PVR list instead of the transparent list, then restore the caller's blend state.
This is worth calling out because DCDocs also uses "Patch E" for a more ambitious possible direct PVR submission path. The local raylib-dc DCMesh "Patch E" is not that full direct-PVR bypass. It is a narrower opaque-list routing optimization layered on top of GLdc.
The sidecar contains strip-expanded vertices, but raylib still owns the canonical mesh arrays that downstream code may edit. The runtime therefore adds dcMeshHandleUpload():
- If the mesh is DCMesh-backed, sync positions and colors from raylib arrays into strip-expanded DCMesh vertices.
- Use
vertex_mapto map each expanded strip vertex back to its source raylib vertex. - Return
1so the caller skips normalUploadMesh().
That keeps runtime mesh edits, recentering, or color changes from silently diverging between raylib's mesh and the DCMesh sidecar.
There is a small risk marker in this area: dcMeshSyncFromRaylib() still has a TODO around red/blue color channel swapping. That should be resolved before upstreaming the format as a stable API.
The runtime adds:
dcMeshRecenterGeometry(Model *model, float offsetX, float offsetY, float offsetZ)dcMeshUploadSafe(Mesh *mesh, bool dynamic)
dcMeshRecenterGeometry() applies the same recentering offset to strip vertices that downstream code applies to raylib vertices. dcMeshUploadSafe() temporarily clears the DCMesh vaoId tag so code can call UploadMesh() without triggering the "already loaded" warning path, then restores the tag.
GLdc can draw triangle strips, and the PVR likes coherent polygon submissions. The expensive part is preparing that geometry every frame. DCMesh moves stripification and de-indexing offline:
- no meshoptimizer work at runtime
- no index buffer needed in the Dreamcast draw path
- fewer per-triangle EOL decisions
- one client-array setup per submesh
- predictable vertex layout for GLdc
The cost is a sidecar file format and a registry association in vaoId. For a Dreamcast-specific performance path, that tradeoff is reasonable, but it should be presented upstream as an optional backend feature, not as core raylib behavior.
UploadMesh() now frees an existing mesh->vboId array before allocating a new one when the normal "already uploaded" guard does not catch the re-upload.
Why this exists:
- On modern GL paths,
vaoId > 0usually catches a second upload. - On GLdc / OpenGL 1.x,
vaoIdremains zero for normal meshes. - That means repeated
UploadMesh()calls can allocate a freshvboIdarray every time and leak the old array.
The fix frees only the bookkeeping array before reallocating it. It does not delete real GL buffers on the GL33/ES2 paths because those paths return early via the vaoId > 0 guard.
src/raymath.h now has optional USE_SH4ZAM paths:
- includes
sh4zam/shz_vector.h - includes
sh4zam/shz_matrix.h - includes
sh4zam/shz_quat.h - includes
sh4zam/shz_xmtrx.h
Accelerated or modified functions include:
Vector3Normalize()Vector3Transform()MatrixInvert()MatrixMultiply()MatrixRotateX()MatrixRotateY()MatrixRotateZ()QuaternionNormalize()
There are conversion helpers between raylib Matrix/Quaternion conventions and SH4ZAM types. The matrix comments are important because raylib's named members and SH4ZAM's memory layout do not line up naively. The helper code treats named matrix fields as semantic truth and reorders memory where necessary.
The most important correctness change is the switch to unaligned SH4ZAM matrix loaders in Vector3Transform() and MatrixMultiply(). On SH4, by-value Matrix parameters may only be 4-byte aligned. Plain SH4ZAM fmov.d loaders can fault on real hardware when the address is not sufficiently aligned. The fork uses runtime-alignment-checked unaligned loaders to keep the acceleration path safe.
The TODO plan still treats SH4ZAM as incomplete. That is accurate. More hot rotation/matrix-stack paths remain scalar or KOS-matrix based.
rlGenTextureMipmaps() now has a Dreamcast branch:
- binds the texture
- requires square power-of-two dimensions
- calls GLdc
glGenerateMipmap(GL_TEXTURE_2D) - sets raylib's mipmap count to
1 + floor(log2(width)) - logs a Dreamcast-specific success or warning message
This allows raylib texture code to use GLdc's mipmap generation where GLdc supports it, instead of falling through to "GPU mipmap generation not supported."
The restriction to square POT textures mirrors GLdc's implementation constraints. Non-square textures warn and do not generate mipmaps.
src/Makefile now:
- defines
USE_SH4ZAM ?= 0 - adds
-DUSE_SH4ZAMand-lsh4zamwhen enabled - builds
dc_mesh.o - adds an explicit
dc_mesh.odependency rule
src/CMakeLists.txt now lists:
dc_mesh.cdc_mesh.hdcmesh.h
The current Makefile adds dc_mesh.o to the general object list. The source body is guarded by PLATFORM_DREAMCAST, so non-Dreamcast builds see an effectively empty translation unit. If upstreaming, this might be tightened so DCMesh only enters the object list for Dreamcast targets.
InitWindow() logs a modified local raylib canary and whether USE_SH4ZAM is enabled.
This is useful for downstream verification but should be removed or converted to normal version/build metadata before upstreaming. The timestamp-only commits are diagnostic churn, not conceptual renderer changes.
New files:
GL/gldc_stats.hGL/gldc_stats.c
The stats are compiled in only when GLDC_ENABLE_STATS is defined. Without it, the macros are no-ops.
Counters include:
- draw calls:
draw_arrays_calls,draw_elements_calls - submit calls:
submit_vertices_calls - fast path:
fast_path_hits,fast_path_misses - headers and state:
headers_emitted,state_dirty_events - vertex counts:
vertices_transformed - texture binds:
texture_binds - immediate mode:
immediate_begin_calls,immediate_end_calls,immediate_vertices - clipping:
clip_triangles_tested,clip_all_visible,clip_none_visible,clip_partial,clip_edges_generated - scene submission:
scene_list_submits,scene_vertices_in,scene_headers_seen - strips and opaque routing:
strip_count,strip_vertices_total,patchE_hits,patchE_fallbacks
Public functions:
glKosResetStats()glKosGetStats()glKosPrintStats()
Hook points:
glDrawArrays()glDrawElements()submitVertices()generate()apply_poly_header()glBindTexture()_glGPUStateMarkDirty()glBegin(),glVertex3f(),glEnd()SceneListSubmit()and near-plane clipping cases
This is the measurement layer for the entire optimization stack. It answers:
- Did raylib batching reduce GLdc submit calls?
- Are large batches actually hitting GLdc fast paths?
- Are texture binds and dirty-state events still high?
- Is clipping cost material?
- Are DCMesh strips being used?
- Are opaque-list routing opportunities happening?
This should be an upstream-friendly feature if kept optional and low overhead when disabled.
Modified file:
GL/draw.c
New specialized generators:
generateArraysFastPath_PUC_QUADS()generateArraysFastPath_PUC_TRIS()
PUC means:
- position
- UV
- color
- no normals
- no second texture coordinate/ST data
- no indices
The specialized path skips the normal and ST loops entirely. It only gathers/copies UV and color, transforms positions, and writes final PVR vertex flags for quads or triangles.
The dispatch sits inside generate():
- if
ATTRIB_LIST.fast_pathis true - if there are no indices
- if normal and ST attributes are disabled
- if mode is
GL_QUADSorGL_TRIANGLES - then use the PUC generator
- otherwise fall through to the existing fast path or generic path
The raylib-dc batcher deliberately emits exactly this layout. That means GLdc can avoid work for attributes that are known absent in the hot 2D/UI/helper path. This is a lower-risk optimization than a full renderer rewrite because it is just a narrower dispatch inside the existing GLdc submission pipeline.
The code comment estimates roughly 40% less per-vertex loop work compared with the generic fast path for this common layout.
Modified file:
GL/draw.c
In apply_poly_header(), punch-through list headers now use:
- source blend:
GPU_BLEND_SRCALPHA - destination blend:
GPU_BLEND_INVSRCALPHA - depth comparison:
GPU_DEPTHCMP_LEQUAL
The local rationale is hardware correctness. Alpha-test/punch-through discard can be unreliable on real PVR hardware for some texels that should disappear. If punch-through writes use one/zero blending, those surviving transparent texels become visible opaque boxes. With source-alpha/inverse-source-alpha blending, fully transparent survivors resolve to the destination and become invisible, while fully opaque texels still write normally.
This is a case where emulator behavior can hide a real-hardware issue. It is not merely a stylistic blend-state change.
Modified file:
GL/platforms/sh4.c
Changes include:
PVR_OPB_COUNTincreased from 2 to 4.- clipping counters added.
_glClipEdge()replacesfabsf(d0) * (1 / sqrtf((d1 - d0)^2))withfabsf(d0) / fabsf(d1 - d0).- directional epsilon added to keep clipped vertices just inside the near plane.
- header detection changed from two equality checks to a single unsigned compare.
- the queued vertex copy uses
memcpy_vertex()instead of struct assignment. - scratch vertices are hoisted outside the loop and 32-byte aligned.
- one-line-ahead prefetch is added for the common loop.
- the all-visible case is made a likely fast path before the switch.
- scene submit/header counters are added.
Near-plane clipping and PVR scene-list submission sit below all raylib batching. Any remaining geometry, batched or not, pays this cost. The changes are small but targeted:
- remove an expensive square-root expression that simplifies mathematically
- reduce per-iteration branch and stack work
- make the overwhelmingly common all-visible case cheaper
- gather data on how often clipping is actually happening
Modified files:
GL/platforms/sh4.cinclude/GL/glkos.h
New public functions:
glKosQueueFogTableLinear(GLfloat a, GLfloat r, GLfloat g, GLfloat b, GLfloat start, GLfloat end)glKosQueueFogTableFlat(GLfloat amount, GLfloat a, GLfloat r, GLfloat g, GLfloat b, GLfloat farDepth)
GLdc stores a single deferred fog request and applies it inside SceneBegin() / SceneBeginToTexture() after pvr_wait_ready() and immediately before pvr_scene_begin() or pvr_scene_begin_txr().
PVR fog table/color registers are global hardware state. Updating them while the previous scene may still be rendering can glitch on hardware. Queueing the change until GLdc has waited for the renderer and is about to begin the next scene provides a safer handoff point.
The API is Dreamcast-specific and belongs in glkos.h, not generic OpenGL headers.
Modified files:
GL/flush.cGL/platforms/sh4.cGL/platform.hGL/texture.cinclude/GL/glkos.h
New public functions:
glKosFlushToTexture(void *tex, unsigned int w, unsigned int h)glKosTextureData(GLuint texId)
glKosFlushToTexture() renders the currently queued GLdc lists into a VRAM texture target using KOS pvr_scene_begin_txr(), then clears OP/PT/TR lists just like a normal buffer swap path would.
glKosTextureData() returns the raw level-0 VRAM pointer for a GL texture ID. The target texture must be suitable for KOS render-to-texture use, specifically a nontwiddled 16-bit target in the current local assumptions.
This supports a two-pass composition pattern:
- Render the world to a texture.
- Draw that texture plus overlay/HUD/UI to the real framebuffer.
On PowerVR, list order and translucent behavior can make HUD composition awkward if everything shares the same scene submission. Flattening the world first gives the overlay pass predictable ordering.
This is not generic OpenGL FBO support. It is a KOS/PVR-specific utility API, which is why it is exposed through glkos.h.
Modified file:
GL/texture.c
glTexImage2D() now checks whether the 32-byte-aligned conversion buffer allocation succeeded before writing through it. If allocation fails, GLdc throws GL_OUT_OF_MEMORY and returns.
Conversion buffers can be large. A 512x512 RGBA8888 source is roughly 1 MiB before conversion details. On Dreamcast memory budgets, a failed allocation is plausible. The previous path could dereference NULL and fault.
Modified files:
GL/texture.cGL/state.c
glBindTexture() increments texture_binds. _glGPUStateMarkDirty() increments state_dirty_events.
These counters are essential for proving that deferred unbinds and rlgl batching are reducing real backend state churn, not merely reducing apparent helper calls.
Modified files:
CMakeLists.txtGL/matrix.c
CMakeLists.txt adds:
option(USE_SH4ZAM "Use SH4ZAM optimized Dreamcast math" OFF)target_compile_definitions(GL PUBLIC USE_SH4ZAM)target_link_libraries(GL PUBLIC sh4zam)
GL/matrix.c includes sh4zam/shz_matrix.h under USE_SH4ZAM. The currently implemented acceleration is limited: UpdateNormalMatrix() uses SH4ZAM inverse and transpose when enabled.
The TODO plan correctly calls out that the main GLdc matrix stack still needs more work. Functions such as glLoadMatrixf, glMultMatrixf, glTranslatef, glScalef, and glRotatef still largely build matrices and go through KOS matrix operations or scalar construction paths. That is a planned future optimization, not already complete.
glKosInitEx() logs a modified local GLdc canary and SH4ZAM status.
As with the raylib canary, this is useful for downstream debugging but should not be upstreamed in timestamp form.
Modified file:
src/dcmesh.h
Format version is now:
DCMESH_VERSION 2
Version 2 adds a vertex_map after each submesh's vertices and strips:
uint16_t vertex_map[vertex_count]
This maps each expanded strip vertex back to the original source vertex index.
The converter de-indexes and strip-expands geometry for fast Dreamcast rendering. That is good for drawing but bad for later synchronization: once one source vertex has been duplicated into several strip positions, the runtime needs a way to update all expanded copies if raylib mesh data changes. vertex_map is that bridge.
The uint16_t map is compact, but it implies a practical source-vertex index limit. If this format is generalized, models above 65535 source vertices per primitive/submesh need either validation or a wider map.
The DCMesh vertex format is:
| Field | Type |
|---|---|
| position | float x, y, z |
| texcoord | float u, v |
| color | packed BGRA uint32_t |
Total: 24 bytes.
This intentionally matches the raylib-dc batcher and GLdc PUC fast path. That shared layout is one of the main architectural wins:
- offline converter emits it
- raylib-dc runtime consumes it
- GLdc fast path is specialized for it
- no normal/ST data is carried for the hot path
Modified file:
src/dcmesh.c
The converter now finds the glTF node that references each mesh and calls cgltf_node_transform_world(). It applies that world transform to positions before stripifying/writing vertices.
raylib's glTF loader applies node transforms to vertex positions during load. Without matching that behavior in the sidecar converter, raylib mesh vertices and DCMesh sidecar vertices would disagree in scale, rotation, or position. That would break bounds, recentering, and visual alignment.
This is not a cosmetic fix. It is required for the sidecar data to match raylib's loaded model.
The converter pipeline is:
- load
.glbviacgltf - read indexed triangle primitives
- read positions, first UV set, and first color set if present
- apply node world transform
- optimize for strip generation with
meshopt_optimizeVertexCacheStrip() - stripify with
meshopt_stripify() - split strips on restart indices
- pre-expand strip vertices
- write submesh headers, vertices, strips, and
vertex_map
Opacity is derived from glTF material alpha mode:
cgltf_alpha_mode_opaque=> opaque- other alpha modes => translucent
That flag feeds the raylib-dc runtime's opaque-list routing check.
gen_meshes now invokes dcmesh.exe for the downstream Dreamcast GLB assets, including the logo model path. This is local project glue, not a general-purpose upstream API, but it documents how the converter is expected to be used.
The changes are most understandable as one pipeline:
Before:
raylib helper -> rlBegin/rlEnd -> GLdc immediate -> glDrawArrays per helper -> submitVertices per helper
After:
raylib helper -> rlgl Dreamcast batcher -> large interleaved P+UV+color arrays -> GLdc PUC fast path -> fewer PVR list submissions
Before:
LoadModel -> raylib mesh arrays -> DrawMesh -> GLdc triangles/elements path
After, if a sidecar exists:
LoadModel -> raylib mesh arrays + .dcmesh sidecar -> DrawMesh routes to DCMesh -> precomputed GL_TRIANGLE_STRIP arrays -> GLdc strip path
Before:
helper-local set/unset texture/blend state -> frequent GLdc dirty events -> frequent polygon header work
After:
deferred unbinds + state flush boundaries -> fewer dirty events -> more coherent headers/list submissions
The stack still preserves raylib as the public API. None of this requires application code to use a new renderer for ordinary raylib helper drawing. DCMesh is the exception: it adds an optional sidecar data path for model assets, while still falling back to normal DrawMesh() when no sidecar exists.
The TODO plan treats SH4ZAM work as staged and hardware-verified. That framing is important because the current forks already contain some SH4ZAM paths, but they are not a complete acceleration pass.
The TODO notes that a fully updated DreamSDK + KOS + SH4ZAM environment has been reported to compile poorly and show broken geometry. The plan is to avoid updating the local toolchain until the regression can be isolated or there is time for a proper bisect.
When the update is eventually attempted, the plan is:
- snapshot the current DreamSDK tree first
- update raylib-dc and GLdc to matching commits
- update SH4ZAM
- confirm
USE_SH4ZAMstill compiles - perform a full Dreamcast dependency rebuild
- validate on real hardware, not just emulator
- if geometry breaks, suspect GLdc matrix-stack changes first, then raylib rlgl matrix path, then SH4ZAM/KOS ABI drift around
shz_xmtrx_*
Current concern:
rlRotatef / glRotatef still build rotation matrices through scalar or KOS-style paths, then apply them.
Planned change:
- in raylib-dc
src/rlgl.h, replace the matrix-build plus load/apply path underUSE_SH4ZAMwith direct SH4ZAM XMTRX rotation helpers where the API allows it - do the same in GLdc's
glRotatefif it remains separate - preserve public function signatures and OpenGL multiplication semantics
Rationale:
Rotation is common in camera, model, and helper paths. A direct XMTRX rotation avoids memory round trips and can be one of the highest-ROI SH4ZAM swaps.
Current state:
Some raymath functions already have SH4ZAM paths:
MatrixRotateXMatrixRotateYMatrixRotateZMatrixMultiplyMatrixInvert- normalization helpers
Still planned:
MatrixRotateMatrixRotateXYZMatrixRotateZYX- quaternion axis/euler constructors
- remaining scalar
sinf/cosfpairs that can useshz_sincosf
Rationale:
The current acceleration is useful but incomplete. Composite/euler rotation helpers still build matrices with scalar trig. The plan is to use SH4ZAM direct constructors or faster trig helpers while preserving raylib's matrix conventions.
Current state:
GLdc has a USE_SH4ZAM build option and uses SH4ZAM for normal-matrix inverse/transpose, but the core matrix stack still largely uses existing scalar/KOS matrix operations.
Planned audit targets in GL/matrix.c:
_glMultMatrixUploadMatrix4x4MultiplyMatrix4x4- wrappers around
mat_load - wrappers around
mat_apply glLoadIdentityglLoadMatrixfglMultMatrixfglTranslatefglScalefglRotatef
Rationale:
The TODO notes that KOS routines may use the same hardware instruction family but can be slower because of scheduling, prefetch, and FPU details. Replacing matrix stack hot paths with SH4ZAM equivalents could matter, but it is higher risk because GLdc matrix semantics are subtle.
Current state:
The raylib-dc fork already had to move from plain aligned SH4ZAM loads to unaligned-safe loaders for by-value matrix parameters.
Planned options:
- align public
Matrixunder Dreamcast where ABI-safe - use aligned scratch buffers at specific SH4ZAM call sites
- keep unaligned runtime-checked loaders where public ABI or stack alignment is uncertain
Rationale:
SH4ZAM is fastest with 32-byte-aligned operands, but changing public struct alignment can have ABI consequences. The TODO plan correctly says not to change that casually.
Planned audit:
- search raylib-dc and GLdc for
sinf,cosf,tanf, andatan2f - replace hot Dreamcast-only cases with SH4ZAM equivalents where semantics are clear
- use
shz_sincosfwhen both sine and cosine of the same angle are needed
Rationale:
This is lower risk and lower payoff than matrix-stack work, but it is useful polish once the larger SH4ZAM paths are stable.
The TODO also describes a downstream HyperSolar CPU-lighting optimization: batch up to four light direction dot products through XMTRX/FTRV so one instruction can produce four light intensities. This is not a raylib-dc or GLdc upstream change by itself, but it is a useful example of the same SH4ZAM principle: group vector work into the SH4's vector register/matrix hardware instead of scalar loops.
For upstream maintainers, the main takeaway is not the game-specific lighting code. It is that SH4ZAM wins are most valuable when the data is arranged to use FTRV/XMTRX in batches, not when individual scalar operations are swapped one by one.
The TODO plan calls for per-tier verification rather than one large final check:
- build both
USE_SH4ZAM=0andUSE_SH4ZAM=1 - inspect disassembly for expected SH4ZAM canary symbols
- compare visuals on hardware
- watch for FPU exceptions, NaNs, distorted matrices, and geometry corruption
- benchmark fixed scenes before and after each tier
Expected canary symbols include:
shz_xmtrx_rotateshz_sincosfshz_xmtrx_load_4x4shz_xmtrx_apply_4x4shz_sinfshz_cosfshz_xmtrx_load_rowsshz_xmtrx_transform
This verification discipline matters because the current history already shows one important real-hardware issue: unaligned matrix loads can fault even when code looks correct in C.
- Dreamcast-only rlgl immediate-mode batcher, if kept behind
PLATFORM_DREAMCAST. - Deferred texture unbinds for the Dreamcast GL 1.1 path.
- Optional GLdc stats counters.
- GLdc P+UV+color fast path.
- GLdc punch-through alpha blend fix, assuming maintainers agree with the hardware behavior and test coverage.
- GLdc conversion OOM guard.
rlGenTextureMipmaps()Dreamcast branch, if GLdcglGenerateMipmap()availability is treated carefully.
glKosQueueFogTableLinear()/glKosQueueFogTableFlat().glKosFlushToTexture()/glKosTextureData().- DCMesh as an optional Dreamcast-sidecar model acceleration path.
These are not generic raylib features, but they could be acceptable as glkos.h/Dreamcast-backend extensions.
- Timestamp/canary log churn in raylib-dc and GLdc.
- DCMesh color-channel TODO in
dcMeshSyncFromRaylib(). - Clearer naming around local DCMesh "Patch E" versus DCDocs' direct-PVR "Patch E".
dc_mesh.obuild inclusion should probably be Dreamcast-only in upstream build files.- DCMesh registry ownership should be reviewed for multi-model sharing and unload edge cases.
vertex_mapshould validate source vertex counts before narrowing touint16_t.- The
vaoIdregistry-tag technique should be documented as Dreamcast GL 1.1-specific or replaced with a backend-private association if upstream prefers not to overload raylib fields.
- The batcher skips normals for captured geometry. That is correct for the targeted helper path, but any future capture expansion must flush/fall back when normals materially affect output.
- Matrix changes currently flush the batch. This is safe, but helper paths with many push/pop operations may still fragment. Pre-transforming vertices is possible future work, not current behavior.
- DCMesh assumes sidecar submesh order matches raylib model mesh order. The converter and loader should keep this invariant explicit.
- Render-to-texture requires suitable VRAM texture allocation and format choices. The API is low-level and should remain documented as such.
- Deferred fog stores one pending fog request. If callers queue multiple changes before a scene begins, the latest one wins.
- SH4ZAM acceleration must be hardware-tested. Emulator success is not enough for alignment and geometry correctness.
The forks are not random downstream hacks; they are a coherent attempt to make raylib's Dreamcast backend behave more like a Dreamcast renderer without changing the public raylib programming model.
The most important general lesson is that raylib helper traffic needs backend-aware batching on Dreamcast. The GLdc OpenGL 1.1 path does not get the same batching behavior as modern OpenGL/ES paths, and tiny helper-local draws are disproportionately expensive on SH4/PVR. The current raylib-dc batcher is a practical, conservative implementation of that idea.
The second lesson is that GLdc already has the right kind of fast path surface. It does not need to be bypassed immediately. Feeding it larger P+UV+color arrays, instrumenting the pipeline, and specializing the common layouts gives a strong first round of wins while keeping GLdc in control of state, transforms, clipping, and PVR list submission.
The third lesson is that model geometry benefits from offline Dreamcast-specific preparation. DCMesh is a sidecar approach: it leaves raylib's model loader intact but gives the Dreamcast draw path strip-expanded, cache-friendly data when available. That is a good fit for static or mostly static assets, provided the format and registry ownership are tightened before broader use.
Finally, SH4ZAM should be treated as a staged optimization program, not a single on/off switch. The current forks contain useful SH4ZAM work, especially around raymath and safe unaligned matrix loads, but the TODO plan correctly identifies remaining hot paths in rlgl and GLdc matrix handling.