From 78690755b0b068b1b9a0f742ebf1cb86d407e1a9 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Thu, 20 Aug 2026 19:41:53 +0000 Subject: [PATCH 1/7] world: decompile six we_object4 functions and settle their types func_800A9300 particle renderer: steps and projects every live slot in the D_800D9CB0 pool, sizes the sprite from the slot's 4.12 scale, and emits a TILE or POLY_FT4 into the scene's OT. func_800A8C1C colour-zone palette blender: walks the D_800C9EE8 zone list and fades the five palette stops and the GTE colour matrix towards whichever zone the camera is inside, via GTE INTPL. func_800A7590 backdrop OT linker: pushes the D_800D42D0 record pool and the fixed prims onto two bone slots of the ordering table. func_800A64DC strip sub-OT builder: expands four VRAM strips per band into two depth-cued copies and packs a DR_MOVE into every sub-slot, then chains each record's slots into a mini ordering table. func_800A6BE0 strip-pool primer: stages each band's strips into scratch VRAM, builds the draw environments and quads, and points every DR_LOAD at its 1x1 source pixel. func_800AB06C kind-11 particle spawner at a fixed world position. All six are dead code in the retail build -- no jal in any remaining assembly, no C caller, and their addresses appear nowhere in world.bin, which also covers dispatch tables. They are documented as such, and the reachability notes on the functions already in this file were corrected to match: func_800A9CC0 is now called from C, and func_800A6A74 was never called at all. Types the decomp settled: Slot30 +0x10 was padding but is an SVECTOR holding the camera-relative position; +0x20 was proj_x/y/z but is the per-tick velocity the spawner seeds and func_800A9CC0 integrates; +0x2C was "life" but is a 4.12 scale that spawns at PARTICLE_SCALE_ONE (= 1.0). SLOT_FLAG_JITTER_LIFE is renamed SLOT_FLAG_JITTER_SCALE to match, confirmed against func_800AC0A0. KindParams grows from two known fields to the full per-kind sprite descriptor: texture u/v, CLUT, size, prim kind, colour, frame count and divisor, scale rate, billboard shape and velocity increment. func_800A9CC0's second parameter was typed WorldParticle * -- the call site proves it is a KindParams *. WorldParticle was a duplicate view of Slot30 and is deleted. TILE::w,h and SPRT::w,h were u16 where the SDK declares short; the target reads them with lh, so the unsigned form cost the match. SDK additions, all verbatim from Psy-Q - 46/INCLUDE/: gte_sttr, gte_ReadRotMatrix, gte_ldlvl, gte_ldfc, gte_SetFarColor, gte_lddp, gte_strgb and gte_intpl (re-encoded as a real COP2 word per this header's convention), plus DR_MOVE, DR_LOAD, SetDrawMove, SetDrawLoad, SetDrawEnv and setPolyF4. SetDrawMove, SetDrawLoad and SetDrawEnv also needed adding to symbols.extern.txt or the overlay fails to link. Retired three raw names that were really SDK entry points, each of which was a second declaration of an already-named symbol: func_8003F4A4 is SquareRoot0 (10 sites; the duplicate in common.h is gone), func_800491E8 is DrawPrim, and func_80041E84 is ratan2 -- which had three declarations under two names and was misfiled in an overlay header, so it moves to libgte.h beside rsin/rcos. StoreImage was being called with no declaration at all. func_800A8C1C keeps a forward goto to reach the shared exit. The structured alternative costs three instructions and cannot match at any percentage; the tail-duplication route only reaches 95% because gcc forwards the just-stored bytes in the default path. Approved deliberately and noted in the source. rm -rf build asm && make split && make verify: all 25 targets Match. --- config/symbols.extern.txt | 3 + include/common.h | 1 - include/psxsdk/libgpu.h | 27 +- include/psxsdk/libgte.h | 95 ++- include/world.h | 62 +- include/world/we_object10.h | 3 +- include/world/we_object5.h | 5 +- src/field/fe_object1.c | 8 +- src/world/we_object4.c | 1317 ++++++++++++++++++++++++++++++++--- src/world/we_object5.c | 34 +- 10 files changed, 1426 insertions(+), 129 deletions(-) diff --git a/config/symbols.extern.txt b/config/symbols.extern.txt index 29f831ce..e4dfcaf4 100644 --- a/config/symbols.extern.txt +++ b/config/symbols.extern.txt @@ -71,6 +71,7 @@ PutDrawEnv = 0x800492B4; PutDispEnv = 0x80049480; GetODE = 0x800499AC; SetDrawArea = 0x80049A14; +SetDrawEnv = 0x80049B78; OpenTIM = 0x8004B8F4; ReadTIM = 0x8004B904; GetTPage = 0x8004D524; @@ -80,6 +81,8 @@ SetShadeTex = 0x8004D634; SetSprt = 0x8004D6E4; SetTile = 0x8004D704; SetDrawTPage = 0x8004D724; +SetDrawMove = 0x8004D754; +SetDrawLoad = 0x8004D7B4; g_gameState = 0x80077378; g_characterMagic = 0x80077818; g_characterAbilities = 0x80077864; diff --git a/include/common.h b/include/common.h index d0b551f3..4324fdc3 100644 --- a/include/common.h +++ b/include/common.h @@ -216,7 +216,6 @@ extern EncounterParams D_80082C90; extern void func_80047C74(u8 *dst, u8 *src); /** @brief Integer square root of @p a (main-binary helper). */ -extern s32 func_8003F4A4(s32 a); /** @brief Poll the GPU; returns non-zero while it is still busy. * Callers that only need the side effect ignore the result. */ diff --git a/include/psxsdk/libgpu.h b/include/psxsdk/libgpu.h index 4082a40d..90385b82 100644 --- a/include/psxsdk/libgpu.h +++ b/include/psxsdk/libgpu.h @@ -91,6 +91,9 @@ typedef struct { (p)->u2 = (_u0), (p)->v2 = (_v0) + (_h), \ (p)->u3 = (_u0) + (_w), (p)->v3 = (_v0) + (_h)) +/* Initialise a flat-shaded 4-vertex polygon (len=5 words, code=0x28). */ +#define setPolyF4(p) setlen(p, 5), setcode(p, 0x28) + /* Initialise a textured 4-vertex polygon primitive (len=9 words, code=0x2C). */ #define setPolyFT4(p) setlen(p, 9), setcode(p, 0x2c) @@ -151,7 +154,7 @@ typedef struct { u32 tag; u8 r0, g0, b0, code; s16 x0, y0; - u16 w, h; + s16 w, h; } TILE; /** @brief 1x1 tile — fixed-size TILE with no w/h. Code 0x68. */ @@ -243,6 +246,19 @@ typedef struct { u32 code[1]; } DR_TPAGE; +/** @brief Packed VRAM-to-VRAM move command, built by @ref SetDrawMove. */ +typedef struct { + u32 tag; + u32 code[5]; +} DR_MOVE; + +/** @brief Packed VRAM upload command, built by @ref SetDrawLoad. */ +typedef struct { + u32 tag; + u32 code[3]; + u32 p[13]; +} DR_LOAD; + /** @brief Flat-shaded triangle. Code 0x20. */ typedef struct { u32 tag; @@ -379,7 +395,7 @@ typedef struct { u8 r0, g0, b0, code; s16 x0, y0; u8 u0, v0; u16 clut; - u16 w, h; + s16 w, h; } SPRT; /** @brief 16x16 sprite. Code 0x7C. */ @@ -420,6 +436,13 @@ void ClearOTagR(u32 *ot, s32 n); void DrawOTag(void *p); void DrawPrim(void *p); void LoadImage(RECT *rect, u32 *data); +void StoreImage(RECT *rect, u32 *data); +/* Pack a VRAM-to-VRAM move of @p rect to (@p x, @p y) into the primitive @p p. */ +void SetDrawMove(DR_MOVE *p, RECT *rect, s32 x, s32 y); +/* Pack an upload of @p rect's pixels into the primitive @p p. */ +void SetDrawLoad(DR_LOAD *p, RECT *rect); +/* Pack the drawing environment @p env into the primitive @p dr_env. */ +void SetDrawEnv(DR_ENV *dr_env, DRAWENV *env); DRAWENV *SetDefDrawEnv(DRAWENV *env, s32 x, s32 y, s32 w, s32 h); DISPENV *SetDefDispEnv(DISPENV *env, s32 x, s32 y, s32 w, s32 h); void PutDrawEnv(void *env); diff --git a/include/psxsdk/libgte.h b/include/psxsdk/libgte.h index 8a9bb14b..d8301244 100644 --- a/include/psxsdk/libgte.h +++ b/include/psxsdk/libgte.h @@ -78,6 +78,7 @@ s32 SquareRoot12(s32 a); /**< Square root of a 20.12 fixed-point value. */ s32 VectorNormal(VECTOR *v0, VECTOR *v1); /**< Normalise @p v0 into @p v1; returns the squared length. */ s32 rsin(s32 a); s32 rcos(s32 a); +s32 ratan2(s32 y, s32 x); /**< Angle of (x, y); 0x1000 = full circle. */ /* --- Inline GTE (COP2) opcode macros --- * @@ -115,6 +116,43 @@ s32 rcos(s32 a); : "r"( r0 ) \ : "$12", "$13", "$14" ) +/* Read the rotation matrix back out of the GTE into the MATRIX @p r0 -- + * the 3x3 at +0 and the translation vector at +20, so it is the inverse of + * gte_SetRotMatrix plus gte_SetTransMatrix in one go. */ +#define gte_ReadRotMatrix( r0 ) __asm__ volatile ( \ + "cfc2 $12, $0;" \ + "cfc2 $13, $1;" \ + "sw $12, 0( %0 );" \ + "sw $13, 4( %0 );" \ + "cfc2 $12, $2;" \ + "cfc2 $13, $3;" \ + "cfc2 $14, $4;" \ + "sw $12, 8( %0 );" \ + "sw $13, 12( %0 );" \ + "sw $14, 16( %0 );" \ + "cfc2 $12, $5;" \ + "cfc2 $13, $6;" \ + "cfc2 $14, $7;" \ + "sw $12, 20( %0 );" \ + "sw $13, 24( %0 );" \ + "sw $14, 28( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "$13", "$14", "memory" ) + +/* Read the translation vector back out of the GTE into @p r0 (inverse of + * gte_SetTransVector). */ +#define gte_sttr( r0 ) __asm__ volatile ( \ + "cfc2 $12, $5;" \ + "cfc2 $13, $6;" \ + "cfc2 $14, $7;" \ + "sw $12, 0( %0 );" \ + "sw $13, 4( %0 );" \ + "sw $14, 8( %0 )" \ + : \ + : "r"( r0 ) \ + : "$12", "$13", "$14", "memory" ) + #define gte_SetTransVector( r0 ) __asm__ volatile ( \ "lw $12, 0( %0 );" \ "lw $13, 4( %0 );" \ @@ -194,6 +232,53 @@ s32 rcos(s32 a); : "r"( r0 ) \ : "memory" ) +/* Load IR1/IR2/IR3 from the three words of the VECTOR @p r0. */ +#define gte_ldlvl( r0 ) __asm__ volatile ( \ + "lwc2 $9, 0( %0 );" \ + "lwc2 $10, 4( %0 );" \ + "lwc2 $11, 8( %0 )" \ + : \ + : "r"( r0 ) ) + +/* Load the far colour (RFC/GFC/BFC) from the VECTOR @p r0 -- the colour + * INTPL fades towards. */ +#define gte_ldfc( r0 ) __asm__ volatile ( \ + "lw $12, 0( %0 );" \ + "lw $13, 4( %0 );" \ + "lw $14, 8( %0 );" \ + "ctc2 $12, $21;" \ + "ctc2 $13, $22;" \ + "ctc2 $14, $23" \ + : \ + : "r"( r0 ) \ + : "$12", "$13", "$14" ) + +/* Same, but from three 8-bit colour components, each scaled to the GTE's + * 4.12 fixed point. */ +#define gte_SetFarColor( r0, r1, r2 ) __asm__ volatile ( \ + "sll $12, %0, 4;" \ + "sll $13, %1, 4;" \ + "sll $14, %2, 4;" \ + "ctc2 $12, $21;" \ + "ctc2 $13, $22;" \ + "ctc2 $14, $23" \ + : \ + : "r"( r0 ), "r"( r1 ), "r"( r2 ) \ + : "$12", "$13", "$14" ) + +/* Load IR0, the interpolation fraction used by INTPL / DPCS. */ +#define gte_lddp( r0 ) __asm__ volatile ( \ + "mtc2 %0, $8" \ + : \ + : "r"( r0 ) ) + +/* Store the packed RGB result (GTE data register 22). */ +#define gte_strgb( r0 ) __asm__ volatile ( \ + "swc2 $22, 0( %0 )" \ + : \ + : "r"( r0 ) \ + : "memory" ) + #define gte_mvmva_core( r0 ) __asm__ volatile ( \ "nop;" \ "nop;" \ @@ -278,7 +363,7 @@ s32 rcos(s32 a); "sw $12, 0( %0 )" \ : \ : "r"( r0 ) \ - : "memory" ) + : "$12", "memory" ) #define gte_stsz( r0 ) __asm__ volatile ( \ "swc2 $19, 0( %0 )" \ @@ -305,6 +390,14 @@ s32 rcos(s32 a); ".word 0x4A180001" \ : : ) +/* INTPL -- fade IR1/IR2/IR3 towards the far colour by the fraction in IR0, + * leaving the packed result in RGB2. */ +#define gte_intpl() __asm__ volatile ( \ + "nop;" \ + "nop;" \ + ".word 0x4A980011" \ + : : ) + #define gte_avsz4() __asm__ volatile ( \ "nop;" \ "nop;" \ diff --git a/include/world.h b/include/world.h index f8b1f4ec..e572d232 100644 --- a/include/world.h +++ b/include/world.h @@ -391,45 +391,77 @@ extern ActorRecord D_800DD6A8[]; */ typedef struct { /* 0x00 */ VECTOR pos; /**< World position (12 bytes used + pad). */ - /* 0x10 */ u8 pad10[0x08]; + /* 0x10 */ SVECTOR view; /**< Camera-relative position, refreshed every + frame by @c func_800B01A0 and fed to the + GTE by the renderer (@c func_800A9300). */ /* 0x18 */ SVECTOR rot; /**< Rotation vector (8 bytes). */ - /* 0x20 */ s16 proj_x; /**< Projected screen X. */ - /* 0x22 */ s16 proj_y; /**< Projected screen Y. */ - /* 0x24 */ s16 proj_z; /**< Projected depth. */ - /* 0x26 */ u8 pad26[0x02]; + /* 0x20 */ SVECTOR vel; /**< Per-tick position delta, seeded by the + spawner from the kind's GTE-projected + offset and added to @c pos each step. */ /* 0x28 */ u8 limit; /**< Active-slot kill threshold for @c count. */ - /* 0x29 */ u8 count; /**< Lifecycle tick counter. */ + /* 0x29 */ u8 count; /**< Lifecycle tick counter (the particle's age). */ /* 0x2A */ u8 kind; /**< Slot kind (= index into @c D_800C5480). */ /* 0x2B */ u8 pad2B; - /* 0x2C */ u16 life; /**< Per-frame life decrement. */ + /* 0x2C */ u16 scale; /**< Current size, 4.12 fixed (spawns at 0x1000 + = 1.0 and is scaled every tick). */ /* 0x2E */ u8 pad2E[0x02]; } Slot30; /* 0x30 bytes */ /** - * @brief Per-kind spawn parameters at @c D_800C5480. + * @brief Per-kind particle description at @c D_800C5480. * - * Stride 0x28. Indexed by @c Slot30::kind. Most fields are still - * opaque; only @c limit (0x12) and @c offset (0x18) are known. + * Stride 0x28, indexed by @c Slot30::kind. Carries everything the + * renderer (@ref func_800A9300) needs to draw one slot: where its sprite + * lives in VRAM, how it is oriented, and how it animates. */ typedef struct { - /* 0x00 */ u8 unk00[0x12]; + /* 0x00 */ s16 u; /**< Texture u of frame 0, in 4-pixel units. */ + /* 0x02 */ s16 v; /**< Texture v; bits 8 and 9 also carry tpage flags. */ + /* 0x04 */ u16 clutX; /**< CLUT VRAM x. */ + /* 0x06 */ u16 clutY; /**< CLUT VRAM y. */ + /* 0x08 */ u8 w; /**< Sprite width in texels. */ + /* 0x09 */ u8 h; /**< Sprite height in texels. */ + /* 0x0A */ u8 prim; /**< Primitive to emit: 0 = TILE, 1 = POLY_FT4. */ + /* 0x0B */ u8 unk0B; + /* 0x0C */ u8 r, g, b; /**< Flat colour applied to the primitive. */ + /* 0x0F */ u8 frameCount; /**< Number of animation frames. */ + /* 0x10 */ u16 scaleRate; /**< Per-tick scale multiplier, 4.12 fixed. */ /* 0x12 */ u8 limit; /**< Initial @c Slot30::limit value. */ - /* 0x13 */ u8 unk13[0x05]; + /* 0x13 */ u8 frameDiv; /**< Ticks per animation frame. */ + /* 0x14 */ u8 shape; /**< Billboard shape, see @c PARTICLE_SHAPE_*. */ + /* 0x15 */ u8 tpageBits; /**< Low 2 bits select the texture page's colour mode. */ + /* 0x16 */ s16 size; /**< Size multiplier against @c Slot30::scale. */ /* 0x18 */ SVECTOR offset; /**< Per-kind GTE-projected offset. */ - /* 0x20 */ u8 unk20[0x08]; + /* 0x20 */ SVECTOR vel; /**< Per-tick velocity increment added to @c Slot30::vel. */ } KindParams; /* 0x28 bytes */ +/* KindParams::shape — how the sprite is oriented in the world. */ +#define PARTICLE_SHAPE_GROUND 0 /**< Flat on the ground plane, w x w. */ +#define PARTICLE_SHAPE_SCREEN 1 /**< Screen-aligned square around the projected point. */ +#define PARTICLE_SHAPE_ROTATED 2 /**< Screen-aligned, sized by a second projection. */ +#define PARTICLE_SHAPE_GROUND_2X 3 /**< Flat on the ground plane, stretched 2x in z. */ +#define PARTICLE_SHAPE_UPRIGHT 4 /**< Standing in the x/y plane. */ + +/** @brief 1.0 in @c Slot30::scale's 4.12 fixed-point format; every spawner + * seeds a slot at full size. */ +#define PARTICLE_SCALE_ONE 0x1000 + /* Spawn-flag bits for the Slot30 pool spawners (func_800AC0A0 and the * fixed-kind wrappers around it). */ #define SLOT_FLAG_JITTER_LIMIT 1 /**< RNG-jitter Slot30::limit by [-4, +3]. */ -#define SLOT_FLAG_JITTER_LIFE 2 /**< RNG-jitter Slot30::life by [-0x80, +0x7F]. */ +#define SLOT_FLAG_JITTER_SCALE 2 /**< RNG-jitter Slot30::scale by [-0x80, +0x7F]. */ #define SLOT_FLAG_UNUSED4 4 /**< Never set by any caller; meaning unknown. */ extern Slot30 D_800D9CB0[64]; extern MATRIX D_800DA8B0; /**< GTE lighting color matrix (sits at &D_800D9CB0[64]). */ extern MATRIX D_800C5428; /**< Source color matrix copied into D_800DA8B0. */ extern CVECTOR D_800C53F8; /**< Background (ambient) color source. */ +extern CVECTOR D_800DB0DC; /**< Fourth palette stop; the world-zone blender fills it + alongside D_800DB0D0[0..2] and D_800DB0E0. */ extern CVECTOR D_800DB0E0; /**< Active background-color cache (programmed via SetBackColor). */ +/** Base of the world zone block: a table of s32 offsets, each relative to + * this pointer, locating one record. Entry 0 is the map-wide default. */ +extern s32 *D_800C9EE8; extern VECTOR D_800DB0E8; /**< World camera-base translation (GTE TR bias; used by setWorldMapTransVector and func_800423DC). */ extern KindParams D_800C5480[]; @@ -487,7 +519,7 @@ extern TransformEntry *D_800D2128; /** @brief One 0x18-byte worldmap sub-OT slot: link tag + prim payload. */ typedef struct { P_TAG link; /**< 0x00: P_TAG with low-24 next-prim addr. */ - u8 pad08[0x10]; /**< 0x08..0x17: slot payload (primed by func_800491E8). */ + u8 pad08[0x10]; /**< 0x08..0x17: slot payload (primed by DrawPrim). */ } OTSubSlot; extern BattleSceneCtx D_800CA040; /**< Worldmap "no-battle" sentinel — also functions as an empty BattleSceneCtx. */ diff --git a/include/world/we_object10.h b/include/world/we_object10.h index 0b2424cb..bd7fe726 100644 --- a/include/world/we_object10.h +++ b/include/world/we_object10.h @@ -60,8 +60,9 @@ extern s32 func_800BA870(u8 *p); extern s32 func_800B0010(void); extern void func_800AC0A0(s32 marker, VECTOR *position, SVECTOR *vec, s32 zero); extern void *func_80047CE4(void *dst, s32 c, u32 n); +/* Nonzero when any active slot carries the 0x4F marker. */ +extern s32 func_800BEF6C(void); extern s32 func_8009CC3C(void); -extern s32 ratan2(s32 y, s32 x); extern void func_800AEB58(XformEntry *entry, XformGroup *group); extern void func_8009C5FC(s32 *data); extern void func_800BC51C(VECTOR *src, VECTOR *dst); diff --git a/include/world/we_object5.h b/include/world/we_object5.h index 4586c540..206749be 100644 --- a/include/world/we_object5.h +++ b/include/world/we_object5.h @@ -62,12 +62,15 @@ extern VECTOR D_800DD658; /* source position for func_800BC51C extern void func_800A84D0(void); extern s32 func_8003F9F4(CVECTOR *input, CVECTOR *cue, s32 w1, s32 w2, CVECTOR *out); extern s32 func_8009CC3C(void); -extern s32 func_80041E84(s32 y, s32 x); extern s32 worldPosToCell(VECTOR *pos, SVECTOR *out); extern s32 func_800A4700(s32 a, s32 b); extern s32 func_800A475C(s32 a, s32 b); extern s32 func_800A5DC8(s32 x, s32 y); extern void func_800423DC(VECTOR *a, s32 *b_pos, VECTOR *out); +/* Rotates four corner offsets into place and projects them; @c outSXY takes + * the four packed screen points and @c outOTZ their average depth. */ +extern void func_800ABEF0(SVECTOR *src, SVECTOR *rot, SVECTOR *trans, + DVECTOR *outSXY, u32 *outOTZ); extern void func_800ACC68(MATRIX *out_mat, SVECTOR *angles, SVECTOR *rotBuf, SVECTOR *offset); extern void func_800B5C60(s32 ctx, s16 count, MATRIX *outMat, SVECTOR *outAngles, diff --git a/src/field/fe_object1.c b/src/field/fe_object1.c index 724067eb..2810319b 100644 --- a/src/field/fe_object1.c +++ b/src/field/fe_object1.c @@ -885,7 +885,7 @@ void func_80099348(void) { * @brief Angle (8-bit BAM) and distance between two 2D points. * * Computes @c dx / @c dy from the two points, stores the squared distance to - * @c *outDist, replaces it with the true distance via @c func_8003F4A4 (isqrt), + * @c *outDist, replaces it with the true distance via @c SquareRoot0 (isqrt), * then normalizes the deltas to a fixed ~[-128,128] scale and resolves the * octant with a magnitude compare (@c |dx| vs @c |dy|) plus the two sign tests. * Each octant reads the arctangent table @c D_800C319C at @c 2*|minor| and adds @@ -910,7 +910,7 @@ s32 func_8009A0E8(s32 *p0, s32 *p1, s32 *outDist) { s32 r; *outDist = d2; - dist = func_8003F4A4(d2); + dist = SquareRoot0(d2); *outDist = dist; dx = ((dx << 12) / dist) / 32; @@ -2052,7 +2052,7 @@ void func_8009BEC8(Actor *ents, s32 flags) { a.vx = (ents[i].msgTextPtr - ents[i].moveStartX) / 1024; a.vy = (ents[i].msgPosX - ents[i].moveStartY) / 1024; a.vz = (ents[i].msgPosY - ents[i].moveStartZ) / 1024; - dist[0] = func_8003F4A4(a.vx * a.vx + a.vy * a.vy + a.vz * a.vz); + dist[0] = SquareRoot0(a.vx * a.vx + a.vy * a.vy + a.vz * a.vz); ents[i].field_0x1D8 = dist[0] / D_800704A8.unk1AE; ents[i].field_0x1DA = 0; ents[i].msgState = 1; @@ -3364,7 +3364,7 @@ void func_8009FE18(s32 entIdx, Actor *actor, s32 flags) { d.vx = (actor->field_0x1C0 - actor->unk1A8) / 1024; d.vy = (actor->field_0x1C4 - actor->unk1AC) / 1024; d.vz = (actor->field_0x1C8 - actor->unk1B0) / 1024; - actor->field_0x1D8 = func_8003F4A4(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz) / D_80070656; + actor->field_0x1D8 = SquareRoot0(d.vx * d.vx + d.vy * d.vy + d.vz * d.vz) / D_80070656; actor->field_0x1DA = 0; actor->msgState = 1; break; diff --git a/src/world/we_object4.c b/src/world/we_object4.c index 4eaf507e..87713d8e 100644 --- a/src/world/we_object4.c +++ b/src/world/we_object4.c @@ -45,28 +45,6 @@ extern s32 D_800C5454; /**< Likely nonzero while the map pointer hi /* Map-view HUD drawers of this unit (see func_800A8400 for the driving values). */ -/** - * @brief World-map effect particle (covers offsets 0x00-0x2D; full stride unknown). - * - * func_800A9CC0 steps one of these against a second instance that supplies - * the per-step deltas (velocity increments and the scale multiplier). - */ -typedef struct { - /* 0x00 */ s32 x; /**< Position x (fixed point). */ - /* 0x04 */ s32 y; /**< Position y (fixed point). */ - /* 0x08 */ s32 z; /**< Position z (fixed point). */ - /* 0x0C */ u8 pad0C[4]; - /* 0x10 */ u16 scaleRate; /**< Per-step scale multiplier, 4.12 fixed (read from the delta-source instance). */ - /* 0x12 */ u8 pad12[0xE]; - /* 0x20 */ s16 vx; /**< Velocity x. */ - /* 0x22 */ s16 vy; /**< Velocity y. */ - /* 0x24 */ s16 vz; /**< Velocity z. */ - /* 0x26 */ u8 pad26[3]; - /* 0x29 */ u8 age; /**< Step counter, incremented every update. */ - /* 0x2A */ u8 kind; /**< Particle kind; 12 and 13 get the camera-follow drift. */ - /* 0x2B */ u8 pad2B; - /* 0x2C */ u16 scale; /**< Current scale, 4.12 fixed. */ -} WorldParticle; /* Camera-follow reference for the kind-12/13 drift in func_800A9CC0. * volatile is load-bearing: it keeps this read ordered against the @@ -158,28 +136,63 @@ extern s16 D_800C53B4[]; /**< Per-plane depth-cue weight fed to func_ extern POLY_GT4 D_800D58C0[3]; /**< Map-panel quads for the active scene. */ extern POLY_GT4 D_800D595C[3]; /**< Map-panel quads for the D_800CA040 sentinel scene. */ +/** One VRAM strip: 64x16 texels of the worldmap band. */ +#define STRIP_W 0x40 +#define STRIP_H 0x10 +/** Strips per band, and depth planes produced from each. */ +#define STRIP_COUNT 4 +#define STRIP_PLANES 2 +/** Width of a whole band, i.e. its four strips laid side by side. */ +#define BAND_W (STRIP_W * STRIP_COUNT) +/** Single-texel uploads primed per pool, one per entry of @c D_80098120. */ +#define STRIP_LOAD_COUNT 11 +/** VRAM row each band is read from -- and blitted back to. */ +#define BAND0_VRAM_Y 0xE0 +#define BAND1_VRAM_Y 0xF0 +/** Scratch VRAM column the expanded copies are staged in, per band. */ +#define BAND0_SCRATCH_X 0x140 +#define BAND1_SCRATCH_X 0x2C0 + +/* Worldmap backdrop prims, one set per scene bank. Each pair is drawn against + * two bone slots of the ordering table -- see func_800A7590. */ +extern DRAWENV D_800D3930; /**< Strip draw-env template, rebuilt per band. */ +extern DRAWENV D_800D3990[2]; /**< Per-scene draw-env, copied from each context. */ +extern POLY_F4 D_800D3810[2][3]; +extern POLY_F4 D_800D38A0[2][3]; +extern DR_ENV D_800D3A50[2][2]; +extern DR_ENV D_800D3B50[2][2]; +extern DR_ENV D_800D3C50[2][2]; +extern DR_ENV D_800D3D50[2][2]; +/** One record per upload slot, per bank per bone slot; how many are live at + * run time comes from @c func_800BEF6C. */ +extern DR_LOAD D_800D42D0[2][2][STRIP_LOAD_COUNT]; + extern POLY_FT4 D_800D88B0[2][64]; extern TILE D_800DA8D0[2][64]; +/* One-past-the-end of each pool's first bank; the particle renderer stops + * filling a bank when its cursor reaches these. */ +extern POLY_FT4 D_800D92B0[]; /**< == &D_800D88B0[1][0]. */ +extern TILE D_800DACD0[]; /**< == &D_800DA8D0[1][0]. */ extern WorldPolyGT4 D_800D5A00[2][64]; extern WorldPolyGT3 D_800D7400[2][64]; -/* func_800491E8 is main-binary. */ -extern void func_800491E8(void *p); -/* Private to this unit: no caller outside we_object4 references these. - * The last four are reached only from this unit's own INCLUDE_ASM bodies. */ -static void func_800A688C(u16 *src, RECT *area, u16 *dst, s32 count); -static void func_800A7CD0(s32 *block); -static void func_800A8024(void); -static void func_800A8524(s32 scrollX, s16 topY, s32 brightness); -static void func_800A8868(s32 phase, s16 y); -static void func_800A8A28(s16 y); -static void func_800A9F54(WorldPos *pos, s32 x, s32 y); -static void func_800A6A74(BattleSceneCtx *ctx); -static void func_800A9CC0(WorldParticle *p, WorldParticle *q); -static void func_800AAD48(WorldVtx *vtx, TriShade *shade); -static void func_800AAEAC(WorldVtx *vtx, QuadShade *shade); +/** + * @brief One world-map colour zone, reached through the @c D_800C9EE8 offset + * table (entry 0 is the map-wide default). + * + * A zone paints the scene around a point: while the camera is inside + * @c radius, @ref func_800A8C1C fades the five palette stops and the GTE + * colour matrix from the default's values towards this zone's, by distance. + */ +typedef struct { + /* 0x00 */ s32 x; /**< Zone centre, world x. */ + /* 0x04 */ s32 y; /**< Zone centre, world y. */ + /* 0x08 */ s32 radius; /**< Influence radius; also sets the fade rate. */ + /* 0x0C */ CVECTOR colors[5]; /**< Palette stops: background, then the four HUD stops. */ + /* 0x20 */ s16 matrix[3][3]; /**< Colour matrix rows fed to the GTE. */ +} WorldZone; /* 0x32 bytes */ /** * @brief One vertex of the world-map inset mesh (@ref func_800A9F54). @@ -195,6 +208,36 @@ typedef struct { /* 0x06 */ s16 my; /**< Map-cell y. */ } WorldTessVert; /* 0x8 bytes */ +/* Private to this unit: no caller outside we_object4 references these. Many are + * unreferenced entirely in the retail build; each such function says so in an + * @note on its own definition. */ +static void func_800A688C(u16 *src, RECT *area, u16 *dst, s32 count); +static void func_800A7CD0(s32 *block); +static void func_800A8024(void); +static void func_800A8524(s32 scrollX, s16 topY, s32 brightness); +static void func_800A8868(s32 phase, s16 y); +static void func_800A8A28(s16 y); +static void func_800A9F54(WorldPos *pos, s32 x, s32 y); +static void func_800A6A74(BattleSceneCtx *ctx); +static void func_800A9CC0(Slot30 *p, KindParams *q); +static void func_800AAD48(WorldVtx *vtx, TriShade *shade); +static void func_800AAEAC(WorldVtx *vtx, QuadShade *shade); +/* Emits one mesh polygon from the assembled vertices into the current prim + * slot; @c isQuad selects four vertices over three. */ +static void func_800AA210(WorldTessVert *verts, s32 isQuad); +/* Draws the D_800D9CB0 particle pool; defined further down. */ +static void func_800A9300(void); +/* Re-blends the world palette for the camera's position; defined below. */ +static void func_800A8C1C(void); +/* Links the worldmap backdrop prims into the scene's OT; defined below. */ +static void func_800A7590(BattleSceneCtx *ctx); +/* Builds the two worldmap strip sub-OTs; defined below. */ +static void func_800A64DC(void); +/* Primes every worldmap strip pool from VRAM; defined below. */ +static void func_800A6BE0(void); +/* Spawns a kind-11 particle at a fixed world position; defined below. */ +static void func_800AB06C(void); + /** Vertex index per mesh polygon; the first @c MESH_QUAD_COUNT rows are quads * and use all four entries, the rest are triangles and use the first three. */ extern s8 D_800C57F0[][4]; @@ -208,13 +251,118 @@ extern volatile u8 D_800C58D4[][2]; extern s32 D_800D8808; /**< Mesh centre screen x (corner + @c MESH_CENTRE_BIAS). */ extern s32 D_800D880C; /**< Mesh centre screen y (corner + @c MESH_CENTRE_BIAS). */ -/* Emits one mesh polygon from the assembled vertices into the current prim - * slot; @c isQuad selects four vertices over three. Defined at the bottom of - * this file and private to it like the statics above, but it is still - * INCLUDE_ASM, so it cannot be declared static yet. */ -void func_800AA210(WorldTessVert *verts, s32 isQuad); +/** Scratch VRAM rows for the two depth planes. */ +#define SCRATCH_NEAR_Y 0x40 +#define SCRATCH_FAR_Y 0x80 -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A64DC); +/** + * @brief Build the two worldmap strip sub-OTs. + * + * Each band is four 64x16 strips read back out of VRAM. @ref func_800A688C + * expands a strip into two depth-cued copies, which are uploaded side by side + * to scratch VRAM; a @c DR_MOVE is then packed into every sub-slot of both + * pool rows so that drawing the slot blits its copy to the band's position. + * Finally each record's four sub-slots are chained head to tail, turning the + * record into the mini ordering table the renderer splices bone prims into. + * + * The second band repeats the whole thing one VRAM row lower, into the second + * pool. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. + */ +static void func_800A64DC(void) { + RECT src; + RECT near; + RECT far; + u16 planes[STRIP_PLANES][STRIP_W * STRIP_H]; + s32 k; + s32 rec; + s32 slot; + s32 i; + OTSubSlot *prev; + OTSubSlot *next; + + src.x = 0; + src.y = BAND0_VRAM_Y; + src.w = STRIP_W; + src.h = STRIP_H; + near.x = BAND0_SCRATCH_X; + near.y = SCRATCH_NEAR_Y; + near.w = STRIP_W; + near.h = STRIP_H; + far.x = BAND0_SCRATCH_X; + far.y = SCRATCH_FAR_Y; + far.w = STRIP_W; + far.h = STRIP_H; + + for (k = 0; k < STRIP_COUNT; k++) { + func_800A688C(NULL, &src, planes[0], STRIP_PLANES); + LoadImage(&near, (u32 *)planes[0]); + LoadImage(&far, (u32 *)planes[1]); + /* A sub-slot's 0x18-byte payload is exactly a DR_MOVE; the pool is + * typed OTSubSlot because the renderer walks it as an OT chain. */ + for (i = 0; i < 2; i++) { + SetDrawMove((DR_MOVE *)&D_800D3510[i][0][k], &near, k * STRIP_W, BAND0_VRAM_Y); + SetDrawMove((DR_MOVE *)&D_800D3510[i][1][k], &far, k * STRIP_W, BAND0_VRAM_Y); + } + near.y += STRIP_H; + far.y += STRIP_H; + src.x += STRIP_W; + } + + src.x = 0; + src.y = BAND1_VRAM_Y; + src.w = STRIP_W; + src.h = STRIP_H; + near.x = BAND1_SCRATCH_X; + near.y = SCRATCH_NEAR_Y; + near.w = STRIP_W; + near.h = STRIP_H; + far.x = BAND1_SCRATCH_X; + far.y = SCRATCH_FAR_Y; + far.w = STRIP_W; + far.h = STRIP_H; + + for (k = 0; k < STRIP_COUNT; k++) { + func_800A688C(NULL, &src, planes[0], STRIP_PLANES); + LoadImage(&near, (u32 *)planes[0]); + LoadImage(&far, (u32 *)planes[1]); + for (i = 0; i < 2; i++) { + SetDrawMove((DR_MOVE *)&D_800D3690[i][0][k], &near, k * STRIP_W, BAND1_VRAM_Y); + SetDrawMove((DR_MOVE *)&D_800D3690[i][1][k], &far, k * STRIP_W, BAND1_VRAM_Y); + } + near.y += STRIP_H; + far.y += STRIP_H; + src.x += STRIP_W; + } + + for (i = 0; i < 2; i++) { + for (rec = 0; rec < 2; rec++) { + next = D_800D3510[i][rec]; + for (slot = 0, prev = next - 1; slot < 4; slot++) { + if (slot != 0) { + addPrim(prev, next); + } + prev++; + next++; + } + } + } + + for (i = 0; i < 2; i++) { + for (rec = 0; rec < 2; rec++) { + next = D_800D3690[i][rec]; + for (slot = 0, prev = next - 1; slot < 4; slot++) { + if (slot != 0) { + addPrim(prev, next); + } + prev++; + next++; + } + } + } +} /** * @brief Expand a VRAM rectangle to depth-cued BGR555, one scanline at a time. @@ -284,6 +432,9 @@ static void func_800A688C(u16 *src, RECT *area, u16 *dst, s32 count) { * @c D_800CA040 sentinel. * * @param ctx Scene context whose @c primList holds the bone prims. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. */ static void func_800A6A74(BattleSceneCtx *ctx) { s32 cond; @@ -300,7 +451,169 @@ static void func_800A6A74(BattleSceneCtx *ctx) { } } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A6BE0); +/** VRAM source point for one strip upload: x in the low half, y in the high. */ +typedef struct { + /* 0x00 */ u16 x; + /* 0x02 */ u16 y; +} VramXY; /* 0x4 bytes */ + +/** Where in VRAM each of the eleven uploads reads its 1x1 source pixel from. */ +static const VramXY D_80098120[STRIP_LOAD_COUNT] = { + { 0x000, 0x0F9 }, { 0x020, 0x0E5 }, { 0x0D0, 0x0EC }, { 0x0C0, 0x0E1 }, + { 0x000, 0x0E2 }, { 0x0B0, 0x0E4 }, { 0x0B0, 0x0E5 }, { 0x0E0, 0x0E6 }, + { 0x000, 0x0EE }, { 0x080, 0x0EF }, { 0x000, 0x0FD }, +}; + +/** + * @brief Prime every worldmap strip pool from VRAM. + * + * Run once at world setup, for each of the two bands in turn. A band's four + * 64x16 strips are copied out of the map texture into scratch VRAM, and each + * sub-OT record gets a @c DR_MOVE per slot that blits one strip back, with the + * record's four slots chained into a mini ordering table. The band's draw + * environments are then packed: one from a default env covering the band, one + * from each scene context's own template. Each band also gets three flat + * semi-transparent quads spanning the strip. + * + * Finally every @c DR_LOAD in the @c D_800D42D0 pool is pointed at its 1x1 + * source pixel, so drawing one uploads a single texel. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. It is the filler for the pools that + * @ref func_800A7590 links and @ref func_800A64DC chains, both equally + * unreferenced. + */ +static void func_800A6BE0(void) { + RECT rect; + VramXY coords[STRIP_LOAD_COUNT]; + s32 i, j, k; + const VramXY *src; + + /* Taking the address through a pointer is load-bearing: copying straight + * out of the array lets the compiler assume 4-byte alignment and emit only + * the aligned path, where the original tests alignment at run time. */ + src = D_80098120; + memcpy(coords, src, sizeof(coords)); + + rect.x = 0; + rect.y = BAND0_VRAM_Y; + rect.w = STRIP_W; + rect.h = STRIP_H; + for (i = 0; i < STRIP_COUNT; i++) { + MoveImage(&rect, BAND0_SCRATCH_X, i * STRIP_H); + DrawSync(0); + rect.x += STRIP_W; + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 3; j++) { + rect.x = BAND0_SCRATCH_X; + rect.y = 0; + rect.w = STRIP_W; + rect.h = STRIP_H; + /* A sub-slot's 0x18-byte payload is exactly a DR_MOVE; the pool is + * typed OTSubSlot because the renderer walks it as an OT chain. */ + for (k = 0; k < 4; k++) { + SetDrawMove((DR_MOVE *)&D_800D3E50[i][j][k], &rect, + k * STRIP_W, BAND0_VRAM_Y); + rect.y += STRIP_H; + if (k != 0) { + addPrim(&D_800D3E50[i][j][k - 1], &D_800D3E50[i][j][k]); + } + } + } + } + + SetDefDrawEnv(&D_800D3930, 0, BAND0_VRAM_Y, BAND_W, STRIP_H); + for (i = 0; i < 2; i++) { + for (j = 0; j < 2; j++) { + SetDrawEnv(&D_800D3A50[i][j], &D_800D3930); + } + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 3; j++) { + setPolyF4(&D_800D3810[i][j]); + setSemiTrans(&D_800D3810[i][j], 1); + setXY4(&D_800D3810[i][j], 0, 0, BAND_W, 0, 0, STRIP_H, BAND_W, STRIP_H); + setRGB0(&D_800D3810[i][j], 0x80, 0x80, 0x80); + } + } + + for (i = 0; i < 2; i++) { + /* The two scene contexts sit back to back with the sentinel first; + * we_object1 spells the same pair as +0x5C and +0x40CC. */ + D_800D3990[i] = (&D_800CA040)[i].drawEnv; + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 2; j++) { + SetDrawEnv(&D_800D3C50[i][j], &D_800D3990[i]); + } + } + + rect.x = 0; + rect.y = BAND1_VRAM_Y; + rect.w = STRIP_W; + rect.h = STRIP_H; + for (i = 0; i < STRIP_COUNT; i++) { + MoveImage(&rect, BAND1_SCRATCH_X, i * STRIP_H); + DrawSync(0); + rect.x += STRIP_W; + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 3; j++) { + rect.x = BAND1_SCRATCH_X; + rect.y = 0; + rect.w = STRIP_W; + rect.h = STRIP_H; + for (k = 0; k < 4; k++) { + SetDrawMove((DR_MOVE *)&D_800D4090[i][j][k], &rect, + k * STRIP_W, BAND1_VRAM_Y); + rect.y += STRIP_H; + if (k != 0) { + addPrim(&D_800D4090[i][j][k - 1], &D_800D4090[i][j][k]); + } + } + } + } + + SetDefDrawEnv(&D_800D3930, 0, BAND1_VRAM_Y, BAND_W, STRIP_H); + for (i = 0; i < 2; i++) { + for (j = 0; j < 2; j++) { + SetDrawEnv(&D_800D3B50[i][j], &D_800D3930); + } + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 3; j++) { + setPolyF4(&D_800D38A0[i][j]); + setSemiTrans(&D_800D38A0[i][j], 1); + setXY4(&D_800D38A0[i][j], 0, 0, BAND_W, 0, 0, STRIP_H, BAND_W, STRIP_H); + setRGB0(&D_800D38A0[i][j], 0x80, 0x80, 0x80); + } + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 2; j++) { + SetDrawEnv(&D_800D3D50[i][j], &D_800D3990[i]); + } + } + + for (i = 0; i < 2; i++) { + for (j = 0; j < 2; j++) { + for (k = 0; k < STRIP_LOAD_COUNT; k++) { + rect.x = coords[k].x; + rect.y = coords[k].y; + rect.w = 1; + rect.h = 1; + D_800D42D0[i][j][k].p[0] = 0; + SetDrawLoad(&D_800D42D0[i][j][k], &rect); + } + } + } +} /** * @brief Splice three of @c a0's bone prims into the sub-OT records of @@ -325,7 +638,64 @@ void func_800A735C(BattleSceneCtx *a0) { addPrims(&a0->primList[D_800C53B8[4]], &D_800D4090[cond][2][0].link, &D_800D4090[cond][2][3].link); } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A7590); +/** Bone slots of the ordering table the worldmap backdrop is drawn against. */ +#define BACKDROP_OT_FAR 3 +#define BACKDROP_OT_NEAR 4 +/** Records always emitted per bone slot, before the marker adds one more. */ +#define BACKDROP_BASE_RECORDS 10 + +/** + * @brief Link this frame's worldmap backdrop into the scene's ordering table. + * + * Everything here goes onto two of the ordering table's bone slots, whose + * indices come from the bone-id table @c D_800C53B8: a far slot that takes the + * layer drawn behind, and a near slot for the layer in front. The bulk is the + * @c D_800D42D0 record pool -- ten per slot, eleven when @c func_800BEF6C + * reports a marked slot is active -- followed by the fixed prims: a draw + * environment, a flat quad and a second draw environment per group, twice over. + * + * Since @c addPrim pushes onto the head of a chain, the calls read back to + * front: the last one linked is the first one drawn. + * + * @param ctx Scene to link into; the @c D_800CA040 sentinel selects bank 0 and + * any live scene selects bank 1. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. The pools it links are filled by + * func_800A6BE0, which is equally unreferenced. + */ +static void func_800A7590(BattleSceneCtx *ctx) { + s32 bank; + s32 extra; + s32 group; + s32 i; + + bank = (ctx != &D_800CA040); + extra = func_800BEF6C(); + + for (group = 0; group < 2; group++) { + for (i = 0; i < extra + BACKDROP_BASE_RECORDS; i++) { + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR + group]], + &D_800D42D0[bank][group][i]); + } + } + + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D3C50[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D3810[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D3A50[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3C50[bank][1]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3810[bank][1]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3810[bank][2]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3A50[bank][1]); + + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D3D50[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D38A0[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_FAR]], &D_800D3B50[bank][0]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3D50[bank][1]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D38A0[bank][1]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D38A0[bank][2]); + addPrim(&ctx->primList[D_800C53B8[BACKDROP_OT_NEAR]], &D_800D3B50[bank][1]); +} /** * @brief Step the world texture-strip animations and upload changed frames. @@ -417,6 +787,9 @@ void func_800A7B38(void) { * * @param block Animation block (offset table at its start), or a table * whose first entry is 0 to just disable all slots. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. */ static void func_800A7CD0(s32 *block) { WorldTexAnim *anim; @@ -540,6 +913,9 @@ void func_800A7E74(void) { * * The unused @c d vector and the discarded @c func_800A5E40 result are * kept as the original left them. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin. */ static void func_800A8024(void) { VECTOR ofs; @@ -678,6 +1054,11 @@ void func_800A8270(SVECTOR *out) { * b0..b2 are the width bytes of @c D_800DB0D0. Always finishes with * @c func_800A8A28 at the raw coordinate. * + * @note Dead code in the retail build: nothing calls this, and its address + * appears in no pointer table, so the three drawers below it — the + * map panel, the star field and the backdrop gradient — never run + * either. They have no other caller. + * * @note Purpose uncertain — appears to drive the world-map map-view HUD: * a map panel that wraps with the camera heading (@c func_800A8868), * a location-name banner shown only while the map pointer highlights @@ -747,6 +1128,8 @@ void func_800A84D0(void) { * is emitted as a @c TILE_1 on the HUD layer, and a @c DR_TPAGE closes the * layer. * + * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * * @note Four spellings here are matching devices for gcc 2.8.0 rather than * intent. The @c nextColX temp lets the column step fill the inner * loop's branch delay slot; repeating @c (clock >> 1) instead of @@ -867,6 +1250,8 @@ static void func_800A8524(s32 scrollX, s16 topY, s32 brightness) { * the page at VRAM x 0x380 with the CLUT at (0x340, 0xE0), and linked * into the HUD layer @c BSC_HUD_IDX of the active scene. * + * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * * @param phase Wrap offset from the camera heading (see above). * @param y Top edge of the panel row, in screen coordinates. */ @@ -943,6 +1328,8 @@ static void func_800A8868(s32 phase, s16 y) { * The quads come from the pool @ref func_800ABC98 primed, picking the * @c D_800CA040 sentinel's pair when no battle scene is active. * + * @note Dead code: reached only from the unreferenced @ref func_800A8400. + * * @param y Baseline of the gradient, in screen coordinates. */ static void func_800A8A28(s16 y) { @@ -978,7 +1365,161 @@ static void func_800A8A28(s16 y) { setaddr(&D_800D244C->primList[BSC_HUD_IDX], prim); } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A8C1C); +/** Bits of @c D_800D2440 that select which zone list to walk. */ +#define WORLD_SCATTER_KEY_MASK 0x1F +/** Walking this far into the list means the zone is a named area, which is + * what @c D_800C5454 gates the location banner on. */ +#define ZONE_LIST_NAMED_AREA 3 +/** World distances are halved five times before the GTE squares them, so the + * squared sum stays inside the GTE's 32-bit accumulators. */ +#define ZONE_DIST_SHIFT 5 + +/** + * Blend one palette stop between the default zone's colour and this zone's, + * by the fraction already loaded into IR0. The near colour is shifted up by + * four to reach the GTE's 4.12 fixed point -- the same scaling + * @c gte_SetFarColor applies to the far end for us. + */ +#define ZONE_BLEND(dst, k) \ + gte_SetFarColor(def->colors[k].r, def->colors[k].g, def->colors[k].b); \ + work.vx = zone->colors[k].r << 4; \ + work.vy = zone->colors[k].g << 4; \ + work.vz = zone->colors[k].b << 4; \ + gte_ldlvl(wp); \ + gte_lddp(dist); \ + gte_intpl(); \ + gte_strgb(dst) + +/** + * @brief Fade the world palette towards whichever colour zone the camera is in. + * + * The world carries a list of colour zones (the @c D_800C9EE8 offset table, + * whose entry 0 is the map-wide default). This walks the list from the entry + * the current scatter key selects, and stops at the first zone whose centre is + * within @c radius of the camera -- distance measured with the GTE's @c SQR on + * the halved x/y delta. + * + * That zone then decides the scene's colour. Its five palette stops and its + * 3x3 colour matrix are blended against the default's with the GTE's @c INTPL, + * weighted by how far into the zone the camera has travelled, so the world + * tints smoothly as you cross the boundary. With no zone in range the default's + * values are installed unblended. Either way the result is programmed into the + * GTE as the background colour and colour matrix. + * + * @note The @c goto is load-bearing: it is how the matched build skips the + * default-install block. Rewriting it as a "matched" flag costs three + * instructions and no longer matches. + * @note Dead code in the retail build: nothing calls this, and its address + * appears nowhere in world.bin, so the @c D_800C4D24 early-out never + * gets the chance to matter. + */ +static void func_800A8C1C(void) { + SVECTOR near; + VECTOR work; + WorldZone *def; + WorldZone *zone; + s32 *slot; + s32 camX, camY; + s32 dist; + s32 radius; + s32 idx; + s32 j; + s16 *matRows; + SVECTOR *np; + VECTOR *wp; + + camX = D_800C9868.x; + camY = D_800C9868.y; + /* The block is s32 offsets up front and WorldZone records behind them, so + * reaching a record means stepping the base by a byte count and retyping. */ + def = (WorldZone *)((u8 *)D_800C9EE8 + D_800C9EE8[0]); + + if (D_800C4D24 != 0) { + return; + } + + D_800C5454 = 0; + idx = 1; + if ((D_800D2440 & WORLD_SCATTER_KEY_MASK) != 4) { + idx = 2; + } + + if (D_800C9EE8[idx] != 0) { + /* Caching these keeps the scratch addresses and the matrix base in + * saved registers for the whole walk, as the original does. */ + np = &near; + wp = &work; + matRows = D_800DA8B0.m[0]; + slot = &D_800C9EE8[idx]; + do { + zone = (WorldZone *)((u8 *)D_800C9EE8 + *slot); + near.vx = (zone->x - camX) >> ZONE_DIST_SHIFT; + near.vy = (zone->y - camY) >> ZONE_DIST_SHIFT; + near.vz = 0; + gte_ldsv(np); + gte_SQR(0); + gte_stlvnl(wp); + dist = SquareRoot0(work.vx + work.vy + work.vz); + radius = zone->radius >> ZONE_DIST_SHIFT; + if (dist < radius) { + if (idx == ZONE_LIST_NAMED_AREA) { + D_800C5454 = 1; + } + /* How far in we are, as a 4.12 fraction for INTPL. The + * shift is spelled out again rather than reusing radius -- + * the original reloads it here, and sharing the local costs + * the match. */ + dist *= ONE / (zone->radius >> ZONE_DIST_SHIFT); + + ZONE_BLEND(&D_800DB0E0, 0); + ZONE_BLEND(&D_800DB0DC, 1); + + for (j = 0; j < 3; j++) { + work.vx = def->matrix[j][0]; + work.vy = def->matrix[j][1]; + work.vz = def->matrix[j][2]; + gte_ldfc(wp); + gte_lddp(dist); + near.vx = zone->matrix[j][0]; + near.vy = zone->matrix[j][1]; + near.vz = zone->matrix[j][2]; + gte_ldsv(np); + gte_intpl(); + gte_stsv(&matRows[j * 3]); + } + + ZONE_BLEND(&D_800DB0D0[0], 2); + ZONE_BLEND(&D_800DB0D0[1], 3); + ZONE_BLEND(&D_800DB0D0[2], 4); + + goto apply; + } + slot++; + idx++; + } while (*slot != 0); + } + + /* No zone in range: install the default's colours unblended. */ + D_800DB0E0.r = def->colors[0].r; + D_800DB0E0.g = def->colors[0].g; + D_800DB0E0.b = def->colors[0].b; + D_800DB0DC.r = def->colors[1].r; + D_800DB0DC.g = def->colors[1].g; + D_800DB0DC.b = def->colors[1].b; + D_800DA8B0 = D_800C5428; + for (idx = 0; idx < 3; idx++) { + for (j = 0; j < 3; j++) { + D_800DA8B0.m[idx][j] = def->matrix[idx][j]; + } + } + D_800DB0D0[0] = def->colors[2]; + D_800DB0D0[1] = def->colors[3]; + D_800DB0D0[2] = def->colors[4]; + +apply: + SetBackColor(D_800DB0E0.r, D_800DB0E0.g, D_800DB0E0.b); + SetColorMatrix(&D_800DA8B0); +} /** @@ -1013,21 +1554,260 @@ void func_800A9254(void) { } } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A9300); +/* Projects a slot into camera space. Declared here rather than pulled in from + * we_object7.h: that unit prototypes it over its own TrackEntry view of the + * same memory, which conflicts with Slot30. */ +extern s32 func_800B01A0(s16 headingA, s16 headingB, Slot30 *slot, + SVECTOR *viewOut, s32 unused4, s32 unused5); + +/** Depth past which a particle is dropped rather than linked into the OT. */ +#define PARTICLE_MAX_OTZ 0x2000 +/** Ordering-table slot for a projected depth. */ +#define PARTICLE_OT_INDEX(otz) ((otz) >> 1) + +/** + * @brief Draw every live particle in the @c D_800D9CB0 pool. + * + * Walks all 64 slots; a slot is live while its @c count is still below its + * @c limit. Each live slot is stepped by @ref func_800A9CC0, projected into + * camera space, and then turned into one primitive whose size follows the + * slot's own 4.12 @c scale times the kind's @c size. + * + * The kind's @c shape decides how the sprite sits in the world: the two + * screen-aligned shapes project the centre point and lay a square around it + * (@c PARTICLE_SHAPE_ROTATED first measuring the square by projecting a second + * point one half-width away, so the sprite shrinks with distance), while the + * three world-aligned shapes hand four corner offsets to @ref func_800ABEF0 + * and use the quad it projects. + * + * The finished sprite goes out as a flat @c TILE or, when the kind is + * animated, a textured @c POLY_FT4 whose u/v pick the current frame out of a + * 64-texel-wide strip. Both are linked into the scene's ordering table at the + * projected depth. Each pool holds 64 prims per scene bank and simply stops + * emitting when full. + * + * The GTE rotation matrix and translation vector are saved on entry and put + * back on exit, so callers see the transform they set up. + * + * @note Dead code in the retail build: nothing calls this and its address + * appears nowhere in world.bin -- the whole particle/backdrop layer + * (this, @ref func_800A7590 and the pool filler func_800A6BE0) is + * unreachable, like the map-view HUD layer above. + */ +static void func_800A9300(void) { + DVECTOR quad[4]; + DVECTOR centre; + SVECTOR corners[4]; + VECTOR savedTrans; + MATRIX savedMat; + SVECTOR span[3]; + SVECTOR noRotation; + MATRIX spanMat; + DVECTOR spanScr[3]; + s32 bank; + Slot30 *slot; + KindParams *kp; + POLY_FT4 *ft4; + TILE *tile; + u32 otz; + s32 half; + s32 wide; + s16 frame; + s16 cell; + s16 tex; + s16 row; + s16 headingB, headingA; + + bank = (D_800D244C == &D_800CA040); + ft4 = &D_800D88B0[bank][0]; + tile = &D_800DA8D0[bank][0]; + + headingA = func_800A5DC8(D_800D23C0.x, D_800D23C0.y); + headingB = func_800A5DC8(D_800C9868.x, D_800C9868.y); + + gte_sttr(&savedTrans); + gte_ReadRotMatrix(&savedMat); + + for (slot = D_800D9CB0; slot < &D_800D9CB0[64]; slot++) { + if (slot->count >= slot->limit) { + continue; + } + kp = &D_800C5480[slot->kind]; + func_800A9CC0(slot, kp); + + gte_SetRotMatrix(&D_800C9838); + gte_SetTransVector(D_800C9838.t); + if (!func_800B01A0(headingB, headingA, slot, &slot->view, 0, 0)) { + continue; + } + + half = slot->scale * kp->size / 4096 >> 1; + + if (kp->shape == PARTICLE_SHAPE_SCREEN) { + gte_ldv0(&slot->view); + gte_rtps(); + gte_stsxy(¢re); + gte_stszotz(&otz); + quad[3].vx = centre.vx - half; + quad[0].vx = centre.vx - half; + quad[2].vx = centre.vx + half; + quad[1].vx = centre.vx + half; + quad[1].vy = centre.vy - half; + quad[0].vy = centre.vy - half; + quad[3].vy = centre.vy + half; + quad[2].vy = centre.vy + half; + } else if (kp->shape == PARTICLE_SHAPE_ROTATED) { + /* Measure the sprite on screen: project the centre and a point one + * half-width to its side, and take the gap between them. */ + func_80047CE4(&noRotation, 0, sizeof(SVECTOR)); + span[2] = slot->view; + span[1] = span[2]; + span[0] = span[1]; + span[1].vx += half; + RotMatrix(&noRotation, &spanMat); + gte_SetRotMatrix(&spanMat); + gte_SetTransVector(D_800C9838.t); + gte_ldv3c(span); + gte_rtpt(); + gte_stsxy3c(spanScr); + /* Read unsigned: the original takes the raw gap between the two + * projected x's without sign-extending either. */ + wide = (u16)spanScr[1].vx - (u16)spanScr[0].vx; + + gte_SetRotMatrix(&D_800C9838); + gte_ldv0(&slot->view); + gte_rtps(); + gte_stsxy(¢re); + gte_stszotz(&otz); + quad[3].vx = centre.vx - wide; + quad[0].vx = centre.vx - wide; + quad[2].vx = centre.vx + wide; + quad[1].vx = centre.vx + wide; + quad[1].vy = centre.vy - wide; + quad[0].vy = centre.vy - wide; + quad[3].vy = centre.vy + wide; + quad[2].vy = centre.vy + wide; + } else if (kp->shape == PARTICLE_SHAPE_GROUND) { + corners[3].vy = 0; + corners[2].vy = 0; + corners[1].vy = 0; + corners[0].vy = 0; + corners[3].vx = -half; + corners[0].vx = -half; + corners[2].vx = half; + corners[1].vx = half; + corners[1].vz = half; + corners[0].vz = half; + corners[3].vz = -half; + corners[2].vz = -half; + func_800ABEF0(corners, &slot->rot, &slot->view, quad, &otz); + otz -= 4; + } else if (kp->shape == PARTICLE_SHAPE_GROUND_2X) { + corners[3].vx = -half; + corners[0].vx = -half; + corners[3].vy = 0; + corners[2].vy = 0; + corners[1].vy = 0; + corners[0].vy = 0; + corners[2].vx = half; + corners[1].vx = half; + corners[1].vz = half * 2; + corners[0].vz = half * 2; + corners[3].vz = -half * 2; + corners[2].vz = -half * 2; + func_800ABEF0(corners, &slot->rot, &slot->view, quad, &otz); + } else if (kp->shape == PARTICLE_SHAPE_UPRIGHT) { + corners[3].vz = 0; + corners[2].vz = 0; + corners[1].vz = 0; + corners[0].vz = 0; + corners[3].vx = -half; + corners[0].vx = -half; + corners[2].vx = half; + corners[1].vx = half; + corners[1].vy = -half; + corners[0].vy = -half; + corners[3].vy = half; + corners[2].vy = half; + func_800ABEF0(corners, &slot->rot, &slot->view, quad, &otz); + } + + if (otz >= PARTICLE_MAX_OTZ) { + continue; + } + + if (kp->prim == 0 && tile < &D_800DACD0[bank * 64]) { + setSemiTrans(tile, 0); + tile->r0 = kp->r; + tile->g0 = kp->g; + tile->b0 = kp->b; + tile->x0 = quad[0].vx; + tile->y0 = quad[0].vy; + tile->w = quad[1].vx - quad[0].vx; + tile->h = quad[1].vx - quad[0].vx; + tile->w += (tile->w == 0); + tile->h += (tile->h == 0); + addPrim(&D_800D244C->primList[PARTICLE_OT_INDEX(otz)], tile); + tile++; + } else if (kp->prim == 1 && ft4 < &D_800D92B0[bank * 64]) { + frame = slot->count / kp->frameDiv & 0xFF; + cell = frame; + if (frame >= 0) { + if (frame > kp->frameCount - 1) { + cell = kp->frameCount - 1; + } + } else { + cell = 0; + } + setSemiTrans(ft4, 1); + ft4->r0 = kp->r; + ft4->g0 = kp->g; + ft4->b0 = kp->b; + tex = (kp->u + cell * (kp->w >> 2)) % 64 * 4; + ft4->u2 = tex; + ft4->u0 = tex; + tex = kp->w + (kp->u + cell * (kp->w >> 2)) % 64 * 4 - 1; + ft4->u3 = tex; + ft4->u1 = tex; + tex = kp->v % 256; + ft4->v1 = tex; + ft4->v0 = tex; + row = kp->v % 256; + tex = row + kp->h - 1; + ft4->v3 = tex; + ft4->v2 = tex; + /* The projected points are already packed x,y pairs, so they go + * across a word at a time; vertices 2 and 3 swap to give the + * quad's triangle-strip winding. */ + *(u32 *)&ft4->x0 = *(u32 *)&quad[0]; + *(u32 *)&ft4->x1 = *(u32 *)&quad[1]; + *(u32 *)&ft4->x2 = *(u32 *)&quad[3]; + *(u32 *)&ft4->x3 = *(u32 *)&quad[2]; + ft4->tpage = getTPage(0, kp->tpageBits, kp->u + cell, kp->v); + ft4->clut = getClut(kp->clutX, kp->clutY); + addPrim(&D_800D244C->primList[PARTICLE_OT_INDEX(otz)], ft4); + ft4++; + } + } + + gte_SetRotMatrix(&savedMat); + gte_SetTransVector(&savedTrans); +} /** * @brief Step one world-map effect particle. * - * Advances @p p by one tick: multiplies its 4.12 scale by @p q's - * @c scaleRate, integrates position from the 16-bit velocity, and then - * accelerates the velocity by @p q's velocity deltas and bumps @c age. + * Advances @p p by one tick: multiplies its 4.12 @c scale by the kind's + * @c scaleRate, integrates @c pos from the 16-bit @c vel, and then + * accelerates @c vel by the kind's own @c vel and bumps @c count. * From the second tick on, particles of kind 12 or 13 additionally drift * with the camera: 5/6 of the per-frame camera deltas (@c D_800C9E38[0], - * @c D_800C9870 - @c D_800C974C, @c D_800C9E38[2]) is added to x/y/z so - * the effect roughly follows the view (weather-style particles). + * @c D_800C9870 - @c D_800C974C, @c D_800C9E38[2]) is added to the position + * so the effect roughly follows the view (weather-style particles). * - * @param p Particle to update. - * @param q Delta source: velocity increments and scale multiplier. + * @param p Particle slot to update. + * @param q The kind's parameters, supplying the per-tick scale and velocity + * increments. * * @note The position words are updated through the walking pointer @c w on * purpose: the multi-set pointer defeats gcc's alias base tracking, @@ -1035,16 +1815,16 @@ INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800A9300); * store, matching the original instruction schedule. The offsets * still fold to plain 0x0/0x4/0x8 accesses. */ -static void func_800A9CC0(WorldParticle *p, WorldParticle *q) { +static void func_800A9CC0(Slot30 *p, KindParams *q) { s32 *w; p->scale = p->scale * q->scaleRate / 4096; - p->x += p->vx; - p->y += p->vy; - p->z += p->vz; - if (p->age != 0) { + p->pos.vx += p->vel.vx; + p->pos.vy += p->vel.vy; + p->pos.vz += p->vel.vz; + if (p->count != 0) { if (p->kind == 12 || p->kind == 13) { - w = &p->x; + w = &p->pos.vx; *w += D_800C9E38[0] * 5 / 6; w++; *w += (D_800C9870 - D_800C974C) * 5 / 6; @@ -1052,17 +1832,17 @@ static void func_800A9CC0(WorldParticle *p, WorldParticle *q) { *w += D_800C9E38[2] * 5 / 6; } } - p->vx += q->vx; - p->vy += q->vy; - p->vz += q->vz; - p->age++; + p->vel.vx += q->vel.vx; + p->vel.vy += q->vel.vy; + p->vel.vz += q->vel.vz; + p->count++; } /** * @brief Initialise the first record's 4 sub-OT slots in each of two * pools for the entity model in @p a0. * - * Runs @c func_800491E8 on all four sub-slots of record 0 in the + * Runs @c DrawPrim on all four sub-slots of record 0 in the * @c D_800D3E50 and @c D_800D4090 pools, then dispatches * @c DrawSync(0). @c cond is the canonical entity bit — @c 0 for * @c &D_800CA040, @c 1 otherwise — selecting the pool row. @@ -1070,14 +1850,14 @@ static void func_800A9CC0(WorldParticle *p, WorldParticle *q) { void func_800A9E24(BattleSceneCtx *a0) { s32 cond = (a0 != &D_800CA040) ? 1 : 0; - func_800491E8(&D_800D3E50[cond][0][0]); - func_800491E8(&D_800D3E50[cond][0][1]); - func_800491E8(&D_800D3E50[cond][0][2]); - func_800491E8(&D_800D3E50[cond][0][3]); - func_800491E8(&D_800D4090[cond][0][0]); - func_800491E8(&D_800D4090[cond][0][1]); - func_800491E8(&D_800D4090[cond][0][2]); - func_800491E8(&D_800D4090[cond][0][3]); + DrawPrim(&D_800D3E50[cond][0][0]); + DrawPrim(&D_800D3E50[cond][0][1]); + DrawPrim(&D_800D3E50[cond][0][2]); + DrawPrim(&D_800D3E50[cond][0][3]); + DrawPrim(&D_800D4090[cond][0][0]); + DrawPrim(&D_800D4090[cond][0][1]); + DrawPrim(&D_800D4090[cond][0][2]); + DrawPrim(&D_800D4090[cond][0][3]); DrawSync(0); } @@ -1200,7 +1980,253 @@ static void func_800A9F54(WorldPos *pos, s32 x, s32 y) { } } -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800AA210); +/** Width of the world map in cells; map x wraps modulo this. */ +#define MAP_CELLS_X 256 +/** Height of the world map in cells; map y wraps modulo this. */ +#define MAP_CELLS_Y 192 + +/** + * @brief Emit one mesh polygon, splitting it first if it straddles a map seam. + * + * The world map is a @c MAP_CELLS_X x @c MAP_CELLS_Y torus, so a polygon near + * an edge can have vertices on both sides of the wrap. This first slides the + * whole polygon back into range (if every vertex is off the same side), then + * re-measures its bounding box. With no seam crossing left it shades and emits + * directly; otherwise a quad is cut along the seam and each half is re-emitted + * through a recursive call, once per axis. Triangles are never split — they + * fall through to the emit path with their map coordinates zeroed. + * + * Shading is per vertex: the squared distance from the mesh centre + * (@c D_800D8808 / @c D_800D880C) goes through the GTE's @c SQR, and the + * resulting length is inverted into a grey level, so the inset fades out + * towards its edges. + * + * @param verts Polygon vertices, three for a triangle and four for a quad. + * Wrapped in place, and the triangle path clears the map + * coordinates before emitting. + * @param isQuad Nonzero to treat @p verts as a quad (four vertices). + * + * @note @c loHalf and @c hiHalf are never read; they only reserve the frame + * space the retail build allocates. + * @note The four seam interpolations read the base screen coordinate as + * @c u16 on purpose -- the original loads it unsigned and only the + * interpolated term is signed. + * @note Dead code in the retail build: the only caller is + * @ref func_800A9F54, which is itself unreachable. + */ +static void func_800AA210(WorldTessVert *verts, s32 isQuad) { + WorldTessVert loHalf[4]; + WorldTessVert hiHalf[4]; + SVECTOR dist; + /* Scratch for the two halves of a split: the y split fills part[1..4] and + * the x split part[0..3], so each recursive call gets four contiguous + * vertices. On the emit paths the split is over and the same storage + * carries the shade bytes instead. */ + WorldTessVert part[5]; + s16 minX, minY, maxX, maxY; + s32 spanX, spanY; + s32 i; + s32 seam; + + spanY = spanX = 0; + + minY = 0x7FFF; + maxY = -0x8000; + minX = minY; + maxX = maxY; + for (i = 0; i < isQuad + 3; i++) { + if (verts[i].mx < minX) minX = verts[i].mx; + if (verts[i].my < minY) minY = verts[i].my; + if (maxX < verts[i].mx) maxX = verts[i].mx; + if (maxY < verts[i].my) maxY = verts[i].my; + } + + /* Wholly off one edge: slide the polygon back onto the map. */ + if (maxX < 0) { + for (i = 0; i < isQuad + 3; i++) verts[i].mx += MAP_CELLS_X; + } + if (minX >= MAP_CELLS_X) { + for (i = 0; i < isQuad + 3; i++) verts[i].mx -= MAP_CELLS_X; + } + if (maxY < 0) { + for (i = 0; i < isQuad + 3; i++) verts[i].my += MAP_CELLS_Y; + } + if (minY >= MAP_CELLS_Y) { + for (i = 0; i < isQuad + 3; i++) verts[i].my -= MAP_CELLS_Y; + } + + minY = minX = 0x7FFF; + maxY = maxX = -0x8000; + for (i = 0; i < isQuad + 3; i++) { + if (verts[i].mx < minX) minX = verts[i].mx; + if (verts[i].my < minY) minY = verts[i].my; + if (maxX < verts[i].mx) maxX = verts[i].mx; + if (maxY < verts[i].my) maxY = verts[i].my; + } + + if (minX < 0 || maxX >= MAP_CELLS_X) spanX = 1; + if (minY < 0 || maxY >= MAP_CELLS_Y) spanY = 1; + + if (!spanX && !spanY) { + /* The mesh centre is published as a word but only ever holds a screen + * coordinate, so the distance is taken from its low halfword. */ + for (i = 0; i < isQuad + 3; i++) { + dist.vx = (u16)D_800D8808 - verts[i].sx; + dist.vy = (u16)D_800D880C - verts[i].sy; + dist.vz = 0; + gte_ldsv(&dist); + gte_SQR(0); + gte_stsv(&dist); + ((u8 *)part)[i] = + ~(SquareRoot0(dist.vx + dist.vy + dist.vz) << 2); + } + if (isQuad) { + func_800AAEAC((WorldVtx *)verts, (QuadShade *)part); + D_800D8800++; + return; + } + func_800AAD48((WorldVtx *)verts, (TriShade *)part); + D_800D8804++; + return; + } + + if (isQuad) { + if (spanY) { + /* Cut along the horizontal seam: part[1..2] is the half that wraps + * round to the far edge, part[3..4] the half that stays put, and + * the shared edge sits on the last row. Its screen y is the linear + * interpolation of the polygon's leading edge at that row. */ + if (verts[0].my < 0) { + part[3] = verts[0]; + part[1] = part[3]; + part[4] = verts[1]; + part[2] = part[4]; + part[3].my = MAP_CELLS_Y - 1; + part[4].my = MAP_CELLS_Y - 1; + part[1].my += MAP_CELLS_Y; + part[2].my += MAP_CELLS_Y; + seam = (u16)part[1].sy + + (verts[2].sy - verts[0].sy) * (part[3].my - part[1].my) + / (verts[2].my - verts[0].my); + part[4].sy = seam; + part[3].sy = seam; + func_800AA210(&part[1], isQuad); + + part[1] = part[3]; + part[2] = part[4]; + part[2].my = 0; + part[1].my = 0; + part[3] = verts[2]; + part[4] = verts[3]; + func_800AA210(&part[1], isQuad); + return; + } + part[3] = verts[0]; + part[1] = part[3]; + part[4] = verts[1]; + part[2] = part[4]; + part[3].my = MAP_CELLS_Y - 1; + part[4].my = MAP_CELLS_Y - 1; + seam = (u16)part[1].sy + + (verts[2].sy - verts[0].sy) * (part[3].my - part[1].my) + / (verts[2].my - verts[0].my); + part[4].sy = seam; + part[3].sy = seam; + func_800AA210(&part[1], isQuad); + + part[1] = part[3]; + part[2] = part[4]; + part[2].my = 0; + part[1].my = 0; + part[3] = verts[2]; + part[4] = verts[3]; + part[3].my -= MAP_CELLS_Y; + part[4].my -= MAP_CELLS_Y; + func_800AA210(&part[1], isQuad); + return; + } + if (!spanX) { + return; + } + /* Same cut along the vertical seam, on the last column. */ + if (verts[0].mx < 0) { + part[1] = verts[0]; + part[0] = part[1]; + part[3] = verts[2]; + part[2] = part[3]; + part[1].mx = MAP_CELLS_X - 1; + part[3].mx = MAP_CELLS_X - 1; + part[0].mx += MAP_CELLS_X; + part[2].mx += MAP_CELLS_X; + seam = (u16)part[0].sx + + (verts[1].sx - verts[0].sx) * (part[1].mx - part[0].mx) + / (verts[1].mx - verts[0].mx); + part[3].sx = seam; + part[1].sx = seam; + func_800AA210(part, isQuad); + + part[0] = part[1]; + part[2] = part[3]; + part[2].mx = 0; + part[0].mx = 0; + part[1] = verts[1]; + part[3] = verts[3]; + func_800AA210(part, isQuad); + return; + } + part[1] = verts[0]; + part[0] = part[1]; + part[3] = verts[2]; + part[2] = part[3]; + part[1].mx = MAP_CELLS_X - 1; + part[3].mx = MAP_CELLS_X - 1; + seam = (u16)part[0].sx + + (verts[1].sx - verts[0].sx) * (part[1].mx - part[0].mx) + / (verts[1].mx - verts[0].mx); + part[3].sx = seam; + part[1].sx = seam; + func_800AA210(part, isQuad); + + part[0] = part[1]; + part[2] = part[3]; + part[2].mx = 0; + part[0].mx = 0; + part[1] = verts[1]; + part[3] = verts[3]; + part[1].mx -= MAP_CELLS_X; + part[3].mx -= MAP_CELLS_X; + func_800AA210(part, isQuad); + return; + } + + /* Triangle straddling a seam: it is emitted whole, with the split scratch + * doing double duty -- part[0] as the GTE distance vector and part[1] + * onward as the shade bytes. + * + * Addressing the shade through &part[1] is load-bearing, not laziness: an + * overlay struct with a shade field at +8 lets gcc fold the displacement + * into the store, whereas the original computes the shade base once and + * reuses it for both the loop store and the call below. */ + for (i = 0; i < isQuad + 3; i++) { + ((SVECTOR *)part)->vx = (u16)D_800D8808 - verts[i].sx; + ((SVECTOR *)part)->vy = (u16)D_800D880C - verts[i].sy; + ((SVECTOR *)part)->vz = 0; + gte_ldsv((SVECTOR *)part); + gte_SQR(0); + gte_stsv((SVECTOR *)part); + ((u8 *)&part[1])[i] = ~(SquareRoot0(((SVECTOR *)part)->vx + + ((SVECTOR *)part)->vy + + ((SVECTOR *)part)->vz) << 2); + } + verts[0].my = 0; + verts[0].mx = 0; + verts[1].my = 0; + verts[1].mx = 0; + verts[2].my = 0; + verts[2].mx = 0; + func_800AAD48((WorldVtx *)verts, (TriShade *)&part[1]); + D_800D8804++; +} /** * @brief Fill the current POLY_GT3 (@c D_800D8804) from a transformed @@ -1294,19 +2320,136 @@ static void func_800AAEAC(WorldVtx *vtx, QuadShade *shade) { -INCLUDE_ASM("asm/ovl/world/nonmatchings/we_object4", func_800AB06C); +/** Fixed world position the kind-11 particle is spawned at, a shade over six + * tiles east and thirteen south of the map origin. */ +static const VECTOR D_8009814C = { 6 * WORLD_TILE_SIZE + 29, -800, + 13 * WORLD_TILE_SIZE + 50, 0 }; + +/** + * @brief Spawn a kind-11 particle at the fixed world position @c D_8009814C. + * + * Claims the first inactive slot in the @c D_800D9CB0 pool (count >= limit), + * seeds it with kind 11, a zeroed rotation, @c scale = @c PARTICLE_SCALE_ONE and the kind's + * @c limit from @c D_800C5480[11], jitters scale by [-0x80, +0x7F] and limit + * by [-4, +3] (both jitter flags are set here), then projects the kind's + * @c offset vector through a rotation matrix built from the zeroed angles and + * stores the result as the slot's per-tick velocity. Always restores + * @c D_800C9838 as the GTE rotation and translation matrix on exit. + * + * This is @ref func_800AB2D4 with the caller-supplied position replaced by a + * constant and the one-in-256 spawn gate dropped, so it fires on every call. + * + * @note Dead code in the retail build: nothing calls it, and its address + * appears nowhere in world.bin. + * + * @note The spellings below are load-bearing for the byte-exact match + * (gcc 2.8.0 register allocation), not behavior. Each was confirmed by + * removing it on its own; the percentage is what the match drops to: + * - @c pp carries @c &local_pos into the loop, which is where the + * original reads the position from; reading @c local_pos directly + * addresses it off @c sp and loses the pointer. + * - @c rp is assigned inside the claimed-slot block, after the scan + * loop. Assigning it up front lets cse share one register with the + * @c memset argument, which then costs a fifth saved register. + * - the @c do / @c while(0) around the @c rot_in read (97.14) gives cse + * a block boundary so that copy's address temp is not reused as the + * @c RotMatrix first argument, which must rematerialize. + * - @c t holding @c flags & @c SLOT_FLAG_JITTER_SCALE across that copy + * (98.60), and @c proj_p holding @c &projected (96.83). + * - @c kind as a variable rather than a literal subscript (99.08). + * - the dead @c mp += flags & SLOT_FLAG_UNUSED4 (never set, so it folds + * to nothing) invalidates cse's record of @c &m so the + * @c gte_SetRotMatrix / @c gte_SetTransMatrix operands rematerialize + * @c &m after the @c RotMatrix call instead of holding it across it + * (96.98). + */ +static void func_800AB06C(void) { + VECTOR local_pos; + SVECTOR rot_in; + VECTOR projected; + SVECTOR vec_copy; + MATRIX m; + Slot30 *slot; + KindParams *kp; + SVECTOR *rp; + VECTOR *proj_p; + VECTOR *pp; + s32 flags; + s32 kind; + s32 t; + MATRIX *mp; + + local_pos = D_8009814C; + func_80047CE4(&rot_in, 0, sizeof(SVECTOR)); + + slot = D_800D9CB0; + proj_p = &projected; + pp = &local_pos; + kind = 11; + kp = &D_800C5480[kind]; + flags = SLOT_FLAG_JITTER_SCALE | SLOT_FLAG_JITTER_LIMIT; + + while (slot < &D_800D9CB0[64]) { + if (slot->count >= slot->limit) break; + slot++; + } + if (slot < &D_800D9CB0[64]) { + slot->kind = 11; + slot->count = 0; + slot->limit = kp->limit; + slot->scale = PARTICLE_SCALE_ONE; + slot->pos.vx = pp->vx; + slot->pos.vy = pp->vy; + slot->pos.vz = pp->vz; + + rp = &rot_in; + t = flags & SLOT_FLAG_JITTER_SCALE; + do { + vec_copy = *rp; + } while (0); + + if (t) { + slot->scale += func_8009CC3C() - 0x80; + } + if (flags & SLOT_FLAG_JITTER_LIMIT) { + slot->limit += (func_8009CC3C() & 7) - 4; + } + + slot->rot = vec_copy; + + mp = &m; + RotMatrix(&vec_copy, mp); + mp += flags & SLOT_FLAG_UNUSED4; + gte_SetRotMatrix(&m); + m.t[2] = 0; + m.t[1] = 0; + m.t[0] = 0; + gte_SetTransMatrix(&m); + + gte_ldv0(&kp->offset); + gte_mvmva(1, 0, 0, 0, 0); + gte_stlvnl(proj_p); + + slot->vel.vx = projected.vx; + slot->vel.vy = projected.vy; + slot->vel.vz = projected.vz; + } + + SetRotMatrix(&D_800C9838); + SetTransMatrix(&D_800C9838); +} /** * @brief Spawn a kind-0xE particle in the @c D_800D9CB0 pool at @p pos. * * Gated to fire on roughly 1 in 256 calls (@c func_8009CC3C returns * 0..0x7FFF). Claims the first inactive slot (count >= limit), seeds it - * with kind 0xE, a zeroed rotation, @c life = 0x1000 and the kind's - * @c limit from @c D_800C5480[14], jitters life by [-0x80, +0x7F] and + * with kind 0xE, a zeroed rotation, @c scale = @c PARTICLE_SCALE_ONE and the + * kind's @c limit from @c D_800C5480[14], jitters scale by [-0x80, +0x7F] and * limit by [-4, +3] (both jitter flags always set here), then * projects the kind's @c offset vector through a rotation matrix built - * from the zeroed angles and stores the result as the slot's projected - * screen XYZ. Always restores @c D_800C9838 as the GTE rotation and + * from the zeroed angles and stores the result as the slot's per-tick + * velocity. Always restores @c D_800C9838 as the GTE rotation and * translation matrix on exit. * * @note Dead code in the retail build: the only caller, func_800B99A4 @@ -1343,7 +2486,7 @@ void func_800AB2D4(VECTOR *pos) { MATRIX *mp; s32 t; - func_80047CE4(&rot_in, 0, 8); + func_80047CE4(&rot_in, 0, sizeof(SVECTOR)); local_pos = *pos; if (func_8009CC3C() < 0x80) { @@ -1352,7 +2495,7 @@ void func_800AB2D4(VECTOR *pos) { rp = &rot_in; kind = 0xE; kp = &D_800C5480[kind]; - flags = SLOT_FLAG_JITTER_LIFE | SLOT_FLAG_JITTER_LIMIT; + flags = SLOT_FLAG_JITTER_SCALE | SLOT_FLAG_JITTER_LIMIT; while (slot < &D_800D9CB0[64]) { if (slot->count >= slot->limit) break; @@ -1362,18 +2505,18 @@ void func_800AB2D4(VECTOR *pos) { slot->kind = 0xE; slot->count = 0; slot->limit = kp->limit; - slot->life = 0x1000; + slot->scale = PARTICLE_SCALE_ONE; slot->pos.vx = local_pos.vx; slot->pos.vy = local_pos.vy; slot->pos.vz = local_pos.vz; - t = flags & SLOT_FLAG_JITTER_LIFE; + t = flags & SLOT_FLAG_JITTER_SCALE; do { vec_copy = *rp; } while (0); if (t) { - slot->life += func_8009CC3C() - 0x80; + slot->scale += func_8009CC3C() - 0x80; } if (flags & SLOT_FLAG_JITTER_LIMIT) { slot->limit += (func_8009CC3C() & 7) - 4; @@ -1394,9 +2537,9 @@ void func_800AB2D4(VECTOR *pos) { gte_mvmva(1, 0, 0, 0, 0); gte_stlvnl(proj_p); - slot->proj_x = (s16)projected.vx; - slot->proj_y = (s16)projected.vy; - slot->proj_z = (s16)projected.vz; + slot->vel.vx = projected.vx; + slot->vel.vy = projected.vy; + slot->vel.vz = projected.vz; } } diff --git a/src/world/we_object5.c b/src/world/we_object5.c index 2a30d168..5e637a9d 100644 --- a/src/world/we_object5.c +++ b/src/world/we_object5.c @@ -335,11 +335,11 @@ void func_800ABDD8(CVECTOR *input, u16 *output, s32 z, s16 count) { * @param src 4 input SVECTORs (stride 8 — packed array). * @param rot Rotation angles for the inner matrix (passed to @c RotMatrix). * @param trans Translation (s16 components sign-extended into @c matrix.t). - * @param outSXY 4 packed s32 SXY outputs (16 bytes total). - * @param outOTZ s32 average-Z output. + * @param outSXY The four projected screen points. + * @param outOTZ Average-Z output, the ordering-table depth. */ void func_800ABEF0(SVECTOR *src, SVECTOR *rot, SVECTOR *trans, - s32 *outSXY, s32 *outOTZ) { + DVECTOR *outSXY, u32 *outOTZ) { MATRIX m; SVECTOR transformed[4]; s32 i; @@ -381,7 +381,7 @@ void func_800ABEF0(SVECTOR *src, SVECTOR *rot, SVECTOR *trans, * optionally jitters @c life and @c limit via the RNG @c func_8009CC3C, * builds a rotation matrix from @p vec via @c RotMatrix, pushes it to * the GTE (zero translation), then projects the kind's per-axis - * @c offset through the matrix into the slot's @c proj_xyz. + * @c offset through the matrix into the slot's @c vel. * * @note The two sentinel checks (pre-loop and post-loop) use the same * expression @c &D_800D9CB0[64] so gcc CSEs both branches into a @@ -393,7 +393,7 @@ void func_800ABEF0(SVECTOR *src, SVECTOR *rot, SVECTOR *trans, * @param type Slot kind — also index into the @c D_800C5480 KindParams table. * @param pos Source world position (12 bytes — only low s16 of each coord read). * @param vec Source rotation vector (8 bytes, unaligned-tolerant copy). - * @param flags @c SLOT_FLAG_JITTER_LIFE → RNG-jitter @c life by [-0x80, +0x7F]. + * @param flags @c SLOT_FLAG_JITTER_SCALE → RNG-jitter @c scale by [-0x80, +0x7F]. * @c SLOT_FLAG_JITTER_LIMIT → RNG-jitter @c limit by [-4, +3]. */ s32 func_800AC0A0(s32 type, VECTOR *pos, SVECTOR *vec, u16 flags) { @@ -417,7 +417,7 @@ s32 func_800AC0A0(s32 type, VECTOR *pos, SVECTOR *vec, u16 flags) { slot->kind = (u8)type; slot->count = 0; slot->limit = kp->limit; - slot->life = 0x1000; + slot->scale = 0x1000; slot->pos.vx = pos->vx; slot->pos.vy = pos->vy; slot->pos.vz = pos->vz; @@ -426,8 +426,8 @@ s32 func_800AC0A0(s32 type, VECTOR *pos, SVECTOR *vec, u16 flags) { vcp2 = vcp; memcpy(vcp2, vec, 8); - if (flags & SLOT_FLAG_JITTER_LIFE) { - slot->life += func_8009CC3C() - 0x80; + if (flags & SLOT_FLAG_JITTER_SCALE) { + slot->scale += func_8009CC3C() - 0x80; } if (flags & SLOT_FLAG_JITTER_LIMIT) { slot->limit += (func_8009CC3C() & 7) - 4; @@ -446,9 +446,9 @@ s32 func_800AC0A0(s32 type, VECTOR *pos, SVECTOR *vec, u16 flags) { gte_mvmva(1, 0, 0, 0, 0); gte_stlvnl(&projected); - slot->proj_x = (s16)projected.vx; - slot->proj_y = (s16)projected.vy; - slot->proj_z = (s16)projected.vz; + slot->vel.vx = (s16)projected.vx; + slot->vel.vy = (s16)projected.vy; + slot->vel.vz = (s16)projected.vz; } @@ -657,9 +657,9 @@ void func_800AC468(void *unused_a0, WorldViewXform *oa, WorldViewXform *mh, s32 * Treating each @c VECTOR as low-s16 world coords, this: * -# Computes the s16 component-wise delta and its squared distance via * the GTE @c SQR instruction. - * -# Takes the 3D and horizontal sqrts (@c func_8003F4A4) for the slant + * -# Takes the 3D and horizontal sqrts (@c SquareRoot0) for the slant * and ground-plane distances. - * -# Resolves @c pitch and @c yaw via @c func_80041E84 (atan2-like). + * -# Resolves @c pitch and @c yaw via @c ratan2 (atan2-like). * -# Calls @c worldPosToCell (with @p posB) to seed @c rotBuf[0] with the * view-yaw projection and returns the view-yaw. * -# Wraps the view-yaw and @p angleArg into per-axis tile deltas @@ -702,11 +702,11 @@ void func_800AC778(VECTOR *posA, VECTOR *posB, s16 angleArg, gte_SQR(0); gte_stlvnl(&sq); - distFull = func_8003F4A4(sq.vx + sq.vy + sq.vz); - distXZ = func_8003F4A4(sq.vx + sq.vz); + distFull = SquareRoot0(sq.vx + sq.vy + sq.vz); + distXZ = SquareRoot0(sq.vx + sq.vz); - angles.vx = func_80041E84(-delta.vy, distXZ); - angles.vy = func_80041E84(delta.vx, delta.vz); + angles.vx = ratan2(-delta.vy, distXZ); + angles.vy = ratan2(delta.vx, delta.vz); angles.vz = 0; offset.vx = 0; From 36f693acea27c209fed1a252780b04c4a34b20d1 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Thu, 20 Aug 2026 19:42:07 +0000 Subject: [PATCH 2/7] world: let we_object4 own its bss block instead of the linker script The world overlay had no bss subsegment, so every translation unit's .bss was empty and parked at 0x800C8635 while splat handed all 350 addresses above the loaded image straight to the linker as assignments (D_800D3930 = 0x800D3930). Under that layout the C file cannot own any of that memory: marking one of those externs static compiles and links with no diagnostic and silently produces a mismatched world.bin, because the object lands at the .bss cursor rather than at its address. Model the region instead. we_object4 takes 0x800D3510..0x800D4E80 -- the two strip sub-OT pools, the flat quads, the draw environments and the DR_LOAD record pool -- as a [.bss, we_object4] subsegment, with splat-owned fillers either side. Those thirteen objects become static definitions in address order and leave world.h, which no longer needs to publish four of them; the comment describing the sub-OT pool layout moves to the definitions it describes. Two objects in the block are not accounted for by anything that references them: 4 bytes at 0x800D398C and 8 at 0x800D3A48. gcc packs .bss flat rather than re-creating the original's alignment, so without them the section is 0xC short and every bss symbol above the block shifts down -- which surfaces as byte differences in we_object1's text, nowhere near this file. They are declared as named padding with their purpose recorded as unknown. The block totals 0x1970 and ends exactly where D_800D4E80 begins, so declaration order is address order and resizing one object shifts the rest. rm -rf build asm && make split && make verify: all 25 targets Match. --- config/ff8.yaml | 9 ++++++++- include/world.h | 9 --------- src/world/we_object4.c | 42 ++++++++++++++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/config/ff8.yaml b/config/ff8.yaml index a553348e..0a503986 100644 --- a/config/ff8.yaml +++ b/config/ff8.yaml @@ -519,12 +519,16 @@ binaries: options: target_path: original/world.bin section_order: [.rodata, .text, .data, .bss] + ld_bss_is_noload: true subalign: 1 segments: - name: world type: code start: 0x0 vram: 0x80098000 + # Work RAM past the loaded image (which ends 0x800C8635). we_object4 owns + # its own strip/backdrop pools at 0x800D3510; the rest is not yet split. + bss_size: 0x1FACB subsegments: - [0x0, rodata] - [0xEC, .rodata, we_object3] @@ -549,7 +553,10 @@ binaries: - [0x2780C, c, we_object11] - [0x2C0F8, c, we_object13] - [0x2CCA4, bin] - - [0x30635] + - [0x30640, bss, world_bss] + - [0x3B510, .bss, we_object4] + - [0x3CE80, bss, world_bss2] + - [0x50100] - name: SLUS_008.92 sha1: 40706b4e0553fc6cbeb044ca1e0e9004d5ac2561 options: diff --git a/include/world.h b/include/world.h index e572d232..38ad0ae2 100644 --- a/include/world.h +++ b/include/world.h @@ -524,15 +524,6 @@ typedef struct { extern BattleSceneCtx D_800CA040; /**< Worldmap "no-battle" sentinel — also functions as an empty BattleSceneCtx. */ extern s16 D_800C53B8[]; /**< Bone-id table (used by we_object4). */ -/* Worldmap sub-OT pools: [row][record][subslot], where row is the - * canonical-entity bit (D_800CA040 vs active ctx), each 0x60 record is a - * mini ordering table of 4 sub-slots, and bone prims get spliced between - * a record's last sub-slot ([3]) and its first ([0]). */ -extern OTSubSlot D_800D3E50[2][3][4]; /**< Primary worldmap sub-OT pool. */ -extern OTSubSlot D_800D4090[2][3][4]; /**< Secondary worldmap sub-OT pool. */ -extern OTSubSlot D_800D3510[2][2][4]; /**< Sub-OT pool spliced by func_800A6A74. */ -extern OTSubSlot D_800D3690[2][2][4]; /**< Second sub-OT pool spliced by func_800A6A74. */ - extern ScriptOp *func_800AF004(u8 *base, s32 flag); extern s32 func_800AF28C(ScriptOp *p); extern s32 func_800BEFC4(void); diff --git a/src/world/we_object4.c b/src/world/we_object4.c index 87713d8e..be8c0a51 100644 --- a/src/world/we_object4.c +++ b/src/world/we_object4.c @@ -154,18 +154,40 @@ extern POLY_GT4 D_800D595C[3]; /**< Map-panel quads for the D_800CA040 sent #define BAND1_SCRATCH_X 0x2C0 /* Worldmap backdrop prims, one set per scene bank. Each pair is drawn against - * two bone slots of the ordering table -- see func_800A7590. */ -extern DRAWENV D_800D3930; /**< Strip draw-env template, rebuilt per band. */ -extern DRAWENV D_800D3990[2]; /**< Per-scene draw-env, copied from each context. */ -extern POLY_F4 D_800D3810[2][3]; -extern POLY_F4 D_800D38A0[2][3]; -extern DR_ENV D_800D3A50[2][2]; -extern DR_ENV D_800D3B50[2][2]; -extern DR_ENV D_800D3C50[2][2]; -extern DR_ENV D_800D3D50[2][2]; + * two bone slots of the ordering table -- see func_800A7590. + * + * This unit owns the whole 0x800D3510..0x800D4E80 bss block (see the + * [0x3B510, .bss, we_object4] subsegment in config/ff8.yaml), so these are + * definitions, not externs. They are emitted in this order and their sizes + * are exact -- reordering them or resizing one shifts every object after it. + * gcc packs .bss with no alignment bump here, so the original's two gaps are + * declared explicitly as pad_* -- drop them and everything above the block + * shifts down 0xC. + * + * The sub-OT pools are [row][record][subslot], where row is the + * canonical-entity bit (D_800CA040 vs active ctx), each 0x60 record is a mini + * ordering table of 4 sub-slots, and bone prims get spliced between a + * record's last sub-slot ([3]) and its first ([0]). */ +static OTSubSlot D_800D3510[2][2][4]; /**< Sub-OT pool spliced by func_800A6A74. */ +static OTSubSlot D_800D3690[2][2][4]; /**< Second pool spliced by func_800A6A74. */ +static POLY_F4 D_800D3810[2][3]; /**< Band 0's three flat translucent quads, + per scene bank. */ +static POLY_F4 D_800D38A0[2][3]; /**< Band 1's, likewise. */ +static DRAWENV D_800D3930; /**< Strip draw-env template, rebuilt per band. */ +static u8 pad_800D398C[4]; /**< Unreferenced; preserves the original + spacing. Purpose unknown. */ +static DRAWENV D_800D3990[2]; /**< Per-scene draw-env, copied from each context. */ +static u8 pad_800D3A48[8]; /**< Unreferenced; preserves the original + spacing. Purpose unknown. */ +static DR_ENV D_800D3A50[2][2]; +static DR_ENV D_800D3B50[2][2]; +static DR_ENV D_800D3C50[2][2]; +static DR_ENV D_800D3D50[2][2]; +static OTSubSlot D_800D3E50[2][3][4]; /**< Primary worldmap sub-OT pool. */ +static OTSubSlot D_800D4090[2][3][4]; /**< Secondary worldmap sub-OT pool. */ /** One record per upload slot, per bank per bone slot; how many are live at * run time comes from @c func_800BEF6C. */ -extern DR_LOAD D_800D42D0[2][2][STRIP_LOAD_COUNT]; +static DR_LOAD D_800D42D0[2][2][STRIP_LOAD_COUNT]; extern POLY_FT4 D_800D88B0[2][64]; extern TILE D_800DA8D0[2][64]; From 6f6abb60031a6fe95dcb76d670f3f2beff8f5559 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Sat, 22 Aug 2026 18:36:12 +0000 Subject: [PATCH 3/7] battle: make func_800B054C static It is the one of the four functions flagged by the contribution check that really has no caller outside its own unit -- confirmed against the generated asm, not just the C sources. The other three stay as they are: func_800A61CC is called from bc_object19/func_800D72C8, func_800AAFB8 from bc_object5/func_800AB008, and func_800AEACC from two places in bc_object6, all still assembly. Making them static fails the link outright. make verify: 25/25 Match. --- src/battle/bc_object7.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/battle/bc_object7.c b/src/battle/bc_object7.c index c3452723..e3093420 100644 --- a/src/battle/bc_object7.c +++ b/src/battle/bc_object7.c @@ -2,6 +2,8 @@ #include "battle.h" #include "gamestate.h" +static s32 func_800B054C(u32 arg0); + extern u8 D_800EE490[]; extern u8 D_80077EBC[]; extern u8 D_800EEBE8[]; @@ -288,7 +290,7 @@ INCLUDE_ASM("asm/ovl/battle/nonmatchings/bc_object7", func_800B0414); INCLUDE_ASM("asm/ovl/battle/nonmatchings/bc_object7", func_800B04A0); -s32 func_800B054C(u32 arg0) { +static s32 func_800B054C(u32 arg0) { s32 i; for (i = 0; i < 32; i++) { From face52dc710f42d53c8e5003e4c4daaa09486d9f Mon Sep 17 00:00:00 2001 From: rengstrom Date: Sat, 22 Aug 2026 18:39:38 +0000 Subject: [PATCH 4/7] battle: give bc_object7 the header CONTRIBUTE.md asks for The unit had none, so its 23 externally-called functions were reachable only by whatever declaration each caller happened to write. The header now owns them, generated from the definitions rather than retyped, and the unit includes it. Only functions with a caller outside the unit are exported -- ten more are defined here and called from nowhere yet, so they stay unexported rather than being published on the assumption someone will want them. Adding prototypes can change codegen where a caller was relying on an implicit declaration, so this was verified rather than assumed. make verify: 25/25 Match. --- include/battle/bc_object7.h | 33 +++++++++++++++++++++++++++++++++ src/battle/bc_object7.c | 1 + 2 files changed, 34 insertions(+) create mode 100644 include/battle/bc_object7.h diff --git a/include/battle/bc_object7.h b/include/battle/bc_object7.h new file mode 100644 index 00000000..ddd01bfe --- /dev/null +++ b/include/battle/bc_object7.h @@ -0,0 +1,33 @@ +#ifndef BATTLE_BC_OBJECT7_H +#define BATTLE_BC_OBJECT7_H + +#include "common.h" + +/* bc_object7's public surface: the functions other units call. Names are still + * the raw addresses; the unit is only partly decompiled. */ + +extern void func_800AF654(void); +extern void func_800AF740(void); +extern void func_800AF8A4(s32 a0); +extern u8* func_800AFF30(s32 a0); +extern u8* func_800AFF70(s32 a0); +extern u8* func_800AFFB4(s32 a0); +extern void func_800AFFF4(void); +extern void func_800B0014(void); +extern void func_800B0034(void); +extern void func_800B0054(void); +extern s32 func_800B0074(s32 idx); +extern void func_800B00A8(void); +extern void func_800B00C8(void); +extern u8 *func_800B0248(u8* a0, u8 a1, u8* a2); +extern u8* func_800B0360(s32 a0); +extern void func_800B0574(s32 arg0, u32 arg1); +extern void func_800B0600(s32 a0, s32 a1); +extern s32 func_800B0668(s32 a0, s32 a1); +extern void func_800B06DC(u16 arg0); +extern void func_800B0754(s32 a0, s32 a1, s32 a2, u16 a3); +extern void func_800B0C08(void); +extern u16 func_800B0F7C(s32 arg0); +extern u16 func_800B1104(s32 a0); + +#endif /* BATTLE_BC_OBJECT7_H */ diff --git a/src/battle/bc_object7.c b/src/battle/bc_object7.c index e3093420..0859eebf 100644 --- a/src/battle/bc_object7.c +++ b/src/battle/bc_object7.c @@ -1,6 +1,7 @@ #include "common.h" #include "battle.h" #include "gamestate.h" +#include "battle/bc_object7.h" static s32 func_800B054C(u32 arg0); From dda6bba8efb9e291d4c14ef1df44b9e9dd3e833b Mon Sep 17 00:00:00 2001 From: rengstrom Date: Sat, 22 Aug 2026 18:47:01 +0000 Subject: [PATCH 5/7] battle: let bc_object7's header own its symbols Self-review of the header I just added: five of its 23 prototypes were already declared elsewhere, so I had left duplicates rather than removed them. Four -- func_800B0248, func_800B0574, func_800B0600 and func_800B0F7C -- move out of battle.h, and their four callers include the owner's header instead. The fifth is a genuine disagreement rather than a duplicate. bc_object7 defines func_800B0754 with a u16 fourth parameter; bc_object4 declares it (s32, s32, s32, s32). Taking it from the owner's header makes gcc convert the argument at the second call site and battle.bin stops matching, so the original TU evidently saw the wider prototype. The local declaration stays, with a comment saying why it is not taken from the header -- and the explicit (u16) cast at the other call site is the workaround that lying prototype already required. make verify: 25/25 Match. --- include/battle.h | 4 ---- src/battle/bc_object1.c | 1 + src/battle/bc_object2.c | 1 + src/battle/bc_object3.c | 1 + src/battle/bc_object4.c | 4 ++++ src/battle/bc_object5.c | 1 + 6 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/battle.h b/include/battle.h index 6503830c..3c0d5872 100644 --- a/include/battle.h +++ b/include/battle.h @@ -1188,15 +1188,12 @@ extern u8 D_800EEBD0; * ---------------------------------------------------------------- */ /** @brief Apply a status flag, ORing it into the flag word. */ -void func_800B0574(s32 arg0, u32 arg1); /** @brief Set @c field64[lowest-bit-of-a1] = -0x457 on entity @p a0. */ -void func_800B0600(s32 a0, s32 a1); u16 func_800B1050(s32 stat); /** @brief Look up auxiliary ability flags by stat byte (low bits). */ -u16 func_800B0F7C(s32 stat); void func_800B3128(u8 *a0); @@ -1212,7 +1209,6 @@ void func_800A1760(s32 arg0, BattleCharData* arg1); u8 *func_800B04A0(s32 a0, u8 *buf); /** @brief Concatenate two parts into the @c D_800EEBE8 message buffer. */ -u8* func_800B0248(u8* arg0, u8 arg1, u8* arg2); /** @brief Finalize the @c D_800EEBE8 message buffer. */ u8 *func_800B02AC(u8 *buf); diff --git a/src/battle/bc_object1.c b/src/battle/bc_object1.c index 924546d3..94be1120 100644 --- a/src/battle/bc_object1.c +++ b/src/battle/bc_object1.c @@ -2,6 +2,7 @@ #include "battle.h" #include "gf.h" #include "battle/bc_object1.h" +#include "battle/bc_object7.h" /** * @file bc_object1.c diff --git a/src/battle/bc_object2.c b/src/battle/bc_object2.c index 7ebaf93a..54865ed0 100644 --- a/src/battle/bc_object2.c +++ b/src/battle/bc_object2.c @@ -10,6 +10,7 @@ #include "gamestate.h" #include "battle/bc_object2.h" #include "battle/bc_object6.h" +#include "battle/bc_object7.h" diff --git a/src/battle/bc_object3.c b/src/battle/bc_object3.c index 384d03a5..3907cad5 100644 --- a/src/battle/bc_object3.c +++ b/src/battle/bc_object3.c @@ -3,6 +3,7 @@ #include "battle.h" #include "battle/bc_object2.h" #include "battle/bc_object3.h" +#include "battle/bc_object7.h" SoundCmd* func_8009B134(s16 arg0, u8 arg1, s32 unused); diff --git a/src/battle/bc_object4.c b/src/battle/bc_object4.c index 3fb5deca..139f3152 100644 --- a/src/battle/bc_object4.c +++ b/src/battle/bc_object4.c @@ -6,6 +6,10 @@ extern u8 D_800EE464[]; extern u8 D_800EE38C[]; extern u8 D_800EE9B3[]; extern u8 g_gameState[]; +/* bc_object7 defines this with a u16 fourth parameter, but declaring it that + way here makes gcc convert the argument at the second call site and the + overlay stops matching -- the original TU evidently saw a wider prototype. + Kept local, and deliberately not taken from battle/bc_object7.h. */ void func_800B0754(s32, s32, s32, s32); void decrementItemByType(s32); s32 func_800AA4E8(void); diff --git a/src/battle/bc_object5.c b/src/battle/bc_object5.c index 0b0e6011..b153c562 100644 --- a/src/battle/bc_object5.c +++ b/src/battle/bc_object5.c @@ -3,6 +3,7 @@ #include "gamestate.h" #include "battle/bc_object5.h" #include "battle/bc_object6.h" +#include "battle/bc_object7.h" extern u8 D_800E3CEC[]; void func_800AB054(void); From dc47f45c6085acbfe8639af16d5f99c135c53388 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Sat, 22 Aug 2026 18:49:55 +0000 Subject: [PATCH 6/7] battle: give bc_object4 a header Signed-off-by: rengstrom --- config/ff8.yaml | 2 -- include/battle/bc_object4.h | 29 +++++++++++++++++++++++++++++ src/battle/bc_object4.c | 1 + 3 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 include/battle/bc_object4.h diff --git a/config/ff8.yaml b/config/ff8.yaml index 0a503986..3c33ddb8 100644 --- a/config/ff8.yaml +++ b/config/ff8.yaml @@ -526,8 +526,6 @@ binaries: type: code start: 0x0 vram: 0x80098000 - # Work RAM past the loaded image (which ends 0x800C8635). we_object4 owns - # its own strip/backdrop pools at 0x800D3510; the rest is not yet split. bss_size: 0x1FACB subsegments: - [0x0, rodata] diff --git a/include/battle/bc_object4.h b/include/battle/bc_object4.h new file mode 100644 index 00000000..44678c18 --- /dev/null +++ b/include/battle/bc_object4.h @@ -0,0 +1,29 @@ +#ifndef BATTLE_BC_OBJECT4_H +#define BATTLE_BC_OBJECT4_H + +#include "common.h" + +/* bc_object4's public surface: the functions other units call. Names are still + * the raw addresses; the unit is only partly decompiled. */ + +extern void func_800A6184(s32 arg0, s32 arg1, s32 arg2, u16 arg3); +extern void func_800A61CC(s32 arg0, s32 arg1, s32 arg2, s32 arg3, s32 arg4, u16 arg5); +extern void func_800A6218(s32 a0); +extern void func_800A6288(s32 a0, s32 a1, s32 a2, s32 a3); +extern void func_800A62B0(void); +extern void func_800A6310(s32 a0); +extern void func_800A63C0(s16 a, s16 b, s16 c, s16 d); +extern void func_800A64E4(s32 a0, s32 a1); +extern void func_800A6574(s32 value); +extern void func_800A6724(void); +extern s32 func_800A6DD8(void); +extern void func_800A7518(s32 idx); +extern void func_800A779C(s32 a0); +extern s32 func_800A7A44(s32 a0); +extern s32 func_800A7A8C(s32 idx); +extern s32 func_800A7AB8(s32 a0); +extern void func_800A853C(void); +extern void func_800A890C(s32 a0); +extern s32 func_800A8AFC(s32 a0); + +#endif /* BATTLE_BC_OBJECT4_H */ diff --git a/src/battle/bc_object4.c b/src/battle/bc_object4.c index 139f3152..eadac99b 100644 --- a/src/battle/bc_object4.c +++ b/src/battle/bc_object4.c @@ -1,6 +1,7 @@ #include "common.h" #include "battle.h" #include "gf.h" +#include "battle/bc_object4.h" extern u8 D_800EE464[]; extern u8 D_800EE38C[]; From 9186e7236c6ff50846619d9b885bf118ca9fae04 Mon Sep 17 00:00:00 2001 From: rengstrom Date: Sun, 23 Aug 2026 20:59:52 +0000 Subject: [PATCH 7/7] Cleanup Signed-off-by: rengstrom --- config/symbols.extern.txt | 1 + include/battle/bc_object4.h | 3 --- include/battle/bc_object7.h | 3 --- include/psxsdk/libgte.h | 1 + include/world/we_object10.h | 1 - src/battle/bc_object4.c | 10 +++------- src/world/we_object4.c | 28 +++------------------------- 7 files changed, 8 insertions(+), 39 deletions(-) diff --git a/config/symbols.extern.txt b/config/symbols.extern.txt index e4dfcaf4..dad6faa8 100644 --- a/config/symbols.extern.txt +++ b/config/symbols.extern.txt @@ -20,6 +20,7 @@ VectorNormal = 0x8003F5D4; SquareRoot12 = 0x8003FA74; CompMatrix = 0x8003FC24; ApplyMatrixLV = 0x8003FD84; +ApplyMatrixSV = 0x800404D4; MulMatrix = 0x80040264; ScaleMatrix = 0x80040564; SetRotMatrix = 0x800406A4; diff --git a/include/battle/bc_object4.h b/include/battle/bc_object4.h index 44678c18..6d306de3 100644 --- a/include/battle/bc_object4.h +++ b/include/battle/bc_object4.h @@ -3,9 +3,6 @@ #include "common.h" -/* bc_object4's public surface: the functions other units call. Names are still - * the raw addresses; the unit is only partly decompiled. */ - extern void func_800A6184(s32 arg0, s32 arg1, s32 arg2, u16 arg3); extern void func_800A61CC(s32 arg0, s32 arg1, s32 arg2, s32 arg3, s32 arg4, u16 arg5); extern void func_800A6218(s32 a0); diff --git a/include/battle/bc_object7.h b/include/battle/bc_object7.h index ddd01bfe..1864e1d4 100644 --- a/include/battle/bc_object7.h +++ b/include/battle/bc_object7.h @@ -3,9 +3,6 @@ #include "common.h" -/* bc_object7's public surface: the functions other units call. Names are still - * the raw addresses; the unit is only partly decompiled. */ - extern void func_800AF654(void); extern void func_800AF740(void); extern void func_800AF8A4(s32 a0); diff --git a/include/psxsdk/libgte.h b/include/psxsdk/libgte.h index d8301244..ca42f89b 100644 --- a/include/psxsdk/libgte.h +++ b/include/psxsdk/libgte.h @@ -52,6 +52,7 @@ void SetFarColor(s32 rfc, s32 gfc, s32 bfc); /* --- GTE transform operations --- */ VECTOR *ApplyMatrixLV(MATRIX *m, VECTOR *v0, VECTOR *v1); +SVECTOR *ApplyMatrixSV(MATRIX *m, SVECTOR *v0, SVECTOR *v1); s32 RotTransPers(SVECTOR *v0, s32 *sxy, s32 *p, s32 *flag); /** @brief The main binary's own @c RotTransPers copy; overlays link it by address, so it * keeps its @c func_ name. */ diff --git a/include/world/we_object10.h b/include/world/we_object10.h index bd7fe726..15462deb 100644 --- a/include/world/we_object10.h +++ b/include/world/we_object10.h @@ -60,7 +60,6 @@ extern s32 func_800BA870(u8 *p); extern s32 func_800B0010(void); extern void func_800AC0A0(s32 marker, VECTOR *position, SVECTOR *vec, s32 zero); extern void *func_80047CE4(void *dst, s32 c, u32 n); -/* Nonzero when any active slot carries the 0x4F marker. */ extern s32 func_800BEF6C(void); extern s32 func_8009CC3C(void); extern void func_800AEB58(XformEntry *entry, XformGroup *group); diff --git a/src/battle/bc_object4.c b/src/battle/bc_object4.c index eadac99b..39162b16 100644 --- a/src/battle/bc_object4.c +++ b/src/battle/bc_object4.c @@ -1,17 +1,13 @@ #include "common.h" #include "battle.h" #include "gf.h" +#include "gamestate.h" #include "battle/bc_object4.h" extern u8 D_800EE464[]; extern u8 D_800EE38C[]; extern u8 D_800EE9B3[]; -extern u8 g_gameState[]; -/* bc_object7 defines this with a u16 fourth parameter, but declaring it that - way here makes gcc convert the argument at the second call site and the - overlay stops matching -- the original TU evidently saw a wider prototype. - Kept local, and deliberately not taken from battle/bc_object7.h. */ -void func_800B0754(s32, s32, s32, s32); + void decrementItemByType(s32); s32 func_800AA4E8(void); void func_800E1850(void); @@ -619,7 +615,7 @@ void func_800A779C(s32 a0) { s32 val = a0 - 1; s32 q = val / 32; s32 r = val - q * 32; - s32 base = (s32)g_gameState; + s32 base = (s32)&g_gameState; *(s32 *)(base + q * 4 + 0xD04) |= (1 << r); } } diff --git a/src/world/we_object4.c b/src/world/we_object4.c index be8c0a51..6a30b75e 100644 --- a/src/world/we_object4.c +++ b/src/world/we_object4.c @@ -8,8 +8,6 @@ #include "world/we_object10.h" #include "world/we_object4.h" -/* ---- Private to this unit: nothing outside we_object4 references these. ---- */ - extern TILE_1 D_800D5430[96]; /**< Second half of the star pool (== &D_800D4FB0[1][0]). */ extern DR_TPAGE D_800D58B0[2]; /**< Draw-mode packet closing the star layer, one per scene. */ @@ -42,18 +40,12 @@ extern s32 D_800D4EB0[4]; /**< Last blitted frame per slot. */ extern u16 D_800D2452; /**< Map-view HUD slide-in coordinate fed to the func_800A84xx drawers. */ extern s32 D_800C5454; /**< Likely nonzero while the map pointer highlights a named location — enables the name banner (func_800A8524). */ - -/* Map-view HUD drawers of this unit (see func_800A8400 for the driving values). */ - - /* Camera-follow reference for the kind-12/13 drift in func_800A9CC0. * volatile is load-bearing: it keeps this read ordered against the * particle position stores, matching the original schedule. */ extern volatile s32 D_800C974C; /* Main-binary helper: applies matrix @p m to @p in, writing @p out. */ -extern void func_800404D4(MATRIX *m, SVECTOR *in, SVECTOR *out); - /** * @brief POLY_GT3 view with packed 32-bit vertex words (0x28 bytes). @@ -94,7 +86,6 @@ typedef struct { extern WorldPolyGT3 *D_800D8804; /**< Current POLY_GT3 slot being filled. */ - /** * @brief POLY_GT4 view with packed 32-bit vertex words (0x34 bytes). * @@ -154,20 +145,7 @@ extern POLY_GT4 D_800D595C[3]; /**< Map-panel quads for the D_800CA040 sent #define BAND1_SCRATCH_X 0x2C0 /* Worldmap backdrop prims, one set per scene bank. Each pair is drawn against - * two bone slots of the ordering table -- see func_800A7590. - * - * This unit owns the whole 0x800D3510..0x800D4E80 bss block (see the - * [0x3B510, .bss, we_object4] subsegment in config/ff8.yaml), so these are - * definitions, not externs. They are emitted in this order and their sizes - * are exact -- reordering them or resizing one shifts every object after it. - * gcc packs .bss with no alignment bump here, so the original's two gaps are - * declared explicitly as pad_* -- drop them and everything above the block - * shifts down 0xC. - * - * The sub-OT pools are [row][record][subslot], where row is the - * canonical-entity bit (D_800CA040 vs active ctx), each 0x60 record is a mini - * ordering table of 4 sub-slots, and bone prims get spliced between a - * record's last sub-slot ([3]) and its first ([0]). */ + * two bone slots of the ordering table */ static OTSubSlot D_800D3510[2][2][4]; /**< Sub-OT pool spliced by func_800A6A74. */ static OTSubSlot D_800D3690[2][2][4]; /**< Second pool spliced by func_800A6A74. */ static POLY_F4 D_800D3810[2][3]; /**< Band 0's three flat translucent quads, @@ -1017,7 +995,7 @@ static void func_800A8024(void) { * Builds a rotation matrix from the world transform's tail angle * (@c D_800D2390.tail.angle) with no translation, turns the map's zoom * distance (@c D_800C4D30 * 4, biased by the transform's head field) into - * a view-space offset through @c func_800404D4, shifts it by the camera + * a view-space offset through @c ApplyMatrixSV, shifts it by the camera * offset in @c D_800C9770, and runs the result through the GTE with the * world-to-screen matrix @c D_800C9838. The projected y is pulled up by * one unit per 300 units of head angle, tilting the marker with the map. @@ -1044,7 +1022,7 @@ void func_800A8270(SVECTOR *out) { rot.vy = 0; rot.vz = (u16)D_800C4D30 * 4; rot.vz = rot.vz + D_800D2390.head.unk4; - func_800404D4(&m, &rot, &rot); + ApplyMatrixSV(&m, &rot, &rot); rot.vx += D_800C9770[0].vx; rot.vz += D_800C9770[0].vz;