Dungeon Graphics Renderer - HoraceAndTheSpider/Bloodwych-68k GitHub Wiki
Dungeon graphics renderer data
This page describes the SPS 439 source data used to turn dungeon map locations into the 128×76 first-person display.
For the feature-specific branches and shared four-plane drawing primitives used after a wall face is selected, see Dungeon Graphics Components and Planar Compositor.
View cells and wall-face slots
The renderer does not calculate dungeon perspective from a general 3D formula. It uses two fixed coordinate systems:
- 19 player-relative view cells describe the map locations which may contribute to the current view;
- 28 wall-face slots describe the possible projected wall surfaces within the 128×76 viewport.
Dungeon_ViewCell_RelativeCoordinates.positions contains four groups of 19 signed X/Y pairs, one group for each player facing. Each word stores signed X in its high byte and signed Y in its low byte.
For each visible map cell, Dungeon_ViewCell_WallFaces.lookup supplies four bytes. These are the N/E/S/W candidate wall-face slots for that cell; $FF means that the face is unavailable from that view cell.
The four columns are perspective-dependent rather than one universal N/E/S/W order. The seven left-hand view cells use N/W/S/E, the seven right-hand cells use N/E/S/W, and the five central cells use N/E/W/S. This asymmetry is required to expose the correct inner face of wooden walls at the left and right edges of the view.
The source first scans all 19 view cells. It then traverses the cells from farthest to nearest to combine two 32-bit masks per cell:
Dungeon_ViewCell_VisibleFaces.flagscontributes wall faces which the cell can make visible;Dungeon_ViewCell_Occlusion.flagsremoves faces hidden by an opaque location.
The accumulated visible mask is finally ANDed with the accumulated occlusion mask. The surviving view cells are then drawn in the source traversal order. This is why a collection of individually plausible wall sprites can still be wrong if the view-cell masks or face order are ignored.
Dungeon_ViewCell_CentredSlots.lookup provides one centred projected slot for features such as pillars, beds, pits and pads. Nineteen bytes are used; the twentieth byte is spare/padding. $FF means that no centred sprite is available in that view cell.
View cell 18 is the player's current map square. Its north/front wooden edge uses wall-face slot 27, the same source picture and position as the south edge of view cell 17 immediately ahead. Its east and west edges use slots 23 and 11; the south edge behind the player is not visible. This nineteenth cell also supplies the dedicated inside-cell positions for open large doors, stairs, pads, pits and ceiling holes.
Stone and wooden walls
A type-1 stone-wall location tests all four of its candidate faces. The visibility and occlusion masks decide which faces survive. Its direction field does not limit the stone block to one wall surface; that direction selects the one face on which a shelf, sign, switch or socket can appear.
A type-2 wooden-wall location is different. Its first map byte contains four independent two-bit N/E/S/W states:
| Value | Result |
|---|---|
| 0 | no wooden wall or door |
| 1 | solid wooden wall |
| 2 | open wooden doorway |
| 3 | closed wooden door |
The renderer therefore draws only the sides actually present in those four fields.
Sprite selection, mirroring and perspective transforms
The 28 wall-face slots are translated to source pictures by two byte tables:
GFX_Main_Wall_SpriteTableselects the stone-wall picture for each slot;GFX_WallComponent_SpriteMirrorTableselects wall-component, wooden-wall, door and stair pictures. Bit 7 requests the mirrored path and the low seven bits select the picture.
Centred dungeon features use GFX_CentredDungeonComponent_SpriteMirrorTable. Its first 19 entries correspond to the 19 view cells; bit 7 again selects the mirrored path. The twentieth byte is not read by the 19-cell traversal.
Main stone-wall rows always pass through the game's bit-reversal lookup and are written right-to-left. The raw ST-format artwork therefore needs this horizontal reversal even though GFX_Main_Wall_SpriteTable contains ordinary picture numbers. Omitting it reverses the masonry perspective on the near left and right walls.
Two separate 28-byte flag tables select the ordinary writer or the perspective edge/cropping path:
GFX_Main_Wall_DrawTransformFlagsis used for stone-wall faces;GFX_WallComponent_DrawTransformFlagsis used for wall components, wooden walls, doors and stairs.
The tables are nearly, but not completely, identical: slots 6 and 18 differ. They should therefore remain separate resources. Bits 0 and 2 request the extra edge pass before or after the centre, while bit 1 selects the perspective centre path. The low three-bit value indexes the corresponding GFX_*_PerspectiveTrimLookup; that lookup returns 0, 1 or 2 source word-columns to trim while drawing the perspective edge.
For the nearest central component slots, source pictures 12–15 represent one half of the finished image. The renderer draws that half and then its reflected partner at 128 - x - width.
Alternating floor, ceiling and main-wall pattern
Draw_DungeonViewport derives one temporary pattern state from the player position and facing:
pattern parity = (player X + player Y + facing) & 1
Player X, Y and facing are read from character-state offsets $16, $17 and $18. Moving one square or turning 90 degrees changes the parity; turning 180 degrees preserves it.
For parity 1, Draw_FloorAndCeiling copies FloorCeiling.gfx normally and the main-wall path selects its second group of perspective pictures through GFX_Main_Wall_SpriteTable, bit-reversing their rows. For parity 0, the floor and ceiling rows are horizontally bit-reversed and main-wall source indexes are the wall-face slot itself. Slots 24–27 are shared by both main-wall states.
The alternation applies to floor, ceiling and main stone-wall texture. Wooden walls, doors, wall overlays and centred dungeon features retain their normal render paths. The dispatcher at $00B074 is labelled Draw_MainWall_AlternatingPattern; $00B2A4 is the already-labelled Draw_MainWallFace lookup-selected/bit-reversed branch.
Graphic resource companions
Dungeon artwork is not self-describing. A drawable resource commonly consists of:
| Resource | Purpose |
|---|---|
.gfx |
Raw Atari ST four-plane pixels. |
.offsets |
Big-endian byte offsets selecting pictures within the .gfx block. |
.positions |
Packed X, Y, width and height records. For these wall resources each four-byte record is x / 2, y, width in 16-pixel words minus 1, height minus 1. |
.colours |
Optional palette/remapping data used by the component. |
Gem sockets are a useful example. Main_Slots.gfx supplies the pixels, Main_Slots.offsets selects the socket picture, Main_Slots.positions positions and sizes it for each wall-face view, and Main_Slots.colours supplies its colour choices.
The shared face tables described above do not replace the socket position table. They determine which projected wall face is being drawn and which transformation that face uses; Main_Slots.positions then provides the component's exact rectangle within that projected face. The same arrangement applies to signs and switches.
Wall-component colours and states
Wall signs, switches and sockets use four-byte colour-remapping entries. The map data selects the entry and also supplies the component state:
- an empty socket retains the socket artwork but clears the final replacement-colour byte, removing the inserted gem;
- a dim/used switch transforms its selected mask from
[a,b,c,d]to[0,d,0,c]; - sign references 1–4 select the Serpent, Dragon, Moon and Chaos emblem sheets respectively;
- sign references 5 and above use the wall-scroll path instead of an emblem overlay.
Switch reference zero takes a special branch and leaves its complete replacement mask black. Non-zero switches select one of the eight GFX_Switches_Colours records using (map X + map Y) & 7.
Generated signs and wall scrolls use (map X + map Y) & 7 for their colour record. A reference-zero generated sign independently selects one of four emblems using (2 * map X - map Y) & 3. The four named tower signs retain their fixed emblem and colour records.
The eight socket colour entries correspond to Serpent green, Chaos yellow, Dragon red, Moon blue, unused grey, bluish teleport, unused brown and tan teleport gems.
Pits, trigger pads and ceiling holes
Type-6 floor and ceiling features are independent. Bit 2 of the first map byte is tested first, allowing a ceiling hole to coexist with either a floor pit or a trigger pad.
The extracted resources are named for what is ultimately drawn:
Ceiling_Hole.gfxis the ceiling-hole artwork;Trigger_Pad.gfxis the recolourable trigger-pad template;Floor_Pit.gfxis the floor-pit artwork.
Floor pits and trigger pads share FloorPit_TriggerPad.offsets and
FloorPit_TriggerPad.positions. Ceiling holes use their own
Ceiling_Hole.offsets and Ceiling_Hole.positions.
Set_TriggerPad_ColourMask at $0094DC loads the normal trigger-pad mask $01050406. This value is embedded in an instruction rather than stored as a separate table.
All three type-6 resources contain a nineteenth position record for the player's square. The centred-component lookup bypasses its ordinary sentinel and directly selects source picture 11, allowing the current-cell floor feature and ceiling hole to coexist.
The complete graphic blocks include the final inside-view pictures:
| Resource | SPS 439 start | Size |
|---|---|---|
Door_Open.gfx |
$02D2DC |
$1B68 (7016) |
Ceiling_Hole.gfx |
$031BE4 |
$01B8 (440) |
Each resource ends exactly at the next labelled block. The final bytes are graphic pixels, not separate lookup data.
Magic-location drawing
Map type 7 does not provide three independent graphic sheets:
- Firepath reuses the trigger-pad template and flashes between
$090C0B0Dand$090A0B0DfromGFX_Firepath_ColourMasksat$0094D4; - Mindrock conditionally follows the ordinary stone-wall draw path;
- Formwall follows the solid stone-wall draw path.
The two Firepath longwords must remain as eight bytes of addressable table data because the code selects one with an indexed load. They may nevertheless be named as Firepath_ColourMask_Frame1 and Firepath_ColourMask_Frame2 in a source representation.
Shared-table locations in SPS 439
The coordinate table and the two mask tables occupy one contiguous region. The table below gives their boundaries alongside the other shared drawing tables. Descriptive names match those used elsewhere on this page.
| Memory address | SPS 439 file position | Size | Name used here | Resource |
|---|---|---|---|---|
$0094D4 |
$09150 |
$08 |
GFX_Firepath_ColourMasks |
gfx-data/Firepath.colours |
$0095C0 |
$0923C |
$14 |
GFX_CentredDungeonComponent_SpriteMirrorTable |
gfx-data/Dungeon_CentredComponents.lookup |
$00B2BA |
$0AF36 |
$1C |
GFX_Main_Wall_SpriteTable |
gfx-data/Dungeon_MainWall_SpriteSelection.lookup |
$00B43C |
$0B0B8 |
$1C |
GFX_WallComponent_SpriteMirrorTable |
gfx-data/Dungeon_WallComponents.lookup |
$00B4C4 |
$0B140 |
$1C |
GFX_WallComponent_DrawTransformFlags |
gfx-data/Dungeon_WallComponent_DrawTransform.flags |
$00B558 |
$0B1D4 |
$08 |
GFX_WallComponent_PerspectiveTrimLookup |
gfx-data/Dungeon_WallComponent_PerspectiveTrim.lookup |
$00B64A |
$0B2C6 |
$1C |
GFX_Main_Wall_DrawTransformFlags |
gfx-data/Dungeon_MainWall_DrawTransform.flags |
$00B6F2 |
$0B36E |
$08 |
GFX_Main_Wall_PerspectiveTrimLookup |
gfx-data/Dungeon_MainWall_PerspectiveTrim.lookup |
$00B8AE |
$0B52A |
$98 |
Dungeon_ViewCell_RelativeCoordinates |
gfx-data/Dungeon_ViewCell_RelativeCoordinates.positions |
$00B946 |
$0B5C2 |
$4C |
Dungeon_ViewCell_OcclusionMasks |
gfx-data/Dungeon_ViewCell_Occlusion.flags |
$00B992 |
$0B60E |
$4C |
Dungeon_ViewCell_VisibleFaceMasks |
gfx-data/Dungeon_ViewCell_VisibleFaces.flags |
$00B9DE |
$0B65A |
$14 |
Dungeon_ViewCell_CentredSlots |
gfx-data/Dungeon_ViewCell_CentredSlots.lookup |
$00B9F2 |
$0B66E |
$4C |
Dungeon_ViewCell_WallFaceSlots |
gfx-data/Dungeon_ViewCell_WallFaces.lookup |
The reverse traversal refers to the final longword of each mask block, at memory $00B98E and $00B9DA. Each is $48 bytes from its mask-table base: Dungeon_ViewCell_OcclusionMasks+$48 and Dungeon_ViewCell_VisibleFaceMasks+$48.
Door_Lock_Colours at memory $00B2D6 / file position $0AF52 is the adjacent eight-byte door-colour table, represented by data/Door_Lock.colours.
Relevant routine responsibilities
The names below describe each routine's responsibility; the addresses identify its location in SPS 439:
| Source address | Name used here | Responsibility |
|---|---|---|
$0090D4 |
Draw_DungeonViewport |
Scans the 19 relative cells, builds the global masks and draws the surviving cells. |
$00921E |
Draw_DungeonViewCell |
Resolves and draws one player-relative view cell. |
$009388 |
Draw_DungeonLocation_ByType |
Dispatches map types and iterates their candidate wall faces. |
$0094BC |
Select_Firepath_ColourMask |
Randomly selects one of the two Firepath masks, then enters the shared pad/pit draw path. |
$0094DC |
Set_TriggerPad_ColourMask |
Loads the ordinary trigger-pad colour mask $01050406. |
$0094E6 |
Draw_PitPadAndCeilingHole |
Resolves the centred view slot, draws an optional ceiling hole, then selects the floor pit or trigger-pad artwork. |
$0095D4 |
Resolve_DungeonWallFaceDirection |
Converts the current face slot and player facing to N/E/S/W. |
$00B2A4 |
Draw_MainWallFace |
Selects and draws one stone-wall face. |
$00B1C6 |
Select_MainSwitch_ColourMask |
Selects the switch palette table and enters the coordinate-derived colour lookup. |
$00B1CC |
Calculate_WallOverlay_ColourIndex |
Calculates map X + map Y for generated sign, scroll and switch colouring. |
$00B1D4 |
Load_WallOverlay_ColourMask |
Masks the colour index to three bits and loads one four-byte palette record. |
$00B2DE |
Draw_Main_Door_Or_Stairs |
Selects open/closed door artwork and lock mask, or dispatches the shared stair path. |
$00B3D8 |
Draw_WoodenWallOrDoorFace |
Selects solid wall, open frame and optional closed-door overlay. |
$00B410 |
Draw_WallComponentFace |
Selects the wall-component picture and normal, mirrored or two-half path. |
$00B458 |
Draw_WallComponent_TwoHalves |
Draws one source half plus its reflected partner. |
$00B474 |
Select_MainWallGraphicTables |
Selects the main-wall .gfx, .offsets and .positions tables. |
$00B486 |
Prepare_WallSpriteDraw |
Resolves picture offset and packed position into source/destination pointers and dimensions. |
$00B4E0 |
Draw_WallComponent_Transformed |
Applies wall-component transform flags and centre trimming. |
$00B560 |
Draw_WallComponent_EdgeTransform |
Draws the component's perspective edge pass. |
$00B666 |
Draw_MainWall_Transformed |
Applies main-wall transform flags, reversal and centre trimming. |
$00B6FA |
Draw_MainWall_EdgeTransform |
Draws the main wall's reversed perspective edge pass. |
$00B76E |
Draw_WallSprite_BitReversed |
Writes normal wall rows after horizontal bit reversal. |
See also:
- Map Location Data - (1) Stone Walls
- Map Location Data - (2) Wooden Walls
- Atari ST Raw Data Format
- Extracted Data File Types
Floor-object projection
Floor objects are projected independently of the wall faces. The game first rotates the selected mini-space for the player’s facing, adds the front/back depth bias, and looks up a projection group. A projection group selects one of five source distances; a separate X-position entry and the ViewY table then place the graphic on the floor. Shape-specific Y adjustments are applied last.
The extracted graphics therefore need the complete set of companion tables,
not just ObjectsOnFloor.gfx and its graphic offsets. The relevant resources
are listed in Extracted Data File Types.
Character, monster and spell compositor
The dungeon renderer does not draw actors in a separate final overlay. The
source calls Draw_DungeonCellOccupants at particular points in the dungeon
cell traversal, so scenery and actors have an explicit painter order.
The most important case is a wooden wall or door. Resolve_WoodenWallFace
at $0093E4 tests each candidate face and calls the actor compositor at
$009400 only after the already-visited rear faces and before it selects and
draws the remaining foreground wooden face. An actor behind that foreground
face is consequently obscured by it. This ordering comes from a call inside the original wooden-face loop.
Draw_DungeonCellOccupants at $0099F0 first resolves the centred view-cell
slot and its visibility bit, then calls the location lookup. A player,
champion, monster, or airborne spell can therefore be drawn only when that
centred slot is available in the current view. The routine does not establish
a global actor pass after all wall art has been painted.
For live monsters, byte $02 has two rendering uses. Its high nibble is
shifted down and converted to view-relative facing by subtracting the player
direction and masking to two bits. Its low two bits select a team mini-space.
When live byte $0D names a team, the compositor visits the four runtime
team-index slots and rotates each selected mini-space by the player's direction
before projecting it. This explains why a party or monster group must keep
its authored slots stable as the player moves: positions are transformed from
the members' slots, not re-sorted by screen order.
Packed monster records do not retain this runtime byte. The tower-entry
unpacker clears live $02; their first rendered appearance therefore uses
the source's cleared/default orientation until game movement writes runtime
state. The separate packed KL team byte still supplies the authored member
slot used to rebuild the team index.