Skip to content

Instantly share code, notes, and snippets.

@ninjadynamics
Created June 19, 2026 19:20
Show Gist options
  • Select an option

  • Save ninjadynamics/3f6652808cc0d5b41a361fc9a5ddda8f to your computer and use it in GitHub Desktop.

Select an option

Save ninjadynamics/3f6652808cc0d5b41a361fc9a5ddda8f to your computer and use it in GitHub Desktop.
Dreamcast forks

Dreamcast Fork Change Report: raylib-dc, GLdc, and dcmesh

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.

Executive Summary

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:

  1. raylib-dc: capture simple rlgl immediate traffic on Dreamcast and submit it as larger glDrawArrays() batches, with deferred texture unbinds and strict flush boundaries for correctness.
  2. 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.
  3. 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.

Baselines and Current Heads

raylib-dc

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.

GLdc

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.

dcmesh

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.

External Context Consulted

The local changes line up with the DCDocs analysis of raylib/GLdc performance on Dreamcast:

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.

raylib-dc Changes in Detail

1. Dreamcast rlgl Immediate-Mode Batcher

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_QUADS
  • RL_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_BGRA x GL_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.

What changed in rlgl.h

The OpenGL 1.1 path now includes:

  • <GL/glext.h> for Dreamcast, so GLdc's glGenerateMipmap() and related declarations are visible.
  • rlgl_dc_batch.h under PLATFORM_DREAMCAST.
  • Matrix mutation flush triggers before rlFrustum, rlOrtho, rlPushMatrix, rlPopMatrix, rlLoadIdentity, rlTranslatef, rlRotatef, rlScalef, and rlMultMatrixf.
  • rlBegin() capture for quads/triangles.
  • rlEnd() no-op behavior for captured draws, keeping the batch open.
  • rlVertex2i, rlVertex2f, and rlVertex3f append into the batch when active.
  • rlTexCoord2f and rlColor* update the batcher's current attributes.
  • rlNormal3f is 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.

Deferred texture unbind

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.

Correctness boundaries

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.

Rationale

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.

2. DCMesh Runtime Integration

New files:

  • src/dc_mesh.c
  • src/dc_mesh.h
  • src/dcmesh.h

Hooked into:

  • src/rmodels.c
  • src/Makefile
  • src/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 .dcmesh sidecar.
  • UnloadModel() frees DCMesh registry data before normal model cleanup.
  • DrawMesh() checks whether a mesh has DCMesh strip data; if so, it routes through dcMeshDraw().
  • 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.

Registry handle

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.

DCMesh draw path

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.

Opaque routing / local "Patch E"

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.

UploadMesh sync hook

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_map to map each expanded strip vertex back to its source raylib vertex.
  • Return 1 so the caller skips normal UploadMesh().

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.

Recenter helper and safe upload

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.

Rationale

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.

3. Mesh Re-upload Guard

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 > 0 usually catches a second upload.
  • On GLdc / OpenGL 1.x, vaoId remains zero for normal meshes.
  • That means repeated UploadMesh() calls can allocate a fresh vboId array 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.

4. SH4ZAM Integration in raymath

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.

5. Dreamcast Mipmap Generation Path

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.

6. Build Integration

src/Makefile now:

  • defines USE_SH4ZAM ?= 0
  • adds -DUSE_SH4ZAM and -lsh4zam when enabled
  • builds dc_mesh.o
  • adds an explicit dc_mesh.o dependency rule

src/CMakeLists.txt now lists:

  • dc_mesh.c
  • dc_mesh.h
  • dcmesh.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.

7. Local Canary Logging

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.

GLdc Changes in Detail

1. Optional Performance Instrumentation

New files:

  • GL/gldc_stats.h
  • GL/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

Rationale

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.

2. Position + UV + Color Fast Path

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_path is true
  • if there are no indices
  • if normal and ST attributes are disabled
  • if mode is GL_QUADS or GL_TRIANGLES
  • then use the PUC generator
  • otherwise fall through to the existing fast path or generic path

Rationale

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.

3. Punch-through Blend Fix

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.

4. Scene Submission and Clipping Optimizations

Modified file:

GL/platforms/sh4.c

Changes include:

  • PVR_OPB_COUNT increased from 2 to 4.
  • clipping counters added.
  • _glClipEdge() replaces fabsf(d0) * (1 / sqrtf((d1 - d0)^2)) with fabsf(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.

Rationale

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

5. Deferred Fog API

Modified files:

  • GL/platforms/sh4.c
  • include/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().

Rationale

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.

6. Render-to-Texture API

Modified files:

  • GL/flush.c
  • GL/platforms/sh4.c
  • GL/platform.h
  • GL/texture.c
  • include/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.

Rationale

This supports a two-pass composition pattern:

  1. Render the world to a texture.
  2. 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.

7. Texture Conversion OOM Guard

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.

Rationale

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.

8. Texture Bind and Dirty-State Counters

Modified files:

  • GL/texture.c
  • GL/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.

9. SH4ZAM Support in GLdc

Modified files:

  • CMakeLists.txt
  • GL/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.

10. Local Canary Logging

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.

dcmesh Changes in Detail

1. File Format Version 2

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.

Rationale

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.

2. 24-byte Vertex Format

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

3. Node Transform Baking

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.

Rationale

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.

4. Offline Stripification Pipeline

The converter pipeline is:

  1. load .glb via cgltf
  2. read indexed triangle primitives
  3. read positions, first UV set, and first color set if present
  4. apply node world transform
  5. optimize for strip generation with meshopt_optimizeVertexCacheStrip()
  6. stripify with meshopt_stripify()
  7. split strips on restart indices
  8. pre-expand strip vertices
  9. 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.

5. Convenience Generation Script

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.

How the Three Forks Fit Together

The changes are most understandable as one pipeline:

Helper/2D/UI path

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

Model path

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

State/routing 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.

SH4ZAM Improvement Plan from TODO.md

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.

Toolchain update is intentionally on hold

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_SH4ZAM still 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_*

Tier 1.1: Replace rlRotatef / glRotatef rotation paths

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 under USE_SH4ZAM with direct SH4ZAM XMTRX rotation helpers where the API allows it
  • do the same in GLdc's glRotatef if 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.

Tier 1.2: Expand raymath rotation and quaternion constructors

Current state:

Some raymath functions already have SH4ZAM paths:

  • MatrixRotateX
  • MatrixRotateY
  • MatrixRotateZ
  • MatrixMultiply
  • MatrixInvert
  • normalization helpers

Still planned:

  • MatrixRotate
  • MatrixRotateXYZ
  • MatrixRotateZYX
  • quaternion axis/euler constructors
  • remaining scalar sinf/cosf pairs that can use shz_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.

Tier 1.3: GLdc matrix-stack operations

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:

  • _glMultMatrix
  • UploadMatrix4x4
  • MultiplyMatrix4x4
  • wrappers around mat_load
  • wrappers around mat_apply
  • glLoadIdentity
  • glLoadMatrixf
  • glMultMatrixf
  • glTranslatef
  • glScalef
  • glRotatef

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.

Tier 1.4: Matrix alignment audit

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:

  1. align public Matrix under Dreamcast where ABI-safe
  2. use aligned scratch buffers at specific SH4ZAM call sites
  3. 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.

Tier 1.5: Bare trig audit

Planned audit:

  • search raylib-dc and GLdc for sinf, cosf, tanf, and atan2f
  • replace hot Dreamcast-only cases with SH4ZAM equivalents where semantics are clear
  • use shz_sincosf when 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.

Tier 1.6: Downstream CPU lighting path

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.

Tier 1.7: Verification plan

The TODO plan calls for per-tier verification rather than one large final check:

  • build both USE_SH4ZAM=0 and USE_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_rotate
  • shz_sincosf
  • shz_xmtrx_load_4x4
  • shz_xmtrx_apply_4x4
  • shz_sinf
  • shz_cosf
  • shz_xmtrx_load_rows
  • shz_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.

Upstreamability Notes

Strong upstream candidates

  • 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 GLdc glGenerateMipmap() availability is treated carefully.

Good but more Dreamcast-specific candidates

  • 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.

Needs cleanup before upstream

  • 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.o build 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_map should validate source vertex counts before narrowing to uint16_t.
  • The vaoId registry-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.

Risks and open questions

  • 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.

Maintainer Takeaways

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment