maximqad

maximqad 

just a dev who ports fps to Dreamcast

4subscribers

4posts

goals1
$76.52 of $453 raised
BBA

How Half-Life’s Renderer Was Rebuilt for Dreamcast (part 2)

The PC GoldSrc studio renderer was designed around late-1990s OpenGL. It assumes a reasonably capable CPU, abundant system memory, and an OpenGL driver that can absorb thousands of small immediate-mode calls.
That model does not fit the Dreamcast.
The Dreamcast port keeps GoldSrc’s animation system, bones, body groups, skins, chrome effects, render modes, and triangle-command format, but replaces the final half of the pipeline with something much more deliberate. The renderer is built around SH-4 matrix instructions, indexed batching, preprocessed geometry, PVR-native textures, and a special multi-matrix extension hidden inside the Dreamcast Direct3D driver.
Neo models push this strategy further by compressing the data feeding that renderer.

The PC GoldSrc baseline

In the PC build, R_StudioDrawPoints performs most studio work in the engine.
For each model it:
  1. Evaluates animation and builds the bone matrices.
  2. Transforms every model vertex on the CPU.
  3. Transforms or evaluates every normal.
  4. Computes per-normal studio lighting.
  5. Walks the model’s triangle-strip command stream.
  6. Computes texture coordinates and final Lambert lighting for each emitted vertex.
  7. Sends every strip or fan through OpenGL immediate mode.
The inner drawing loop is effectively:
qglBegin(GL_TRIANGLE_STRIP);
while (count--) {
qglTexCoord2f(...); qglColor4f(...); qglVertex3f(...);
}
qglEnd();
This is simple and portable, but produces a large number of function calls and repeated vertex work. A vertex referenced by several triangle runs may be submitted several times. Lighting is also revisited while walking the command stream.
On a PC, the OpenGL driver and faster host CPU make this acceptable. On Dreamcast, it would burn too much SH-4 time in API calls, floating-point work, and data marshaling before the PVR ever sees a triangle.
 

Dreamcast’s first major change: accumulate, then submit

The Dreamcast renderer replaces immediate-mode OpenGL emission with global vertex and index accumulators.
Instead of calling into the driver for every vertex, it writes D3DVERTEX data into g_pAccumVerts, writes indices into g_pAccumIndex, and submits an entire mesh with one DrawIndexedPrimitive call.
The consequences are important:
  • Far fewer driver transitions.
  • Texture and render state changes occur at controlled batch boundaries.
  • Triangle strips and fans become reusable indexed geometry.
  • The renderer can choose different submission methods without changing the model animation code.
  • Vertices are written sequentially, which is much friendlier to the SH-4 cache.
World geometry uses pre-lit D3DLVERTEX, while studio models use D3DFVF_VERTEX, carrying position, normal, and texture coordinates.

Rewriting GoldSrc’s triangle command stream

GoldSrc studio models do not contain a conventional static index buffer. Each mesh contains a command stream consisting of triangle strips and fans. Every command vertex carries:
  • vertex index, normal index, texture S, texture T
The Dreamcast port preprocesses these streams through R_StudioBuildTriangleStrips. If a rewritten representation is smaller, the command list is replaced with a unique vertex-record table followed by indices.
This avoids repeatedly assembling identical vertex descriptions every frame.
Two index-building paths then exist:
  • DCV_BuildStudioIndexList expands commands into ordinary loose triangles.
  • DCV_AssembleStudioIndexListRestart preserves strip-like runs separated by 0xFFFF restart markers.
The second form belongs to the multi-matrix path. It reduces index traffic and lets the Dreamcast-specific driver consume several small runs in one submission.
This preprocessing is conditional: if the optimized stream would be larger than the original, the original representation is retained with a marker describing how it should be decoded. That is a very console-like optimization—spend work once when loading the model, then minimize every rendered frame.

Two Dreamcast studio paths

The port does not use one universal studio draw routine. It selects between a safe path and a fast path.

The safe path

When a model intersects the near clipping plane, or when it is translucent, the renderer uses the CPU-transformed path:
R_StudioDrawPoints
R_StudioDrawPoints_Neo
↓
DCV_SubmitBatchCopy
The engine:
  • Evaluates the bone matrices.
  • Transforms positions on the SH-4.
  • Produces transformed normals and lighting inputs.
  • Builds an ordinary index list.
  • Submits normal D3DFVF_VERTEX data.
This path costs more CPU time, but gives the engine the transformed geometry needed for clipping and translucent rendering.

The fast opaque path

When the model is opaque and completely on the safe side of the near plane, the renderer avoids transforming every position itself:
R_StudioRenderMesh
R_StudioDrawPointsSimple_Neo
↓ DCV_SetupStudioLighting
↓ DCV_SubmitBatchGuarded
Before drawing, DCV_SetupStudioLighting constructs:
  • Up to 80 bone/world/view/projection matrices.
  • One light-direction vector for each bone.
  • A 128-entry lighting lookup table.
  • The model’s accumulated vertices.
Matrix multiplication is implemented with the SH-4 XMTRX and ftrv instructions. The resulting palette contains matrices already combined with the current world, view, and projection transforms.
Each submitted vertex needs a bone index. Instead of expanding the vertex format, the port stores that index in the low byte of the vertex normal’s first float slot:
*(byte *)&normal.x = boneIndex;
The driver extracts the byte, calculates:
matrix palette + bone index × 64
and loads the selected 4×4 matrix into XMTRX.
This is not hardware vertex shading in the modern sense—the PVR2 does not provide programmable vertex shaders. It is a tightly optimized Dreamcast HAL path using the SH-4 matrix unit and a specialized driver contract. The major win is that the generic engine no longer performs several C-level matrix transforms and lighting calculations for every vertex.

Lighting becomes a palette operation

PC GoldSrc calculates studio lighting in software. R_StudioLighting evaluates the bone-space normal against a per-bone light vector, applies GoldSrc’s modified Lambert curve, clamps the result, and looks it up through the gamma table.
The Dreamcast fast path prepares this information per bone instead of recomputing the whole chain per vertex:
  • The light direction is transformed once per bone.
  • A compact shade table supplies the final grayscale lighting level.
  • The tagged vertex tells the driver which bone’s light direction and matrix to use.
Chrome mapping still requires special texture-coordinate generation, but the port caches the viewer-relative chrome basis per bone and reuses it across all normals attached to that bone.

Why clipping and translucency disable the fastest path

The fast path is selected only when the model’s bounding box is safely in front of the near plane and the entity is opaque.
That decision reflects the Dreamcast’s strengths.
Opaque, unclipped geometry can be submitted as a compact batch and consumed directly. Geometry crossing the near plane may require software-visible transformed positions. Translucent geometry also has stricter ordering and render-state requirements on the PVR.
Therefore:
if (r_studio_clip_required || translucent) R_StudioDrawPoints_Neo(); else R_StudioDrawPointsSimple_Neo();
A model moving through the near plane can consequently become more CPU-expensive for a few frames. The same is true for additive and alpha-blended entities.

What is a Neo model?

A normal studio model begins with uppercase IDST. A Neo model begins with lowercase idst.
Both retain the broad GoldSrc studio-header organization, but Neo changes several high-volume data representations and uses its own animation and rendering routines.
The retail loader distinguishes them directly:
IDST → Mod_LoadStudioModel idst → Mod_LoadStudioNeoModel
After loading, the version field becomes an internal cache marker:
0xC0EDBEEF → standard model 0xC0EDBABE → Neo model

Neo optimization 1: indexed normals

A standard studio model stores each normal as three floats:
x, y, z = 12 bytes
Neo stores a one-byte index into GoldSrc’s fixed table of 162 canonical vertex normals:
normal = r_avertexnormals[normalIndex];
That reduces the normal payload from 12 bytes per normal to one byte—a 12:1 reduction for that part of the model.
The runtime must expand the index back into a float vector before submitting it, but the lookup is cheap and sequential. More importantly, the model occupies less disc space, cache memory, and memory bandwidth.
Neo’s transformed-normal routine also remembers the previously used bone. When consecutive normals belong to the same bone, it keeps that bone’s light matrix loaded in XMTRX instead of reloading it for every normal.
The model compiler can improve this further simply by grouping normals by bone.

Neo optimization 2: bit-packed animation

Standard GoldSrc animation uses run-length encoded arrays of 16-bit animation values. This is already compressed, but each stored value remains a full short.
Neo adds another compression layer:
  • Run headers remain accessible.
  • The first position value uses 16 bits.
  • The first quaternion component value uses 12 significant bits shifted into place.
  • Following values are stored as signed deltas.
  • Each run records how many bits are required for its deltas.
The Neo decoder scans the run headers, skips compressed data belonging to irrelevant earlier frames, and expands only the frame range needed for interpolation into an 8 KB scratch buffer.
This is a storage-versus-CPU trade:
  • Smaller model and sequence files.
  • Less GD-ROM traffic.
  • Lower cache pressure.
  • More shifts, masks, branches, and bitstream bookkeeping during animation.
On Dreamcast, that trade is reasonable. Loading and retaining large high-definition character animations is expensive, while the SH-4 can decode a narrow window of animation data as needed.
Neo is therefore not automatically faster at animation evaluation. It is primarily more memory-efficient, with performance benefits coming indirectly from better cache behavior and reduced I/O.

Neo optimization 3: smaller auxiliary structures

Neo also removes data that is unnecessary at runtime.
For example, a normal GoldSrc attachment contains a 32-byte name and a type field. The Neo attachment keeps only:
  • Bone index.
  • Attachment origin.
  • Three orientation vectors.
That changes an attachment from 88 bytes to 52 bytes.
Individually this is small, but Neo applies the same philosophy throughout the model path: preserve what the engine needs at runtime and remove or compress authoring-oriented data.

Neo rendering is still dual-path

Neo does not replace Dreamcast batching. It feeds the same two submission strategies with more compact source data.
For clipped or translucent Neo models:
indexed normals ↓ table expansion CPU-transformed positions and normals ↓ ordinary indexed batch
For opaque, unclipped Neo models:
model-space positions indexed normals → float normal lookup bone tags embedded in vertices ↓ multi-matrix batch
The fast Neo path still transmits normal-sized D3DFVF_VERTEX records to the driver. Its biggest bandwidth saving exists before submission: smaller model data in memory and fewer bytes read while preparing the mesh.

The practical result

The Dreamcast studio renderer is not merely the PC renderer translated from OpenGL to Direct3D. It changes where the work happens.
The PC path asks the engine to transform, light, and submit vertices one at a time. The Dreamcast path tries to evaluate animation once, prepare shared bone state once, assemble indexed geometry sequentially, and hand a whole mesh to a platform-specific driver.
Neo models then reduce the amount of source data required to feed that system.
The expected hierarchy is:
  1. Opaque, unclipped Neo model: best memory behavior and the fast multi-matrix submission path.
  2. Opaque, unclipped standard model: same major rendering acceleration, but larger normals and animation data.
  3. Clipped or translucent Neo model: compact assets, but CPU transform and normal expansion reduce the runtime advantage.
  4. PC-style immediate rendering: simplest path, but poorly suited to Dreamcast’s CPU and driver overhead.
One final historical detail is important: the May 15, 2001 prototype model set contains 215 standard IDST models, but no lowercase idst Neo models.
Go up