Crusader_Decomp/docs/raw-0007-rendering.md
MaddoScientisto 3daffbf113 Add extractor for Crusader's EUSECODE.FLX container
- Implemented a Python script to extract data from the EUSECODE.FLX file format.
- Defined data structures for candidate entries and extracted chunks using dataclasses.
- Added functions to read and parse the FLX table, extract candidate data, and generate human-readable output files.
- Included functionality for analyzing extracted data, including generating summaries, descriptors, and event family reports.
- Implemented utilities for calculating printable ratios, zero ratios, and identifying text-like data.
- Added support for writing various output formats, including JSON, TSV, and Markdown.
2026-03-22 14:27:38 +01:00

17 KiB
Raw Permalink Blame History

Raw 0007: Rendering, Scroll/Camera & Coordinate Transforms

Content extracted from crusader_decompilation_notes.md. Covers the sprite draw list subsystem, tile-based visibility, map scroll/camera, and the isometric coordinate transform system.


Raw 0007 Rendering & Sprite Draw List Subsystem

Draw List Node Format

Each draw node is 0x2a (42) bytes. The free pool holds 1500 nodes based at 0x2cc7.

Offset Field
+8 child link (for drawlist_enqueue_sprite_children)
+10 z-order dependency forward link
+0x14 enqueued flag (bit 0)
+0x15 z-depth (1 = root sentinel)
+0x16 flags/type byte
+0x17 redraw must-flag

Draw List Functions

Address Name Evidence
0007:eb36 drawlist_pool_init Inits free pool: 1500 nodes × 0x2a bytes from base 0x2cc7. Sets 0x2ccf = 1500 (count), 0x2cc3 = linked list head. Copies 0x2cc90x2cae.
0007:eb12 linked_list_push_2cc3 LIFO push: node->next = head_2cc3; head = node; count_2ccf++.
0007:eada linked_list_pop_2cc3 LIFO pop: dequeues node from 0x2cc3 head, decrements 0x2ccf count.
0007:ebd9 linked_list_dequeue_headtail FIFO dequeue from head/tail pair list; calls FUN_0007_03ec after dequeue.
0007:ec2c drawlist_enqueue FIFO enqueue: appends node to draw list (head if empty, tail always updated).
0007:ec63 drawlist_remove_node Unlinks specific node from head or tail position; calls FUN_0007_03ec.
0007:eca8 drawlist_process_and_render Two-stage render pass. Stage 1: drains 0x8442 (pending), viewport-tests each sprite, moves in-bounds to 0x8446 (visible). Stage 2: drains 0x8446, calls sprite vtable[0] to draw. Recursive re-run if 0x2cb2 defer flag set.
0007:edfa drawlist_enqueue_sprite_children Enqueues all child sprites at node +8 linkage to draw pending list if not already queued or rendered. Skips if +0x15 z-depth == 1.
0007:ef3d sprite_add_draw_dependency Links sprites A and B with z-order dependency. Allocates link node via FUN_0007_057f. Stores in A's +10 and B's +8. Sets must-redraw on B if A has +0x17 set.
0007:ef9f sprite_enqueue_for_draw Null-guard; sets node +0x14 |= 1, enqueues to 0x8442.
0007:efca sprite_invalidate_and_unlink Full sprite removal: re-enqueues all dependents, frees link nodes via FUN_0007_0614, clears +8/+10 lists, dequeues self from 0x8442.
0007:f2a0 sprite_sort_by_depth_and_link Compares +0x15 z-depth of two sprites; calls sprite_add_draw_dependency in the correct order (lower z first). Handles equal-depth via thunk.
0007:f654 drawlist_init Full draw system init: drawlist_pool_init, init full-screen viewport (0x844a..0x8450 = 0..639, 0..screen_h), allocate root sentinel node (vtable FUN_0000_2ce4, z-depth=1, flag=0x40), stored at 0x846a.
0007:ea00 bbox_intersect In-place 2D rect intersection: max of mins, min of maxes. Input/output: 4-int array [xmin,ymin,xmax,ymax].
0007:ea6d bbox_union In-place 2D rect union: min of mins, max of maxes.
0007:ee5a viewport_update_from_sprite_bounds Subtracts scroll offsets from sprite bbox; clips to screen rect (0x2ca6..0x2cac); calls bbox_intersect; stores updated viewport at 0x844a..0x8450; dispatches to render.

Isometric Coordinate Transform (draw list side)

Address Name Evidence
0007:be67 world_to_screen_isometric Coarse tile-grid → screen transform. Uses (wx+sx) + (wy+sy)*2 for screen_x, (wy+sy)*2 - (wx+sx) for screen_y. Camera (sx, sy) added before transform.
0007:bef8 world_to_screen_isometric_wrapper Thin wrapper around world_to_screen_isometric.

Tile-Based Visibility System

The rendering system uses a 6×5 tile grid at DS:0x846a (30 entries × 2 bytes each = 60 bytes). Tiles represent screen regions of approximately 128 screen pixels per tile. Each tile holds a near pointer to a linked list of sprite nodes overlapping that tile. Bitmasks track dirty and renderable tiles.

Address Name Evidence
0007:f9e2 drawlist_mark_dirty_tiles Converts bbox to tile grid coords (divide by 128, clamp to 6×5). Walks all sprites in overlapping tiles' spatial buckets; re-enqueues overlapping sprites. Sets bits in dirty bitmask 0x2cbb.
0007:fb53 tile_visibility_update Iterates redraw bitmask (0x2cbb, up to 10240 tiles). For each dirty tile with a sprite, computes isometric screen position from map X/Y/Z. Checks sprite dimension bitfields at pbVar8+2..+3 against 640×480 viewport. Sets corresponding bit in render bitmask 0x2cb7. Clears dirty bitmask after processing.
0007:fd98 tilemap_draw_if_dirty Guard: if render bitmask 0x2cb7 non-zero, calls thunk_FUN_0007_001d to trigger tilemap draw.

Draw List / Viewport Globals

Address Name Notes
0x2bb7 g_scroll_offset_x World-to-screen X offset (scroll)
0x2bb9 g_scroll_offset_y World-to-screen Y offset (scroll)
0x2ca6..0x2cac g_screen_clip_rect Screen clip rectangle [xmin,ymin,xmax,ymax]
0x2cae g_draw_segment DS segment for draw node far pointers
0x2cb0 g_free_pool_seg Segment for node free pool far calls
0x2cb2 g_render_defer_flag If set, defers current render pass
0x2cb7 g_render_tile_bitmask Far ptr to bitmask of tiles ready to render
0x2cbb g_dirty_tile_bitmask Far ptr to bitmask of dirty/changed tiles
0x2cbf g_tile_origin_x Tile grid world origin X (for tile coordinate math)
0x2cc1 g_tile_origin_y Tile grid world origin Y
0x2cc3 g_free_pool_head Free node pool linked list head
0x2ccf g_free_pool_count Free node pool remaining count (max 1500)
0x8442 g_draw_pending_list Draw pending list head/tail (near ptrs)
0x8446 g_draw_visible_list Draw visible list head/tail (near ptrs)
0x844a..0x8450 g_viewport_rect Current viewport [xmin,ymin,xmax,ymax]
0x846a g_tile_grid_base 6×5 tile grid spatial index (near ptr per tile)

Raw 0007 Map Scroll / Camera Subsystem

A scroll/camera management cluster found in the 0007:bxxx0007:dxxx range.

Scroll/Camera Functions

Address Name Evidence
0007:ba00 watch_entity_controller_create_global Thin global-create wrapper around watch_entity_controller_create; allocates default controller object and stores it at 0x2bd8.
0007:ba13 watch_entity_controller_dispatch_if_present If 0x2bd8 is non-null, calls controller vtable slots +0x2c and +0x30.
0007:ba45 watch_entity_controller_create Allocates/initializes a type 0x2c2b controller object, stores it at 0x2bd8, sets event type 0x0219, and installs callback-table entry 0x2be4 through 0x39ca.
0007:bab5 entity_set_watch_ptr Legacy name still in place, but newer constructor evidence now shows 0x2bd8 is a controller object lane rather than just a raw watched-entity FAR pointer.
0007:baea camera_update_and_check_player_scroll Calls watch entity vtable +0x24; if 0x2bd1 flag clear checks if player position (from g_player_entity_farptr+0x40) has moved > 32 units since 0x2be0; if so, updates 0x2be0 and conditionally dispatches scroll event via 0x45aa.
0007:c6ba scroll_camera_set_state_params Stores word params to 0x8354, 0x8356, byte to 0x8358; dispatches.
0007:cd56 dispatch_if_flag_2bd3_set Returns unless 0x2bd3 non-zero.
0007:cfef dispatch_if_mode_flags_set Two-flag check: dispatches if 0x2bca or 0xee0 is non-zero.
0007:d0f6 scroll_call_set_params_unless_blocked Calls scroll_camera_set_state_params only if 0x2bbb == 0.
0007:d119 scroll_update_direction_tracking Guards on 0x2bd3. Calls scroll_call_set_params_unless_blocked. Compares direction bytes from 0x2cf4/0x2cf5 against cached 0x2bbd/0x2bbe; if changed, clears 0x2bbc. Dispatches.
0007:d4a5 scroll_set_option_value Sets 0x2bc6 = param_1.
0007:d4b0 scroll_set_params_default Unconditional call to scroll_camera_set_state_params.
0007:d4d3 scroll_set_map_index_validated If param_1 in [0..250] and differs from 0x2bbf, updates 0x2bbf and clears 0x2bbc/0x2bbb.
0007:d655 map_position_has_changed Compares map arrays 0x7ded/0x7df1/0x7df5 at index 0x2bc6 against cached 0x2bc1/0x2bc3/0x2bc5. Returns 1 if changed, 0 if same.
0007:d6b1 scroll_clear_dirty_flags_and_dispatch Clears 0x2bbb = 0 and 0x2bbc = 0; dispatches.
0007:de57 entity_check_player_range_and_update Reads player world position (g_player_entity_farptr+0x40); if moved > 59 units from entity+0x32 (cached pos), updates cache and calls scrollregion_find_and_dispatch.

Scroll Region Table (0x835a)

8 entries × 0x19 (25) bytes = 200 bytes.

Offset Field Notes
+0x0/+0x2 key pair Matched by scroll region key lookup
+0x8 refcount Reference count (incremented on match)
+0xe count2 Secondary counter
+0x10 active Non-zero when region is live
+0x11/+0x13 x_start/x_end Bounding X coordinates
+0x15/+0x17 y_start/y_end Bounding Y coordinates
Address Name Evidence
0007:e194 scrollregion_process_active Iterates active scroll entries (key non-zero AND +0x10 != 0), reads bounding box fields +0x11/+0x13/+0x15/+0x17, dispatches with bounds args.
0007:e214 scrollregion_find_and_dispatch Finds empty scroll entry (zero key) and dispatches.
0007:e29c scrollregion_register Finds or allocates scroll region entry. Existing match: bump refcount / set active. New: init via scrollregion_entry_init. Special key 0x4ed: thunk dispatch instead.
0007:e50f scrollregion_entry_init Null-guards param_1; zeroes output param_2, param_3, param_4; dispatches continuation.
0007:e74a (unnamed) Sets up call to walk 0x835a table (8 entries, stride 0x19, filter 0x968), with callback return label e763.

Save Slot System (0x8337 + 0x2ba3)

10 save slots, each 0x400 (1024) bytes. Handle table at 0x8337 (10 words). Slot buffer base at FAR ptr 0x2ba3.

Address Name Evidence
0007:ac13 saveslot_table_clear Fills 0x8337..0x834b (10 words) with 0xFFFF (all slots empty).
0007:acab saveslot_free_if_empty Scans slot 0x2ba3[param_1*0x400] for non-zero data; if empty, sets handle 0x8337[param_1] = 0xFFFF.
0007:ad47 saveslot_find_index_by_id Linear scan of 10-word handle table 0x8337; returns index of matching word or -1.
0007:ad79 (unnamed) Finds a free (0xFFFF) slot index. Complement of saveslot_find_index_by_id.
0007:afd4 saveslot_get_or_alloc Gets slot pointer: calls saveslot_find_index_by_id; if not found calls ad79 to get free slot; returns 0x2ba3 + slot * 0x400. Returns 0 if no free slot.
0007:b02c saveslot_write_entry Navigates to slot_base[param_3 * 4]; dispatches thunk paths for write (existing, overwrite, new).
0007:b0de saveslot_read_entry_flags Reads from slot entry far pointer at slot_base[param_3*4]; extracts 4-byte packed bitfield from +4..+7 in entry record into *param_1. Bit-by-bit extraction loop for 4 bytes.

String & Memory Utilities

Address Name Evidence
0007:a96d entity_copy_string_truncated80 Strlen(param_3) ≤ 0x50 guard; copies string word-by-word from param_3 into param_2+8.
0007:b813 memcpy_4words Copies 4 words (8 bytes) from param_2 to param_1.
0007:b46d entity_dispatch_if_slot82e2_valid Guard: if *(int *)0x82e2 != -1, calls dispatch thunk.

Scroll/Camera Globals

Address Name Notes
0x2bb7 g_scroll_offset_x Isometric scroll X — added to world_x in screen transform
0x2bb9 g_scroll_offset_y Isometric scroll Y
0x2bbb g_scroll_blocked If non-zero, blocks scroll_camera_set_state_params call
0x2bbc g_scroll_dirty Scroll direction changed flag (cleared when direction tracks)
0x2bbd g_scroll_dir_x Cached scroll direction X
0x2bbe g_scroll_dir_y Cached scroll direction Y
0x2bbf g_map_index Current map/level index [0..250]
0x2bc1/0x2bc3/0x2bc5 g_map_entry_x/y/z Cached map entry X/Y/Z (vs. live map arrays)
0x2bc6 g_map_slot_index Index into 0x7ded/0x7df1/0x7df5 arrays for current map slot
0x2bca/0x2bc9 g_option_toggle_state UI option toggle state flags
0x2bd1 g_scroll_block_flag Blocks camera update path if non-zero
0x2bd3 g_scroll_active Non-zero = scroll system active
0x2bd8 g_watch_entity_controller_farptr FAR ptr to the watch/camera controller object
0x2be0 g_player_scroll_pos Cached player world X+Y (ulong) for scroll threshold detection
0x8354..0x8358 g_scroll_state_params Three scroll state params (word, word, byte)

Deep Analysis: Coordinate Transform System

world_to_screen_coords at 0004:e7bd (NE seg018:07bd)

Signature:

void world_to_screen_coords(int world_x, int world_y, int *screen_x, int *screen_y)

Isometric Projection Math:

screen_x = (world_x - world_y) / 2 - camera_x    // SAR 1 (signed divide)
screen_y = (world_x + world_y) / 4 - camera_y    // SHR 2 (unsigned divide)

Camera globals: g_scroll_offset_x (DS:0x2bb7), g_scroll_offset_y (DS:0x2bb9).

Assembly detail:

  • SAR AX, 1 for screen_x — signed arithmetic shift preserves sign for negative (world_x - world_y) differences
  • SHR AX, 2 for screen_y — unsigned logical shift (sum world_x + world_y is always positive)
  • The 2:1 ratio (÷2 for X, ÷4 for Y) produces the classic 2:1 isometric diamond tile shape

Coordinate axes on screen:

  • World X axis → lower-right on screen (+0.5 screen_x, +0.25 screen_y per world unit)
  • World Y axis → lower-left on screen (-0.5 screen_x, +0.25 screen_y per world unit)
  • Camera subtraction converts absolute world-space to viewport-relative screen coordinates

Callers (17 across 8 NE segments):

Call site NE Segment Context
0004:7d6f seg012 Map/tile rendering
0005:0305 seg021 Entity system
0005:432f seg021 Entity placement
0005:4457 seg021 Entity placement
0005:6f8f seg022 Entity rendering
0005:7263 seg022 Entity rendering
0007:2262 seg040 snap_entity_to_ground — ground alignment
0007:237d seg040 Ground snap dispatch
0007:cf4e seg049 Entity positioning
0007:d039 seg049 Entity positioning
0007:d43f seg049 Entity positioning
0007:d6fe seg049 Entity positioning
0008:3223 seg053 Entity-to-screen render setup
0008:32e7 seg053 Entity-to-screen render setup
0008:334b seg053 Entity-to-screen render setup
000b:858b seg115 Sprite system
000b:f100 seg120 Sprite system

Entity struct layout (from seg053 caller at 0008:31f6):

entity_array_base = far ptr at [DS:0x2cff]
entity_struct_size = 19 bytes (0x13)
entity.world_x = offset +0x0a (word)
entity.world_y = offset +0x0c (word)

Comparison: Two Coordinate Transform Functions

Property world_to_screen_coords (0004:e7bd) world_to_screen_isometric (0007:be67)
Input type Fine-grained world units (entity positions) Coarse tile-grid units (map rendering)
screen_x (wx - wy) / 2 - cam_x (wx + sx) + (wy + sy) * 2
screen_y (wx + wy) / 4 - cam_y (wy + sy) * 2 - (wx + sx)
Camera handling Subtracted after transform Added before transform
Operations Division (SAR/SHR) Multiplication (SHL)
Aspect ratio 2:1 (from /2 : /4) 2:1 (from 1 : 2 multipliers)

Both functions implement the same 2:1 isometric projection but at different coordinate scales. world_to_screen_coords divides down from fine world units while world_to_screen_isometric multiplies up from coarse tile units.

Adjacent Function: map_position_equal at 0004:e784

Compares two 5-byte map_position structs: { x:word, y:word, layer:byte }. Returns 1 (AL) if all three fields match, 0 otherwise. Located immediately before world_to_screen_coords in seg018.