Carry each billboard particle's resident texture handle in the instance ABI so stable retail alpha ordering can batch mixed textures without rebinding and redrawing each short texture run. Keep DAT blend transitions as the only billboard draw boundary. The connected 0xC95B stress scene improved from 6 FPS / 171 ms to about 153 FPS / 6.6 ms at the same roughly 21,000 visible entities. Release build succeeds and all 5,916 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
7 KiB
Retail shared alpha-list and particle ordering
Scope
This note records the Sept. 2013 retail mechanism that orders translucent
physics-object parts and particle parts. It is the oracle for replacing
acdream's independent WbDrawDispatcher and ParticleRenderer transparent
passes.
The motivating failure is a translucent lifestone crystal drawn before a later scene-particle pass. Both passes depth-test and disable depth writes, so a smoke or flame particle behind the crystal passes the opaque depth buffer and composites at full strength over the already-drawn crystal.
Named-retail sources
ParticleEmitter::EmitParticleat0x0051D010CPhysicsObj::AddPartToShadowCellscall at0x0051D126CPhysicsPart::UpdateViewerDistanceat0x0050E030CPartCell::add_partat0x0052E740RenderDeviceD3D::DrawObjCellForDummiesat0x005A0760CShadowPart::insertion_sortat0x006B5130CShadowPart::drawat0x006B50D0CPhysicsPart::Drawat0x0050D7A0D3DPolyRender::DrawMeshat0x0059D4A0D3DPolyRender::AddMeshToAlphaListat0x0059C230D3DPolyRender::FlushAlphaListat0x0059D2E0PView::DrawCellsat0x005A4840SmartBox::RenderNormalModeat0x00453AA0D3DPolyRender::s_AlphaDelayMask = 0xEat0x00820D88
The matching PDB header defines:
CShadowPart {
uint num_planes;
ClipPlaneList** planes;
Frame* frame;
CPhysicsPart* part;
}
CPhysicsPart {
float CYpt;
Vector3 viewer_heading;
...
}
CYpt is the distance from the viewer to the transformed GfxObj
sort_center, not merely the object's origin.
Retail pseudocode
Particle materialization
EmitParticle(emitter):
particlePart = initialize one particle as a CPhysicsPart
parentPhysicsObject.AddPartToShadowCells(particlePart)
A particle is therefore not a special post-process overlay. It participates
in the same cell-owned part list as an object's ordinary CPhysicsParts.
Viewer-distance key
UpdateViewerDistance(part):
localCenter = part.gfxobj_scale * part.gfxobj.sort_center
viewerOffset = viewer.position offset to (part.position + localCenter)
part.CYpt = length(viewerOffset)
part.viewer_heading = normalize(viewerOffset), or +Z when almost zero
update degradation from CYpt
calculate final draw frame
Cell part ordering
DrawObjCellForDummies(cell):
update all object/part viewer distances in the cell
if cell.shadow_parts.count > 1:
CShadowPart.insertion_sort(cell.shadow_parts)
DrawObjCell(cell)
CShadowPart.insertion_sort(cell.parts):
stable insertion sort using part.CYpt
arrange that cell's parts in descending CYpt order
DrawPartCell(cell):
for shadowPart in cell.shadow_parts:
shadowPart.draw()
CShadowPart.draw(shadowPart):
shadowPart.part.Draw(force = false)
The sort covers ordinary object parts and particle parts together within one
CPartCell. Equal distance retains the established list order. The later
alpha-list append also preserves the order in which retail traverses cells;
it does not globally re-sort entries from different cells.
Deferred translucent subsets
CPhysicsPart.Draw(part):
install material, surface array, object scale, and draw frame
DrawMesh(part.gfxobj)
DrawMesh(mesh):
for each surface subset in authored order:
if subset matches AlphaDelayMask (retail default 0xE):
AddMeshToAlphaList(mesh, subset, surface,
captureCurrentWorldAndMaterialState)
else:
RenderMeshSubset immediately
AddMeshToAlphaList(...):
append one entry to the selected fixed alpha list
preserve append order
optionally snapshot world matrix and material state
FlushAlphaList():
render every queued clip-list entry in append order
clear clip list
render every queued alpha-list entry in append order
clear alpha list
FlushAlphaList does not invent a second distance sort. It preserves the
order established by the cell's CShadowPart list and the authored part /
surface traversal.
Flush scopes
PView.DrawCells():
if outside view exists:
LScape.draw() // queues delayed alpha
FlushAlphaList()
conditionally clear depth
draw indoor cell shells and object lists
SmartBox.RenderNormalMode():
DrawInside(viewerCell) // PView path above
FlushAlphaList() // final world-object alpha scope
The landscape flush must occur before retail's outside-to-inside depth clear. The final flush drains delayed alpha produced after that boundary.
Cross-reference findings
- acdream's extracted WorldBuilder mesh path correctly preserves DAT surface
translucency metadata and a GfxObj
SortCenter, but WorldBuilder renders its editor content in renderer-local passes. It has no liveCPartCell/CShadowPartownership and therefore cannot supply retail's mixed object-particle queue. - ACViewer is useful for DAT geometry and particle asset interpretation but is a viewer rather than the retail cell renderer. It does not supersede the named-retail ordering above.
- ACE is authoritative for particle/gameplay events but is server-side and has no client alpha compositor. It corroborates asset and object identity, not draw ordering.
The named retail client remains the behavioral oracle.
Port contract
- Scene particles and translucent live/static GfxObj batches submit into one stage-scoped queue.
- Each submission carries the retail viewer-distance key derived from the transformed GfxObj sort center (particle billboards use their live center).
- In the modern renderer's stage-scoped reconstruction, sort far-to-near with
stable submission sequence as the equal-distance tiebreak. This recreates
mixed ordinary/particle part ordering, but it is scope-global rather than
retaining retail's missing per-
CPartCelllists. AP-34 records that narrow architectural residual explicitly. - The queue itself does not reorder by renderer or material. Only adjacent compatible submissions may batch. The modern billboard vertex-instance record carries a bindless texture handle, so adjacent particles with different textures are still one compatible ordered draw; blend mode is the only billboard state boundary.
- Every flush keeps depth testing enabled and depth writes disabled.
- Each submission preserves its DAT blend mode, cull mode, transform, texture, lighting, opacity, and selection-lighting state.
- Flush before the outside-to-inside depth clear, then flush the final world scope before world-space overlays and UI.
- Sky-pre-scene and sky-post-scene particles remain in their dedicated sky passes; they do not enter the world alpha queue.
- A particle behind translucent crystal draws first and is then attenuated by the crystal. A particle in front draws after the crystal and remains bright.
- No lifestone-specific branch, transparent depth prepass, or particle depth-test suppression is permitted.