From 2da890a551190b3a0280c3c87001dcff8da4049f Mon Sep 17 00:00:00 2001 From: penta3 Date: Mon, 27 Jul 2026 00:09:35 -0300 Subject: [PATCH 01/12] fix(VehPhys): be consistent with ps1 assembly --- game/Vehicle/VehCalc.c | 6 ++++- game/Vehicle/VehPhysForce.c | 17 ++++++------- game/Vehicle/VehPhysProc.c | 47 ++++++++++++++++++++---------------- game/Vehicle/VehPickupItem.c | 4 ++- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/game/Vehicle/VehCalc.c b/game/Vehicle/VehCalc.c index 3f9a64fa9..36440ad18 100644 --- a/game/Vehicle/VehCalc.c +++ b/game/Vehicle/VehCalc.c @@ -22,9 +22,13 @@ int VehCalc_InterpBySpeed(int val, int speed, int desired) { return desired; } + + return val; } - else + // Retail returns `val` untouched when it already equals `desired`; only the + // strictly-less case adds `speed`. + if (val < desired) { val = CTR_MipsAddLo(val, speed); diff --git a/game/Vehicle/VehPhysForce.c b/game/Vehicle/VehPhysForce.c index c2376c00f..5abe63f0a 100644 --- a/game/Vehicle/VehPhysForce.c +++ b/game/Vehicle/VehPhysForce.c @@ -1100,18 +1100,13 @@ void VehPhysForce_TranslateMatrix(struct Thread *thread, struct Driver *driver) VehPhysForce_TranslateMatrix_UpdateWake(inst, driver); } +// Retail feeds LZCS and reads LZCR unconditionally, before it +// branches on `denom`. Callers observe that CP2 state, so use the GTE here static int VehPhysForce_CountLeadingSignBits(s32 value) { - u32 bits = (u32)value; - u32 sign = bits >> 31; - int count = 0; + MTC2((u32)value, 30); - while (count < 32 && (((bits >> (31 - count)) & 1) == sign)) - { - count++; - } - - return count; + return (int)MFC2(31); } static struct TrigPair VehPhysForce_TrigAngleSinCos(int angle) @@ -1170,6 +1165,8 @@ void VehPhysForce_RotAxisAngle(MATRIX *m, s16 *normVec, s16 angle) m->m[1][1] = (s16)normalY; m->m[2][1] = (s16)normalZ; + int leadingSignBits = VehPhysForce_CountLeadingSignBits(denom); + if (denom == 0) { s32 dot = CTR_MipsAddLo(CTR_MipsMulLo(trig.sin, normalX), CTR_MipsMulLo(trig.cos, normalZ)); @@ -1183,7 +1180,7 @@ void VehPhysForce_RotAxisAngle(MATRIX *m, s16 *normVec, s16 angle) } else { - int shift = CTR_MipsSubLo(0x14, VehPhysForce_CountLeadingSignBits(denom)); + int shift = CTR_MipsSubLo(0x14, leadingSignBits); s32 sinRemainder; s32 cosRemainder; s32 divX; diff --git a/game/Vehicle/VehPhysProc.c b/game/Vehicle/VehPhysProc.c index 8a6924159..ceb0265fd 100644 --- a/game/Vehicle/VehPhysProc.c +++ b/game/Vehicle/VehPhysProc.c @@ -24,7 +24,9 @@ enum VEH_PHYS_PROC_TEN_WUMPA_COUNT = 10, VEH_PHYS_PROC_HAZARD_MOVING_SPEED_MIN = 0x100, VEH_PHYS_PROC_HAZARD_LOW_SPEED_THRESHOLD = 0x101, - VEH_PHYS_PROC_HAZARD_TIMER_EVEN_MASK = 0xfffe, + // Retail materializes this as `addiu v0, zero, -2` (0xfffffffe), so the mask + // must stay 32-bit signed or the sign of hazardTimer is lost. + VEH_PHYS_PROC_HAZARD_TIMER_EVEN_MASK = ~1, VEH_PHYS_PROC_CLOCK_WADDLE_TIMER_SHIFT = 6, VEH_PHYS_PROC_CLOCK_WADDLE_TIMER_MAX = 0x40, VEH_PHYS_PROC_CLOCK_WADDLE_TRIG_SHIFT = 4, @@ -116,7 +118,7 @@ CTR_STATIC_ASSERT(VEH_PHYS_PROC_DISTANCE_SPEED_SHIFT == 8); CTR_STATIC_ASSERT(VEH_PHYS_PROC_TEN_WUMPA_COUNT == 10); CTR_STATIC_ASSERT(VEH_PHYS_PROC_HAZARD_MOVING_SPEED_MIN == 0x100); CTR_STATIC_ASSERT(VEH_PHYS_PROC_HAZARD_LOW_SPEED_THRESHOLD == 0x101); -CTR_STATIC_ASSERT(VEH_PHYS_PROC_HAZARD_TIMER_EVEN_MASK == 0xfffe); +CTR_STATIC_ASSERT(VEH_PHYS_PROC_HAZARD_TIMER_EVEN_MASK == -2); CTR_STATIC_ASSERT(VEH_PHYS_PROC_CLOCK_WADDLE_TIMER_SHIFT == 6); CTR_STATIC_ASSERT(VEH_PHYS_PROC_CLOCK_WADDLE_TIMER_MAX == 0x40); CTR_STATIC_ASSERT(VEH_PHYS_PROC_CLOCK_WADDLE_TRIG_SHIFT == 4); @@ -307,23 +309,6 @@ void VehPhysProc_Driving_PhysLinear(struct Thread *thread, struct Driver *driver VehPhysProc_Driving_DecrementTimer(&driver->clockReceive, msPerFrame); VehPhysProc_Driving_DecrementTimer(&driver->accelTapWindowTimer, msPerFrame); - // If invisible, without Permanent Invisibility cheat, - // dont remove invisibleTimer check, or an invalid - // instFlagsBackup overwrites instFlags - if ((driver->invisibleTimer != 0) && ((gameMode2 & CHEAT_INVISIBLE) == 0)) - { - driver->invisibleTimer = CTR_MipsSubLo(driver->invisibleTimer, msPerFrame); - - // if newly visible - if (driver->invisibleTimer <= 0) - { - driver->invisibleTimer = 0; - driver->instSelf->flags = driver->instFlagsBackup; - driver->instSelf->alphaScale = 0; - OtherFX_Play(VEH_PHYS_PROC_INVISIBLE_REAPPEAR_FX, 1); - } - } - if (0 < driver->jump_TenBuffer) { driver->jump_TenBuffer = (s16)CTR_MipsSubLo(driver->jump_TenBuffer, 1); @@ -552,6 +537,23 @@ void VehPhysProc_Driving_PhysLinear(struct Thread *thread, struct Driver *driver } } + // If invisible, without Permanent Invisibility cheat, + // dont remove invisibleTimer check, or an invalid + // instFlagsBackup overwrites instFlags + if ((driver->invisibleTimer != 0) && ((gameMode2 & CHEAT_INVISIBLE) == 0)) + { + driver->invisibleTimer = CTR_MipsSubLo(driver->invisibleTimer, msPerFrame); + + // if newly visible + if (driver->invisibleTimer <= 0) + { + driver->invisibleTimer = 0; + driver->instSelf->flags = driver->instFlagsBackup; + driver->instSelf->alphaScale = 0; + OtherFX_Play(VEH_PHYS_PROC_INVISIBLE_REAPPEAR_FX, 1); + } + } + // === Normal Vector === @@ -1916,10 +1918,13 @@ void VehPhysProc_PowerSlide_PhysAngular(struct Thread *th, struct Driver *driver // near-spinout distortion SFX driver->turnWobbleAngle = turnWobbleAngleNext; - driver->ampTurnState = (s16)CTR_MipsAddLo(signedSpinRate, driftTurnInput); + // Retail keeps the 32-bit sum live and only truncates on the store, so the + // multiply below must not go through the s16 field. + int ampTurnState = CTR_MipsAddLo(signedSpinRate, driftTurnInput); + driver->ampTurnState = (s16)ampTurnState; driver->angle = (s16)ANG_MODULO_TWO_PI( - CTR_MipsAddLo((u16)driver->angle, CTR_MipsSra(CTR_MipsMulLo(driver->ampTurnState, gGT->elapsedTimeMS), VEH_PHYS_PROC_ANGLE_INTEGRATION_SHIFT))); + CTR_MipsAddLo((u16)driver->angle, CTR_MipsSra(CTR_MipsMulLo(ampTurnState, gGT->elapsedTimeMS), VEH_PHYS_PROC_ANGLE_INTEGRATION_SHIFT))); if (driver->KartStates.Drifting.driftBoostTimeMS != 0) { diff --git a/game/Vehicle/VehPickupItem.c b/game/Vehicle/VehPickupItem.c index 454675bc5..83f657cc1 100644 --- a/game/Vehicle/VehPickupItem.c +++ b/game/Vehicle/VehPickupItem.c @@ -502,7 +502,9 @@ b32 VehPickupItem_PotionThrow(struct MineWeapon *mine, struct Instance *inst, u3 return 0; } - throwVelocity = (MixRNG_Scramble() & POTION_THROW_RANDOM_MASK) - POTION_THROW_RANDOM_BIAS; + // Retail draws from advRng here, not from the MixRNG stream the item + // roulette uses. + throwVelocity = (RngDeadCoed(&sdata->advRng) & POTION_THROW_RANDOM_MASK) - POTION_THROW_RANDOM_BIAS; } else { From d872d9b1c8aa720c307a3b51e7a3ad6a300d8a6d Mon Sep 17 00:00:00 2001 From: penta3 Date: Mon, 27 Jul 2026 04:32:13 -0300 Subject: [PATCH 02/12] fix(VehPickup): align code order to ps1 asm, consistency --- game/Vehicle/VehPickupItem.c | 215 +++++++++++++++++++++-------------- 1 file changed, 127 insertions(+), 88 deletions(-) diff --git a/game/Vehicle/VehPickupItem.c b/game/Vehicle/VehPickupItem.c index 83f657cc1..48ceae558 100644 --- a/game/Vehicle/VehPickupItem.c +++ b/game/Vehicle/VehPickupItem.c @@ -26,7 +26,6 @@ static inline void VehPickupItem_ClearMineMotion(struct MineWeapon *mine) enum { - MASK_GOOD_GUY_CHARACTER_BITS = 0x20c9, MASK_MODEL_COUNT = 2, MASK_SOUND_ID_OFFSET_FROM_MODEL = 0x1a, MASK_BEAM_MODEL_STRIDE = 2, @@ -132,7 +131,6 @@ enum VOICELINE_WEAPON_PRIORITY = 0x10, }; -CTR_STATIC_ASSERT(MASK_GOOD_GUY_CHARACTER_BITS == 0x20c9); CTR_STATIC_ASSERT(MASK_MODEL_COUNT == 2); CTR_STATIC_ASSERT(MASK_SOUND_ID_OFFSET_FROM_MODEL == 0x1a); CTR_STATIC_ASSERT(MASK_BEAM_MODEL_STRIDE == 2); @@ -236,10 +234,14 @@ b32 VehPickupItem_MaskBoolGoodGuy(struct Driver *d) { s32 charID = data.characterIDs[d->driverID]; - // Crash, Coco, Pura, Polar, Penta - u32 maskBits = MASK_GOOD_GUY_CHARACTER_BITS; + // Retail compares against each ID in turn instead of indexing a bitmask, so + // any charID outside the set returns 0 no matter how large or negative it is. + if ((charID == CRASH_BANDICOOT) || (charID == COCO_BANDICOOT) || (charID == POLAR) || (charID == PURA) || (charID == PENTA_PENGUIN)) + { + return 1; + } - return (maskBits >> charID) & 1; + return 0; } // NOTE(aalhendi): ASM-verified NTSC-U 926 0x80064c38-0x80064f94. @@ -535,8 +537,7 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) struct TrackerWeapon *tw; struct GameTracker *gGT = sdata->gGT; int modelID; - int mineHitModel = 0; - int mineShouldInitFollower = 0; + int mineHitModelFlags = 0; switch (weaponID) { @@ -632,19 +633,21 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) dInst = d->instSelf; - // set up missile - modelID = DYNAMIC_ROCKET; - int bucket = TRACKING; - struct Thread *parentTh = 0; - char *weaponName = rdata.s_bombtracker1; + // set up bomb + modelID = DYNAMIC_BOMB; + int bucket = OTHER; + struct Thread *parentTh = dInst->thread; + char *weaponName = sdata->s_bomb1; - // bomb - if ((d->heldItemID == HELD_ITEM_BOMB_1X) || (d->heldItemID == HELD_ITEM_BOMB_3X)) + // missile. Retail asks "is it a missile?" here (0x80065640) and "is it a + // bomb?" at every other branch of this case, so keep both questions in + // their retail form instead of collapsing them into one. + if ((d->heldItemID == HELD_ITEM_MISSILE_1X) || (d->heldItemID == HELD_ITEM_MISSILE_3X)) { - modelID = DYNAMIC_BOMB; - bucket = OTHER; - parentTh = dInst->thread; - weaponName = sdata->s_bomb1; + modelID = DYNAMIC_ROCKET; + bucket = TRACKING; + parentTh = 0; + weaponName = rdata.s_bombtracker1; } // medium stack pool @@ -674,8 +677,8 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) int talk; - // bomb - if (modelID == DYNAMIC_BOMB) + // bomb (retail 0x80065760) + if ((d->heldItemID == HELD_ITEM_BOMB_1X) || (d->heldItemID == HELD_ITEM_BOMB_3X)) { talk = VOICELINE_BOMB_LAUNCH; d->instBombThrow = weaponInst; @@ -726,8 +729,8 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) tw->flags |= TRACKER_FLAG_POWERED_UP; } - // bomb - if (modelID == DYNAMIC_BOMB) + // bomb (retail 0x80065854) + if ((d->heldItemID == HELD_ITEM_BOMB_1X) || (d->heldItemID == HELD_ITEM_BOMB_3X)) { struct GamepadBuffer *gb = &sdata->gGamepads->gamepad[d->driverID]; @@ -807,8 +810,10 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) RB_MinePool_Add(mw); VehPickupItem_PotionThrow(mw, weaponInst, flags); - mineHitModel = weaponInst->model->id | COLL_MODELID_BLOCKAGE_FLAG; - mineShouldInitFollower = (flags == 0); + + // Only the TNT/nitro copy of the block below ORs in the blockage bit + // (retail 0x8006616c); the beaker copy does not (0x80066534). + mineHitModelFlags = COLL_MODELID_BLOCKAGE_FLAG; RunMineCOLL:; @@ -840,7 +845,7 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) if (sps->boolDidTouchHitbox != 0) { - sps->Input1.modelID = mineHitModel; + sps->Input1.modelID = weaponInst->model->id | mineHitModelFlags; RB_Hazard_CollLevInst(sps, weaponTh); @@ -861,6 +866,11 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) COLL_SearchBSP_CallbackQUADBLK(&probeTop, &probeBottom, sps, 0); } + else + { + mw->crateInst = 0; + } + RB_MakeInstanceReflective(sps, weaponInst); SVec3 fallbackNormal; @@ -882,17 +892,29 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) VehPhysForce_RotAxisAngle(&weaponInst->matrix, rotationNormal, d->angle); + // Retail inlines this block twice. The TNT/nitro copy (0x80066250) sets + // instTntSend and the flag before the follower and only spawns it when no + // throw was requested; the beaker copy (0x8006660c) always spawns the + // follower and sets the flag afterwards. if (weaponID == WEAPON_ID_MINE) { d->instTntSend = weaponInst; - } - // dropped a mine - d->actionsFlagSet |= ACTION_DROPPING_MINE; + // dropped a mine + d->actionsFlagSet |= ACTION_DROPPING_MINE; - if (mineShouldInitFollower != 0) + if (flags == 0) + { + RB_Follower_Init(d, weaponTh); + } + } + + else { RB_Follower_Init(d, weaponTh); + + // dropped a mine + d->actionsFlagSet |= ACTION_DROPPING_MINE; } break; @@ -908,12 +930,20 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) { return; } + + weaponTh = weaponInst->thread; + mw = weaponTh->object; + mw->flags = 0; } else { modelID = STATIC_BEAKER_RED; weaponInst = INSTANCE_BirthWithThread(modelID, sdata->s_beaker1, SMALL, MINE, RB_GenericMine_ThTick, sizeof(struct MineWeapon), 0); + + weaponTh = weaponInst->thread; + mw = weaponTh->object; + mw->flags = MINE_WEAPON_FLAG_RED_BEAKER; } dInst = d->instSelf; @@ -923,10 +953,20 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) // potion always faces camera weaponInst->model->headers[0].flags |= BEAKER_MODEL_HEADER_CAMERA_FLAG; - weaponTh = weaponInst->thread; weaponTh->funcThDestroy = PROC_DestroyInstance; weaponTh->funcThCollide = (void *)RB_Hazard_ThCollide_Generic; + // Retail only clears driverTarget on the TNT/nitro path, so a + // beaker inherits whatever the recycled pool object held. Nothing reads it + // for a beaker today (RB_Hazard and RB_TNT both gate on STATIC_CRATE_TNT), + // but native clears it rather than keep a live garbage pointer around. + mw->driverTarget = 0; + + mw->parentSafetyFrames = MINE_PARENT_SAFETY_FRAMES; + mw->boolDestroyed = 0; + mw->crateInst = 0; + mw->instParent = dInst; + PlaySound3D(SOUND_MINE_DROP, weaponInst); // if human and not AI @@ -935,28 +975,18 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) Voiceline_RequestPlay(VOICELINE_MINE_DROP, data.characterIDs[d->driverID], VOICELINE_WEAPON_PRIORITY); } - mw = weaponTh->object; - mw->driverTarget = 0; - mw->instParent = dInst; - mw->crateInst = 0; - mw->boolDestroyed = 0; - mw->parentSafetyFrames = MINE_PARENT_SAFETY_FRAMES; - mw->flags = 0; - if (modelID == STATIC_BEAKER_RED) - { - mw->flags = MINE_WEAPON_FLAG_RED_BEAKER; - } - struct GamepadBuffer *gb = &sdata->gGamepads->gamepad[d->driverID]; - // throw potion forward + // throw potion forward. Retail ORs the bit into the argument only, it + // never writes it back to the caller's flags. + s32 throwFlags = flags; if ((gb->buttonsHeldCurrFrame & BTN_UP) != 0) { - flags |= POTION_THROW_FORWARD; + throwFlags |= POTION_THROW_FORWARD; } RB_MinePool_Add(mw); - b32 didThrowPotion = VehPickupItem_PotionThrow(mw, weaponInst, flags); + b32 didThrowPotion = VehPickupItem_PotionThrow(mw, weaponInst, throwFlags); if (didThrowPotion == 0) { @@ -966,8 +996,7 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) VehPickupItem_ClearMineMotion(mw); - mineHitModel = weaponInst->model->id; - mineShouldInitFollower = 1; + mineHitModelFlags = 0; goto RunMineCOLL; } break; @@ -988,29 +1017,25 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) weaponTh->funcThDestroy = PROC_DestroyInstance; OtherFX_Play(SOUND_SHIELD, 1); + struct Shield *shieldObj = weaponTh->object; + modelID = DYNAMIC_SHIELD_GREEN; if (d->numWumpas >= DRIVER_WUMPA_JUICED_COUNT) { modelID = DYNAMIC_SHIELD; } - struct Instance *instColor = INSTANCE_Birth3D(gGT->modelPtr[modelID], sdata->s_shield, weaponTh); - - struct Instance *instHighlight = INSTANCE_Birth3D(gGT->modelPtr[DYNAMIC_HIGHLIGHT], highlightName, weaponTh); - - instColor->scale.x = SHIELD_SCALE; - instColor->scale.y = SHIELD_SCALE; - instColor->scale.z = SHIELD_SCALE; + shieldObj->instColor = INSTANCE_Birth3D(gGT->modelPtr[modelID], sdata->s_shield, weaponTh); + shieldObj->instColor->scale.x = SHIELD_SCALE; + shieldObj->instColor->scale.y = SHIELD_SCALE; + shieldObj->instColor->scale.z = SHIELD_SCALE; - instHighlight->scale.x = SHIELD_SCALE; - instHighlight->scale.y = SHIELD_SCALE; - instHighlight->scale.z = SHIELD_SCALE; + shieldObj->instHighlight = INSTANCE_Birth3D(gGT->modelPtr[DYNAMIC_HIGHLIGHT], highlightName, weaponTh); + shieldObj->instHighlight->scale.x = SHIELD_SCALE; + shieldObj->instHighlight->scale.y = SHIELD_SCALE; + shieldObj->instHighlight->scale.z = SHIELD_SCALE; - struct Shield *shieldObj = weaponTh->object; - shieldObj->animFrame = 0; shieldObj->flags = 0; - shieldObj->instColor = instColor; - shieldObj->instHighlight = instHighlight; shieldObj->highlightRot.x = 0; shieldObj->highlightRot.y = SHIELD_HIGHLIGHT_ROT_Y; shieldObj->highlightRot.z = 0; @@ -1026,6 +1051,10 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) } weaponInst->alphaScale = SHIELD_ALPHA_SCALE; + + // Retail clears animFrame last, after the duration/flag branch. + shieldObj->animFrame = 0; + d->instBubbleHold = weaponInst; break; @@ -1082,7 +1111,6 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) // Warpball case WEAPON_ID_WARPBALL: - dInst = d->instSelf; GAMEPAD_ShockFreq(d, WEAPON_GAMEPAD_RUMBLE_FRAMES, 0); GAMEPAD_ShockForce1(d, WEAPON_GAMEPAD_RUMBLE_FRAMES, WEAPON_GAMEPAD_RUMBLE_FORCE); @@ -1116,47 +1144,51 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) Voiceline_RequestPlay(VOICELINE_WARPBALL, data.characterIDs[d->driverID], VOICELINE_WEAPON_PRIORITY); } - // used by RB_Warpball_SeekDriver - victim = 0; - int rank = d->driverRank; - if (rank != 0) - { - victim = gGT->driversInRaceOrder[rank - 1]; - } - tw = weaponTh->object; - tw->flags = TRACKER_FLAG_WARPBALL_FALLBACK_PATH; - tw->soundIDCount = 0; - tw->ptrNodeNext = 0; - tw->pathProgress = 0; - tw->turnAroundFrames = 0; tw->driverParent = d; - tw->driverTarget = victim; - tw->instParent = dInst; + tw->turnAroundFrames = 0; + tw->ptrNodeNext = 0; - if (d->numWumpas >= DRIVER_WUMPA_JUICED_COUNT) + // used by RB_Warpball_SeekDriver + victim = 0; + if (d->driverRank != 0) { - tw->flags |= TRACKER_FLAG_POWERED_UP; + victim = gGT->driversInRaceOrder[d->driverRank - 1]; } + tw->driverTarget = victim; // sets nodeCurrIndex RB_Warpball_SeekDriver(tw, d->checkpoint.currentIndex, d); + // Retail initializes flags and pathProgress AFTER SeekDriver, not before. struct CheckpointNode *cn = gGT->level1->ptr_restart_points; tw->nodeNextIndex = tw->nodeCurrIndex; + tw->flags = 0; + tw->pathProgress = 0; tw->ptrNodeCurr = &cn[tw->nodeCurrIndex]; + if (d->numWumpas >= DRIVER_WUMPA_JUICED_COUNT) + { + tw->flags |= TRACKER_FLAG_POWERED_UP; + } + + tw->flags |= TRACKER_FLAG_WARPBALL_FALLBACK_PATH; + // make this driver invincible tw->driversHit = 1 << d->driverID; - victim = 0; - if (rank != 0) + // Retail re-reads driverRank and driverTarget here rather than reusing + // the values it computed before SeekDriver. + if (d->driverRank != 0) { - victim = RB_Warpball_GetDriverTarget(tw, weaponInst); + tw->driverTarget = RB_Warpball_GetDriverTarget(tw, weaponInst); + } + else + { + tw->driverTarget = 0; } - tw->driverTarget = victim; - if (victim != 0) + if (tw->driverTarget != 0) { RB_Warpball_SetTargetDriver(tw); } @@ -1170,17 +1202,24 @@ void VehPickupItem_ShootNow(struct Driver *d, s32 weaponID, s32 flags) tw->flags &= ~TRACKER_FLAG_WARPBALL_FALLBACK_PATH; } - tw->ptrNodeNext = RB_Warpball_NewPathNode(tw->ptrNodeCurr, victim); + tw->ptrNodeNext = RB_Warpball_NewPathNode(tw->ptrNodeCurr, tw->driverTarget); - tw->vel.y = 0; - tw->rotY = d->angle; - tw->parentSafetyFrames = WARPBALL_PARENT_SAFETY_FRAMES; + tw->soundIDCount = 0; + + dInst = d->instSelf; // do NOT patch for 60fps, // velocity uses elapsedTime + tw->vel.y = 0; tw->vel.x = (dInst->matrix.m[0][2] * WARPBALL_VELOCITY_NUMERATOR) >> WARPBALL_VELOCITY_SHIFT; + tw->parentSafetyFrames = WARPBALL_PARENT_SAFETY_FRAMES; tw->vel.z = (dInst->matrix.m[2][2] * WARPBALL_VELOCITY_NUMERATOR) >> WARPBALL_VELOCITY_SHIFT; + // Retail seeds dir.y (0x1a), not rotY (0x1e): RB_Warpball only ever reads + // dir.y, and it interpolates from the previous value. + tw->dir.y = d->angle; + tw->instParent = dInst; + struct Particle *p = Particle_Init(0, gGT->iconGroup[WARPBALL_PARTICLE_ICON_GROUP], &data.emSet_Warpball[0]); tw->ptrParticle = p; From 432593c929b7ced2dfdb0da023463cf91a88c896 Mon Sep 17 00:00:00 2001 From: penta3 Date: Tue, 28 Jul 2026 01:27:28 -0300 Subject: [PATCH 03/12] fix(226): fix sub quads disappearing --- game/226/226_00_DrawLevelOvr1P.c | 16 ++++++++++++++-- game/MAIN/MainFrame.c | 5 ++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/game/226/226_00_DrawLevelOvr1P.c b/game/226/226_00_DrawLevelOvr1P.c index dd8970af2..825148928 100644 --- a/game/226/226_00_DrawLevelOvr1P.c +++ b/game/226/226_00_DrawLevelOvr1P.c @@ -7701,11 +7701,23 @@ static int Ovr226_800a15c0_EmitFullDynamicExtraFace3(struct PushBuffer *pb, stru static int Ovr226_800a15d4_FullDynamicHelperSlot0(struct PushBuffer *pb, struct PrimMem *primMem, struct QuadBlock *block, struct DrawLevelOvr1PScratchVertex *projected, struct TextureLayout *texture, int depth) { - if (!Ovr226_800a1548_EmitFullDynamicFace1(pb, primMem, block, projected, texture, depth, DRAW_LEVEL_OVR1P_DIRECT_QUAD)) + //Retail 0x800a15d4 does not call the shared Face1/Face2 + // emitters. It inlines its own index sets, closing both sub-quads on corner + // vertex 3 instead of on edge midpoints 7 and 8: + // 0x800a15d4: fp+80,20,120,60 -> {4,1,6,3} == GridMixedFaceIndices[0] + // 0x800a15ec: fp+100,120,40,60 -> {5,6,2,3} == GridMixedFaceIndices[1] + // 0x800a160c: b 0x800a1534 -> {0,4,5,6} == GridFaceIndices[0] + // Face1/Face2 stop at those midpoints, so the two sub-quads came out half + // height and the wedge against corner 3 was emitted by nothing. The + // projected-grid family already uses this Mixed[0]/Mixed[1]/GridFace[0] + // sequence for the same near-mask slot. + if (!DrawLevelOvr1P_EmitFullDynamicTerminalFacePreserveSlot(pb, primMem, block, projected, sDrawLevelOvr1PGridMixedFaceIndices[0], 0, texture, depth, + DRAW_LEVEL_OVR1P_DIRECT_QUAD)) { return 0; } - if (!Ovr226_800a155c_EmitFullDynamicFace2(pb, primMem, block, projected, texture, depth, DRAW_LEVEL_OVR1P_DIRECT_QUAD)) + if (!DrawLevelOvr1P_EmitFullDynamicTerminalFacePreserveSlot(pb, primMem, block, projected, sDrawLevelOvr1PGridMixedFaceIndices[1], 0, texture, depth, + DRAW_LEVEL_OVR1P_DIRECT_QUAD)) { return 0; } diff --git a/game/MAIN/MainFrame.c b/game/MAIN/MainFrame.c index 442ff6ffd..e72d5dbf4 100644 --- a/game/MAIN/MainFrame.c +++ b/game/MAIN/MainFrame.c @@ -616,7 +616,10 @@ static int MainFrame_VisMemHasQuad(const int *visFaceList, const struct QuadBloc { int quadIndex = (int)(quad - mesh->ptrQuadBlockArray); - return (visFaceList[quadIndex >> 5] & (1 << (quadIndex & 0x1f))) != 0; + // Retail builds the mask with `sllv` (0x80035af0), which is defined for every + // count. Plain `1 << 31` would be signed overflow, and bit 31 comes up for + // every 32nd quadblock. + return (visFaceList[quadIndex >> 5] & CTR_MipsSll(1, quadIndex)) != 0; } // NOTE(aalhendi): ASM-verified NTSC-U 926 0x80035684-0x800357b8, unnamed in syms926. From 34e6513ea771b8adc823d16479be8fb46bd9dc48 Mon Sep 17 00:00:00 2001 From: penta3 Date: Tue, 28 Jul 2026 04:34:37 -0300 Subject: [PATCH 04/12] fix(gte): consistency with hardware --- include/psx/inline_c.h | 12 +-- platform/native_gte_core.c | 178 ++++++++++++++++++++----------------- platform/native_libgte.c | 53 +++++++---- 3 files changed, 137 insertions(+), 106 deletions(-) diff --git a/include/psx/inline_c.h b/include/psx/inline_c.h index 138ef6b5b..4e5d96818 100644 --- a/include/psx/inline_c.h +++ b/include/psx/inline_c.h @@ -188,7 +188,7 @@ extern int doCOP2(int op); { \ CTC2(*(uint32_t *)((char *)(r0)), 0); \ CTC2(*(uint32_t *)((char *)(r0) + 4), 2); \ - CTC2(*(uint32_t*)((char*)(r0 +8), 4); \ + CTC2(*(uint32_t *)((char *)(r0) + 8), 4); \ } // lwc2 9,10,11 @@ -206,10 +206,10 @@ extern int doCOP2(int op); } // ctc2 24,25 -#define gte_SetGeomOffset(r0, r1) \ - { \ - CTC2(r0 << 16, 24); \ - CTC2(r1 << 16, 25); \ +#define gte_SetGeomOffset(r0, r1) \ + { \ + CTC2((u32)CTR_MipsSll((r0), 16), 24); \ + CTC2((u32)CTR_MipsSll((r1), 16), 25); \ } // ctc2 13,14,15 @@ -346,7 +346,7 @@ extern int doCOP2(int op); #define gte_rtv2tr() doCOP2(0x0490012); -#define gte_rtirtr() op2 0x0498012); +#define gte_rtirtr() doCOP2(0x0498012); #define gte_rtv0bk() doCOP2(0x0482012); diff --git a/platform/native_gte_core.c b/platform/native_gte_core.c index 31571b96a..6aa71e084 100644 --- a/platform/native_gte_core.c +++ b/platform/native_gte_core.c @@ -19,10 +19,15 @@ GTERegisters gteRegs; #define gteop(code) (code & 0x1ffffff) +/* FLAG masks. Written as `1 << 31` these overflowed a signed int, which is + * undefined behaviour; the cast makes every bit position well defined. FLAG + * itself (CP2C.p[31].d) and LIM()'s flag parameter are unsigned already. */ +#define GTE_FLAG(bit) ((u32)1 << (bit)) + #define VX(n) (n < 3 ? gteRegs.CP2D.p[n << 1].sw.l : C2_IR1) #define VY(n) (n < 3 ? gteRegs.CP2D.p[n << 1].sw.h : C2_IR2) #define VZ(n) (n < 3 ? gteRegs.CP2D.p[(n << 1) + 1].sw.l : C2_IR3) -#define MX11(n) (n < 3 ? gteRegs.CP2C.p[(n << 3)].sw.l : -C2_R << 4) +#define MX11(n) (n < 3 ? gteRegs.CP2C.p[(n << 3)].sw.l : -(C2_R << 4)) #define MX12(n) (n < 3 ? gteRegs.CP2C.p[(n << 3)].sw.h : C2_R << 4) #define MX13(n) (n < 3 ? gteRegs.CP2C.p[(n << 3) + 1].sw.l : C2_IR0) #define MX21(n) (n < 3 ? gteRegs.CP2C.p[(n << 3) + 1].sw.h : C2_R13) @@ -84,20 +89,24 @@ internal inline s64 gte_shift(s64 a, int sf) } else if (sf < 0) { - return a << 12; + return a * 4096; } return a; } -internal int BOUNDS(/*int44*/ s64 value, int max_flag, int min_flag) +internal int BOUNDS(/*int44*/ s64 value, u32 max_flag, u32 min_flag) { if (value /*.positive_overflow()*/ > (s64)0x7ffffffffff) { C2_FLAG |= max_flag; } - if (value /*.negative_overflow()*/ < (s64)-0x8000000000) + /* MAC1..3 are 44-bit accumulators, so the negative bound is -2^43. The + * literal was one hex digit short (-2^39), which set the MAC negative + * overflow bits 27/26/25 - and with them the error bit 31 - sixteen + * times more eagerly than hardware does. */ + if (value /*.negative_overflow()*/ < (s64)-0x80000000000) { C2_FLAG |= min_flag; } @@ -139,28 +148,28 @@ internal u32 gte_divide(u16 numerator, u16 denominator) internal int A1(/*int44*/ s64 a) { - return BOUNDS(a, (1 << 31) | (1 << 30), (1 << 31) | (1 << 27)); + return BOUNDS(a, GTE_FLAG(31) | GTE_FLAG(30), GTE_FLAG(31) | GTE_FLAG(27)); } internal int A2(/*int44*/ s64 a) { - return BOUNDS(a, (1 << 31) | (1 << 29), (1 << 31) | (1 << 26)); + return BOUNDS(a, GTE_FLAG(31) | GTE_FLAG(29), GTE_FLAG(31) | GTE_FLAG(26)); } internal int A3(/*int44*/ s64 a) { m_mac3 = a; - return BOUNDS(a, (1 << 31) | (1 << 28), (1 << 31) | (1 << 25)); + return BOUNDS(a, GTE_FLAG(31) | GTE_FLAG(28), GTE_FLAG(31) | GTE_FLAG(25)); } internal int Lm_B1(int a, int lm) { - return LIM(a, 0x7fff, -0x8000 * !lm, (1 << 31) | (1 << 24)); + return LIM(a, 0x7fff, -0x8000 * !lm, GTE_FLAG(31) | GTE_FLAG(24)); } internal int Lm_B2(int a, int lm) { - return LIM(a, 0x7fff, -0x8000 * !lm, (1 << 31) | (1 << 23)); + return LIM(a, 0x7fff, -0x8000 * !lm, GTE_FLAG(31) | GTE_FLAG(23)); } internal int Lm_B3(int a, int lm) { - return LIM(a, 0x7fff, -0x8000 * !lm, (1 << 22)); + return LIM(a, 0x7fff, -0x8000 * !lm, GTE_FLAG(22)); } internal int Lm_B3_sf(s64 value, int sf, int lm) @@ -176,7 +185,7 @@ internal int Lm_B3_sf(s64 value, int sf, int lm) if (value_12 < -0x8000 || value_12 > 0x7fff) { - C2_FLAG |= (1 << 22); + C2_FLAG |= GTE_FLAG(22); } if (value_sf > max) @@ -193,26 +202,26 @@ internal int Lm_B3_sf(s64 value, int sf, int lm) internal int Lm_C1(int a) { - return LIM(a, 0x00ff, 0x0000, (1 << 21)); + return LIM(a, 0x00ff, 0x0000, GTE_FLAG(21)); } internal int Lm_C2(int a) { - return LIM(a, 0x00ff, 0x0000, (1 << 20)); + return LIM(a, 0x00ff, 0x0000, GTE_FLAG(20)); } internal int Lm_C3(int a) { - return LIM(a, 0x00ff, 0x0000, (1 << 19)); + return LIM(a, 0x00ff, 0x0000, GTE_FLAG(19)); } internal int Lm_D(s64 a, int sf) { - return LIM((int)(gte_shift(a, sf)), 0xffff, 0x0000, (1 << 31) | (1 << 18)); + return LIM((int)(gte_shift(a, sf)), 0xffff, 0x0000, GTE_FLAG(31) | GTE_FLAG(18)); } internal u32 Lm_E(u32 result) { if (result == 0xffffffff) { - C2_FLAG |= (1 << 31) | (1 << 17); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(17); return 0x1ffff; } @@ -226,31 +235,36 @@ internal u32 Lm_E(u32 result) internal s64 F(s64 a) { - m_mac0 = a; - + // Overflow is detected on the full-width sum... if (a > 0x7fffffffLL) { - C2_FLAG |= (1 << 31) | (1 << 16); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(16); } if (a < -0x80000000LL) { - C2_FLAG |= (1 << 31) | (1 << 15); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(15); } - return a; + // ...but MAC0 is a 32-bit register, so that is what gets stored and what + // every consumer reads back (SX2/SY2 via SAR 16, IR0 via Lm_H, OTZ via + // Lm_D). Keeping the untruncated 64-bit sum here sent SX2/SY2 to the + // opposite screen edge whenever IR*(H/SZ) overflowed 32 bits. + m_mac0 = (s32)a; + + return m_mac0; } internal int Lm_G1(s64 a) { if (a > 0x3ff) { - C2_FLAG |= (1 << 31) | (1 << 14); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(14); return 0x3ff; } if (a < -0x400) { - C2_FLAG |= (1 << 31) | (1 << 14); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(14); return -0x400; } @@ -261,13 +275,13 @@ internal int Lm_G2(s64 a) { if (a > 0x3ff) { - C2_FLAG |= (1 << 31) | (1 << 13); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(13); return 0x3ff; } if (a < -0x400) { - C2_FLAG |= (1 << 31) | (1 << 13); + C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(13); return -0x400; } @@ -283,7 +297,7 @@ internal int Lm_H(s64 value, int sf) if (value_sf < min || value_sf > max) { - C2_FLAG |= (1 << 12); + C2_FLAG |= GTE_FLAG(12); } if (value_12 > max) @@ -303,9 +317,9 @@ internal int GTE_RotTransPers(int idx, int lm) { int h_over_sz3; - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_TRX << 12) + (C2_R11 * VX(idx)) + (C2_R12 * VY(idx)) + (C2_R13 * VZ(idx))); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_TRY << 12) + (C2_R21 * VX(idx)) + (C2_R22 * VY(idx)) + (C2_R23 * VZ(idx))); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_TRZ << 12) + (C2_R31 * VX(idx)) + (C2_R32 * VY(idx)) + (C2_R33 * VZ(idx))); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_TRX * 4096) + (C2_R11 * VX(idx)) + (C2_R12 * VY(idx)) + (C2_R13 * VZ(idx))); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_TRY * 4096) + (C2_R21 * VX(idx)) + (C2_R22 * VY(idx)) + (C2_R23 * VZ(idx))); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_TRZ * 4096) + (C2_R31 * VX(idx)) + (C2_R32 * VY(idx)) + (C2_R33 * VZ(idx))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3_sf(m_mac3, m_sf, lm); @@ -347,8 +361,10 @@ int GTE_operator(int op) return 1; case 0x06: + /* NCLIP writes MAC0, so it can legitimately raise the MAC0 overflow + * bits 16/15 (and 31). Clearing FLAG here discarded them; hardware + * only clears FLAG at the start of a command, which is done above. */ C2_MAC0 = (int)(F((s64)(C2_SX0 * C2_SY1) + (C2_SX1 * C2_SY2) + (C2_SX2 * C2_SY0) - (C2_SX0 * C2_SY2) - (C2_SX1 * C2_SY0) - (C2_SX2 * C2_SY1))); - C2_FLAG = 0; return 1; case 0x0c: @@ -363,9 +379,9 @@ int GTE_operator(int op) case 0x10: - C2_MAC1 = A1((C2_R << 16) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - (C2_R << 16)), 0))); - C2_MAC2 = A2((C2_G << 16) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - (C2_G << 16)), 0))); - C2_MAC3 = A3((C2_B << 16) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - (C2_B << 16)), 0))); + C2_MAC1 = A1((C2_R << 16) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - (C2_R << 16)), 0))); + C2_MAC2 = A2((C2_G << 16) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - (C2_G << 16)), 0))); + C2_MAC3 = A3((C2_B << 16) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - (C2_B << 16)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -379,9 +395,9 @@ int GTE_operator(int op) case 0x11: - C2_MAC1 = A1((C2_IR1 << 12) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - (C2_IR1 << 12)), 0))); - C2_MAC2 = A2((C2_IR2 << 12) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - (C2_IR2 << 12)), 0))); - C2_MAC3 = A3((C2_IR3 << 12) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - (C2_IR3 << 12)), 0))); + C2_MAC1 = A1((C2_IR1 * 4096) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - (C2_IR1 * 4096)), 0))); + C2_MAC2 = A2((C2_IR2 * 4096) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - (C2_IR2 * 4096)), 0))); + C2_MAC3 = A3((C2_IR3 * 4096) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - (C2_IR3 * 4096)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -405,15 +421,15 @@ int GTE_operator(int op) C2_MAC1 = A1((s64)(MX12(mx) * VY(v)) + (MX13(mx) * VZ(v))); C2_MAC2 = A2((s64)(MX22(mx) * VY(v)) + (MX23(mx) * VZ(v))); C2_MAC3 = A3((s64)(MX32(mx) * VY(v)) + (MX33(mx) * VZ(v))); - Lm_B1(A1(((s64)CV1(cv) << 12) + (MX11(mx) * VX(v))), 0); - Lm_B2(A2(((s64)CV2(cv) << 12) + (MX21(mx) * VX(v))), 0); - Lm_B3(A3(((s64)CV3(cv) << 12) + (MX31(mx) * VX(v))), 0); + Lm_B1(A1(((s64)CV1(cv) * 4096) + (MX11(mx) * VX(v))), 0); + Lm_B2(A2(((s64)CV2(cv) * 4096) + (MX21(mx) * VX(v))), 0); + Lm_B3(A3(((s64)CV3(cv) * 4096) + (MX31(mx) * VX(v))), 0); break; default: - C2_MAC1 = A1(/*int44*/ (s64)((s64)CV1(cv) << 12) + (MX11(mx) * VX(v)) + (MX12(mx) * VY(v)) + (MX13(mx) * VZ(v))); - C2_MAC2 = A2(/*int44*/ (s64)((s64)CV2(cv) << 12) + (MX21(mx) * VX(v)) + (MX22(mx) * VY(v)) + (MX23(mx) * VZ(v))); - C2_MAC3 = A3(/*int44*/ (s64)((s64)CV3(cv) << 12) + (MX31(mx) * VX(v)) + (MX32(mx) * VY(v)) + (MX33(mx) * VZ(v))); + C2_MAC1 = A1(/*int44*/ (s64)((s64)CV1(cv) * 4096) + (MX11(mx) * VX(v)) + (MX12(mx) * VY(v)) + (MX13(mx) * VZ(v))); + C2_MAC2 = A2(/*int44*/ (s64)((s64)CV2(cv) * 4096) + (MX21(mx) * VX(v)) + (MX22(mx) * VY(v)) + (MX23(mx) * VZ(v))); + C2_MAC3 = A3(/*int44*/ (s64)((s64)CV3(cv) * 4096) + (MX31(mx) * VX(v)) + (MX32(mx) * VY(v)) + (MX33(mx) * VZ(v))); break; } @@ -430,15 +446,15 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - ((C2_R << 4) * C2_IR1)), 0))); - C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - ((C2_G << 4) * C2_IR2)), 0))); - C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - ((C2_B << 4) * C2_IR3)), 0))); + C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - ((C2_R << 4) * C2_IR1)), 0))); + C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - ((C2_G << 4) * C2_IR2)), 0))); + C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - ((C2_B << 4) * C2_IR3)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -452,15 +468,15 @@ int GTE_operator(int op) case 0x14: - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - ((C2_R << 4) * C2_IR1)), 0))); - C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - ((C2_G << 4) * C2_IR2)), 0))); - C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - ((C2_B << 4) * C2_IR3)), 0))); + C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - ((C2_R << 4) * C2_IR1)), 0))); + C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - ((C2_G << 4) * C2_IR2)), 0))); + C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - ((C2_B << 4) * C2_IR3)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -482,15 +498,15 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - ((C2_R << 4) * C2_IR1)), 0))); - C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - ((C2_G << 4) * C2_IR2)), 0))); - C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - ((C2_B << 4) * C2_IR3)), 0))); + C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - ((C2_R << 4) * C2_IR1)), 0))); + C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - ((C2_G << 4) * C2_IR2)), 0))); + C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - ((C2_B << 4) * C2_IR3)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -511,9 +527,9 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -533,9 +549,9 @@ int GTE_operator(int op) case 0x1c: - C2_MAC1 = A1(/*int44*/ (s64)(((s64)C2_RBK) << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)(((s64)C2_GBK) << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)(((s64)C2_BBK) << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)(((s64)C2_RBK) * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)(((s64)C2_GBK) * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)(((s64)C2_BBK) * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -561,9 +577,9 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -585,9 +601,9 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -612,9 +628,9 @@ int GTE_operator(int op) case 0x29: - C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - ((C2_R << 4) * C2_IR1)), 0))); - C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - ((C2_G << 4) * C2_IR2)), 0))); - C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - ((C2_B << 4) * C2_IR3)), 0))); + C2_MAC1 = A1(((C2_R << 4) * C2_IR1) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - ((C2_R << 4) * C2_IR1)), 0))); + C2_MAC2 = A2(((C2_G << 4) * C2_IR2) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - ((C2_G << 4) * C2_IR2)), 0))); + C2_MAC3 = A3(((C2_B << 4) * C2_IR3) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - ((C2_B << 4) * C2_IR3)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -630,9 +646,9 @@ int GTE_operator(int op) for (v = 0; v < 3; v++) { - C2_MAC1 = A1((C2_R0 << 16) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC << 12) - (C2_R0 << 16)), 0))); - C2_MAC2 = A2((C2_G0 << 16) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC << 12) - (C2_G0 << 16)), 0))); - C2_MAC3 = A3((C2_B0 << 16) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC << 12) - (C2_B0 << 16)), 0))); + C2_MAC1 = A1((C2_R0 << 16) + (C2_IR0 * Lm_B1(A1(((s64)C2_RFC * 4096) - (C2_R0 << 16)), 0))); + C2_MAC2 = A2((C2_G0 << 16) + (C2_IR0 * Lm_B2(A2(((s64)C2_GFC * 4096) - (C2_G0 << 16)), 0))); + C2_MAC3 = A3((C2_B0 << 16) + (C2_IR0 * Lm_B3(A3(((s64)C2_BFC * 4096) - (C2_B0 << 16)), 0))); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); @@ -710,9 +726,9 @@ int GTE_operator(int op) C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); - C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK << 12) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); - C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK << 12) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); - C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK << 12) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); + C2_MAC1 = A1(/*int44*/ (s64)((s64)C2_RBK * 4096) + (C2_LR1 * C2_IR1) + (C2_LR2 * C2_IR2) + (C2_LR3 * C2_IR3)); + C2_MAC2 = A2(/*int44*/ (s64)((s64)C2_GBK * 4096) + (C2_LG1 * C2_IR1) + (C2_LG2 * C2_IR2) + (C2_LG3 * C2_IR3)); + C2_MAC3 = A3(/*int44*/ (s64)((s64)C2_BBK * 4096) + (C2_LB1 * C2_IR1) + (C2_LB2 * C2_IR2) + (C2_LB3 * C2_IR3)); C2_IR1 = Lm_B1(C2_MAC1, lm); C2_IR2 = Lm_B2(C2_MAC2, lm); C2_IR3 = Lm_B3(C2_MAC3, lm); diff --git a/platform/native_libgte.c b/platform/native_libgte.c index 4540a8019..99adab907 100644 --- a/platform/native_libgte.c +++ b/platform/native_libgte.c @@ -48,24 +48,30 @@ internal inline s32 fst_max(s32 a, s32 b) void InitGeom() { - C2_ZSF3 = 341; - C2_ZSF4 = 256; - C2_H = 1000; - C2_DQA = -98; - C2_DQB = 340; - C2_OFX = 0; - C2_OFY = 0; + /* Route through CTC2 like the hardware's ctc2 does. Assigning the C2_* + * macros directly only touches the low half of the 16-bit control + * registers, so DQA ended up readable as 0x0000FF9E where hardware + * sign-extends it to 0xFFFFFF9E. Nothing reads those upper halves back + * today, but SetDQA/SetDQB already use CTC2 - this makes the two agree. */ + CTC2((u32)341, 29); /* ZSF3 */ + CTC2((u32)256, 30); /* ZSF4 */ + CTC2((u32)1000, 26); /* H */ + CTC2((u32)(s32)-98, 27); /* DQA */ + CTC2((u32)340, 28); /* DQB */ + CTC2(0, 24); /* OFX */ + CTC2(0, 25); /* OFY */ } void SetGeomOffset(int ofx, int ofy) { - C2_OFX = (ofx << 16); - C2_OFY = (ofy << 16); + /* `ofx << 16` is undefined in C for negative offsets; sll is not. */ + CTC2((u32)CTR_MipsSll(ofx, 16), 24); + CTC2((u32)CTR_MipsSll(ofy, 16), 25); } void SetGeomScreen(int h) { - C2_H = h; + CTC2((u32)h, 26); } void SetRotMatrix(MATRIX *m) @@ -860,7 +866,9 @@ void SetFogNear(int a, int h) { // Error division by 0 assert(h != 0); - int depthQ = -(((a << 2) + a) << 6); + /* Shifts and negation in MIPS form: on R3000 these wrap, in C they are + * undefined for negative / overflowing operands. */ + int depthQ = CTR_MipsNegLo(CTR_MipsSll(CTR_MipsAddLo(CTR_MipsSll(a, 2), a), 6)); assert(h != -1 && depthQ != 0x8000); SetDQA(depthQ / h); SetDQB(20971520); @@ -880,10 +888,12 @@ void SetFogNearFar(int a, int b, int h) assert(h != 0); assert(h != -1 && (((-a * b) / (b - a)) << 8) != 32768); - int dqa = (-a * b / (b - a) << 8) / h; + /* Same rewrite as SetFogNear. `-a * b` is a negu followed by mult/mflo on + * hardware; both wrap there and are undefined in C. */ + int dqa = CTR_MipsSll(CTR_MipsMulLo(CTR_MipsNegLo(a), b) / (b - a), 8) / h; SetDQA(MAX(MIN(dqa, 32767), -32767)); - SetDQB((b << 12) / (b - a) << 12); + SetDQB(CTR_MipsSll(CTR_MipsSll(b, 12) / (b - a), 12)); } int rsin(int a) @@ -920,25 +930,30 @@ int ratan2(int y, int x) return 0; } + /* MIPS semantics throughout: `-INT_MIN` and `INT_MIN << 10` are undefined + * in C but perfectly defined on R3000 (both wrap). The guard below tests + * bits 21..30 only, so INT_MIN slips past it and would reach the shift; + * the Mips helpers reproduce exactly what the hardware yields (0), instead + * of leaving the optimiser free to do something else. */ if (x < 0) { - x = -x; + x = CTR_MipsNegLo(x); } if (y < 0) { - y = -y; + y = CTR_MipsNegLo(y); } if (y < x) { if (((u32)y & 0x7fe00000U) == 0) { - ang = (y << 10) / x; + ang = (u32)(CTR_MipsSll(y, 10) / x); } else { - ang = y / (x >> 10); + ang = (u32)(y / CTR_MipsSra(x, 10)); } v = ratan_tbl[ang]; @@ -947,11 +962,11 @@ int ratan2(int y, int x) { if (((u32)x & 0x7fe00000U) == 0) { - ang = (x << 10) / y; + ang = (u32)(CTR_MipsSll(x, 10) / y); } else { - ang = x / (y >> 10); + ang = (u32)(x / CTR_MipsSra(y, 10)); } v = 1024 - ratan_tbl[ang]; From 82a96c279d6ad2e5264b60b6d296e5ede82f0e51 Mon Sep 17 00:00:00 2001 From: penta3 Date: Tue, 28 Jul 2026 20:03:04 -0300 Subject: [PATCH 05/12] fix(gte): consistency with psyq 4.4 --- include/psx/inline_c.h | 16 +++++++++++----- platform/native_libgte.c | 13 ++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/include/psx/inline_c.h b/include/psx/inline_c.h index 4e5d96818..6011e3a13 100644 --- a/include/psx/inline_c.h +++ b/include/psx/inline_c.h @@ -129,11 +129,17 @@ extern int doCOP2(int op); MTC2(*(uint32_t *)((char *)(r2)), 6); \ } -// mtc2 12, lwc2 1 -#define gte_ldlv0(r0) \ - { \ - MTC2((*(uint16_t *)((char *)(r0) + 4) << 16) | *(uint16_t *)((char *)(r0)), 12); \ - MTC2(*(uint16_t *)((char *)(r0) + 8), 1); \ +// mtc2 0, lwc2 1 +// Psy-Q 4.4: lhu $13,4(r0); lhu $12,0(r0); sll $13,$13,16; or $12,$12,$13; +// mtc2 $12,$0; lwc2 $1,8(r0) +// The destination was register 12 (SXY0) instead of 0 (VXY0), so V0 was never +// loaded and the screen-XY FIFO was clobbered instead. The pack also did +// `(uint16_t)x << 16`, which promotes to int and overflows it for x >= 0x8000; +// CTR_PackS16Pair does the same thing in u32, where it is defined. +#define gte_ldlv0(r0) \ + { \ + MTC2(CTR_PackS16Pair(*(uint16_t *)((char *)(r0) + 0), *(uint16_t *)((char *)(r0) + 4)), 0); \ + MTC2(CTR_ReadU32LE((char *)(r0) + 8), 1); \ } // mtc2 8 diff --git a/platform/native_libgte.c b/platform/native_libgte.c index 99adab907..94b073dd6 100644 --- a/platform/native_libgte.c +++ b/platform/native_libgte.c @@ -999,14 +999,21 @@ int SquareRoot0(int a) lzcs &= 0xfffffffe; + /* Psy-Q 4.4 emits srav / sllv here, and a LOGICAL srl for the final >>12. + * In C `a << n` is undefined for negative `a`, and `>>` on an int is + * arithmetic rather than logical. Over the valid domain (a >= 0) both agree + * - SQRT[] holds no entry >= 0x8000, so the intermediate is never negative + * - but the helpers reproduce the exact instruction out of domain too, and + * above all they stop the optimiser from exploiting the UB on the valid + * path. */ if ((lzcs - 24) < 0) { - idx = a >> (24 - lzcs); + idx = CTR_MipsSra(a, (u32)(24 - lzcs)); } else { - idx = a << (lzcs - 24); + idx = CTR_MipsSll(a, (u32)(lzcs - 24)); } - return SQRT[idx - 64] << ((31 - lzcs) >> 1) >> 12; + return (s32)CTR_MipsSrl(CTR_MipsSll(SQRT[idx - 64], (u32)((31 - lzcs) >> 1)), 12); } From 493b06d8f82a81efa9eed51c2edce7fbcb348ac6 Mon Sep 17 00:00:00 2001 From: penta3 Date: Tue, 28 Jul 2026 21:43:59 -0300 Subject: [PATCH 06/12] fix(Vehicle): VehEmitter and VehGround clean up --- game/Vehicle/VehEmitter.c | 88 +++++++++++++++++++++------------- game/Vehicle/VehGroundShadow.c | 9 ++-- game/Vehicle/VehGroundSkids.c | 3 +- 3 files changed, 61 insertions(+), 39 deletions(-) diff --git a/game/Vehicle/VehEmitter.c b/game/Vehicle/VehEmitter.c index 7f4dddb3a..84d54602d 100644 --- a/game/Vehicle/VehEmitter.c +++ b/game/Vehicle/VehEmitter.c @@ -5,6 +5,8 @@ enum VEH_EMITTER_AXIS_COUNT = 3, VEH_EMITTER_EXHAUST_ICON_LOW = 1, VEH_EMITTER_EXHAUST_ICON_WATER = 7, + VEH_EMITTER_EXHAUST_MED_LOD_PLAYERS = 2, + VEH_EMITTER_EXHAUST_LOW_LOD_MIN_PLAYERS = 3, VEH_EMITTER_EXHAUST_WATER_Y_LIMIT = FP8_ONE, VEH_EMITTER_EXHAUST_VEL_Y = 0x400, VEH_EMITTER_EXHAUST_VEL_Z = -0x400, @@ -70,7 +72,6 @@ enum VEH_EMITTER_WALL_SPARK_Y = 0x0a00, VEH_EMITTER_WALL_SPARK_REVERSE_Z = -0x1400, VEH_EMITTER_WALL_SPARK_FORWARD_Z = 0x2800, - VEH_EMITTER_WALL_SPARK_SCRATCH_HALF_COUNT = 6, VEH_EMITTER_ALPHA_FULL = 0x1000, VEH_EMITTER_JOG_GROUND = 0x27, VEH_EMITTER_JOG_WOBBLE_ALT = 0xf0, @@ -85,6 +86,8 @@ enum CTR_STATIC_ASSERT(VEH_EMITTER_AXIS_COUNT == 3); CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_ICON_LOW == 1); CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_ICON_WATER == 7); +CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_MED_LOD_PLAYERS == 2); +CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_LOW_LOD_MIN_PLAYERS == 3); CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_WATER_Y_LIMIT == 0x100); CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_VEL_Y == 0x400); CTR_STATIC_ASSERT(VEH_EMITTER_EXHAUST_VEL_Z == -0x400); @@ -150,7 +153,6 @@ CTR_STATIC_ASSERT(VEH_EMITTER_WALL_SPARK_RIGHT_X == 0x2200); CTR_STATIC_ASSERT(VEH_EMITTER_WALL_SPARK_Y == 0x0a00); CTR_STATIC_ASSERT(VEH_EMITTER_WALL_SPARK_REVERSE_Z == -0x1400); CTR_STATIC_ASSERT(VEH_EMITTER_WALL_SPARK_FORWARD_Z == 0x2800); -CTR_STATIC_ASSERT(VEH_EMITTER_WALL_SPARK_SCRATCH_HALF_COUNT == 6); CTR_STATIC_ASSERT(VEH_EMITTER_ALPHA_FULL == 0x1000); CTR_STATIC_ASSERT(VEH_EMITTER_JOG_GROUND == 0x27); CTR_STATIC_ASSERT(VEH_EMITTER_JOG_WOBBLE_ALT == 0xf0); @@ -177,28 +179,35 @@ struct Particle *VehEmitter_Exhaust(struct Driver *d, VECTOR *exhaustPos, VECTOR return 0; } - // low LOD exhaust (4p or ai car) int exhaustType = VEH_EMITTER_EXHAUST_ICON_LOW; - struct ParticleEmitter *emSet = &data.emSet_Exhaust_Low[0]; + struct ParticleEmitter *emSet; u8 numPlyr = gGT->numPlyrCurrGame; - // equivalent of (d->driverID < numPlyr), + // Retail picks the LOD by falling through to the high set, so every player count below 2 + // (including 0) lands on the high set before the robot-car override below. + if (numPlyr >= VEH_EMITTER_EXHAUST_LOW_LOD_MIN_PLAYERS) + { + // 3P/4P mode, low LOD exhaust + emSet = &data.emSet_Exhaust_Low[0]; + } + else if (numPlyr == VEH_EMITTER_EXHAUST_MED_LOD_PLAYERS) + { + // 2P mode, med LOD exhaust + emSet = &data.emSet_Exhaust_Med[0]; + } + else + { + // 1P mode, high LOD exhaust + emSet = &data.emSet_Exhaust_High[0]; + } + + // low LOD exhaust for ai cars. equivalent of (d->driverID >= numPlyr), // because modelIndex is not set to DYNAMIC_ROBOT_CAR // for human players after BOTS_Driver_Convert is called - if (dInst->thread->modelIndex != DYNAMIC_ROBOT_CAR) + if (dInst->thread->modelIndex == DYNAMIC_ROBOT_CAR) { - switch (numPlyr) - { - case 1: - // 1P mode, high LOD exhaust - emSet = &data.emSet_Exhaust_High[0]; - break; - case 2: - // 2P mode, med LOD exhaust - emSet = &data.emSet_Exhaust_Med[0]; - break; - } + emSet = &data.emSet_Exhaust_Low[0]; } if (((dInst->flags & SPLIT_LINE) != 0) && ((exhaustPos->vy - exhaustVel->vy) + d->posCurr.y < VEH_EMITTER_EXHAUST_WATER_Y_LIMIT)) @@ -421,16 +430,26 @@ void VehEmitter_Sparks_Wall(struct Driver *d, struct ParticleEmitter *emSet) } union VehEmitterWallScratch *scratch = CTR_SCRATCHPAD_PTR(union VehEmitterWallScratch, 0); - s32 *tireLeftOutWord = &scratch->word[0]; - s32 *tireRightOutWord = &scratch->word[3]; + s32 *tireLeftInWord = &scratch->word[0]; + s32 *tireRightInWord = &scratch->word[3]; s16 *tireLeftOutHalf = &scratch->half[0]; s16 *tireRightOutHalf = &scratch->half[3]; + // tireRightInWord starts at half[6], not at half[3]: half[3] is where the packed right-tire + // result lands below, where it doubles as rows 4-6 of the 3x2 light matrix. Reading the + // right-tire GTE input from half[3] would feed back the tail of the left-tire result. + s16 *tireLeftInHalf = &scratch->half[0]; + s16 *tireRightInHalf = &scratch->half[6]; s16 *distIn4 = &scratch->half[6]; - s32 *distOut4 = &scratch->word[3]; + // Retail keeps both untruncated rotated triplets in registers across the packing below and + // adds those to the particle start position; only the packed copies feed the light matrix. + s32 tireLeftRotated[VEH_EMITTER_AXIS_COUNT]; + s32 tireRightRotated[VEH_EMITTER_AXIS_COUNT]; + s32 *tireRotated = tireLeftRotated; + s32 distOut4[VEH_EMITTER_AXIS_COUNT]; // s16[3] array - tireLeftOutWord[0] = (s32)CTR_PackS16Pair(VEH_EMITTER_WALL_SPARK_LEFT_X, VEH_EMITTER_WALL_SPARK_Y); - tireRightOutWord[0] = (s32)CTR_PackS16Pair(VEH_EMITTER_WALL_SPARK_RIGHT_X, VEH_EMITTER_WALL_SPARK_Y); + tireLeftInWord[0] = (s32)CTR_PackS16Pair(VEH_EMITTER_WALL_SPARK_LEFT_X, VEH_EMITTER_WALL_SPARK_Y); + tireRightInWord[0] = (s32)CTR_PackS16Pair(VEH_EMITTER_WALL_SPARK_RIGHT_X, VEH_EMITTER_WALL_SPARK_Y); int valZ = VEH_EMITTER_WALL_SPARK_REVERSE_Z; if (d->speedApprox > 0) @@ -439,22 +458,23 @@ void VehEmitter_Sparks_Wall(struct Driver *d, struct ParticleEmitter *emSet) } // s16[3] array - tireLeftOutWord[1] = valZ; - tireRightOutWord[1] = valZ; + tireLeftInWord[1] = valZ; + tireRightInWord[1] = valZ; - CTR_GteLoadS16TripletV0(tireLeftOutHalf); + CTR_GteLoadS16TripletV0(tireLeftInHalf); gte_rtv0(); - CTR_GteStoreMAC(&tireLeftOutWord[0]); + CTR_GteStoreMAC(tireLeftRotated); - CTR_GteLoadS16TripletV0(tireRightOutHalf); + CTR_GteLoadS16TripletV0(tireRightInHalf); gte_rtv0(); - CTR_GteStoreMAC(&tireRightOutWord[0]); + CTR_GteStoreMAC(tireRightRotated); // this compresses TireLeft and TireRight from int to s16, // which then doubles in usage as a matrix (3x2) - for (int i = 0; i < VEH_EMITTER_WALL_SPARK_SCRATCH_HALF_COUNT; i++) + for (int i = 0; i < VEH_EMITTER_AXIS_COUNT; i++) { - tireLeftOutHalf[i] = (u16)scratch->word[i]; + tireLeftOutHalf[i] = (s16)tireLeftRotated[i]; + tireRightOutHalf[i] = (s16)tireRightRotated[i]; } #ifdef CTR_NATIVE @@ -490,10 +510,10 @@ void VehEmitter_Sparks_Wall(struct Driver *d, struct ParticleEmitter *emSet) CTR_GteLoadS16TripletV0(&distIn4[0]); gte_llv0(); - CTR_GteStoreMAC(&distOut4[0]); + CTR_GteStoreMAC(distOut4); if (distOut4[0] < distOut4[1]) { - tireLeftOutHalf = tireRightOutHalf; + tireRotated = tireRightRotated; } // Create instance in particle pool @@ -506,14 +526,14 @@ void VehEmitter_Sparks_Wall(struct Driver *d, struct ParticleEmitter *emSet) for (int i = 0; i < VEH_EMITTER_AXIS_COUNT; i++) { - p->axis[i].startVal += tireLeftOutHalf[i]; + p->axis[i].startVal += tireRotated[i]; distIn4[i] = p->axis[i].velocity; } // dist4 now determines velocity CTR_GteLoadS16TripletV0(&distIn4[0]); gte_rtv0(); - CTR_GteStoreMAC(&distOut4[0]); + CTR_GteStoreMAC(distOut4); p->axis[0].velocity = (s16)distOut4[0]; p->axis[1].velocity = (s16)distOut4[1]; diff --git a/game/Vehicle/VehGroundShadow.c b/game/Vehicle/VehGroundShadow.c index b22ce0dd2..8449e6918 100644 --- a/game/Vehicle/VehGroundShadow.c +++ b/game/Vehicle/VehGroundShadow.c @@ -27,7 +27,7 @@ enum VEH_GROUND_SHADOW_LOCAL_SCALE_SHIFT = 6, VEH_GROUND_SHADOW_PRIM_GUARD_WORDS = 0x140, - VEH_GROUND_SHADOW_GTE_SCREEN_SHIFT = 15, + VEH_GROUND_SHADOW_GEOM_OFFSET_SHIFT = 1, VEH_GROUND_SHADOW_LARGE_GEOM_SCREEN_THRESHOLD = 0x100, VEH_GROUND_SHADOW_CAMERA_DELTA_SCALE = 4, VEH_GROUND_SHADOW_SMALL_SCREEN_MAX_EXCLUSIVE = 0x1771, @@ -405,9 +405,10 @@ void VehGroundShadow_Main(void) u32 *otBase = pb->ptrOT; int isLargeGeomScreen; - CTC2((u32)(s32)pb->rect.w << VEH_GROUND_SHADOW_GTE_SCREEN_SHIFT, 24); - CTC2((u32)(s32)pb->rect.h << VEH_GROUND_SHADOW_GTE_SCREEN_SHIFT, 25); - CTC2((u32)pb->distanceToScreen_PREV, 26); + // retail halves the rect first and only then shifts into 16.16, so odd extents keep + // truncating instead of landing half a pixel off + gte_SetGeomOffset(pb->rect.w >> VEH_GROUND_SHADOW_GEOM_OFFSET_SHIFT, pb->rect.h >> VEH_GROUND_SHADOW_GEOM_OFFSET_SHIFT); + gte_SetGeomScreen(pb->distanceToScreen_PREV); VehGroundShadow_LoadGteRotMatrix(&pb->matrix_ViewProj); isLargeGeomScreen = pb->distanceToScreen_PREV > VEH_GROUND_SHADOW_LARGE_GEOM_SCREEN_THRESHOLD; diff --git a/game/Vehicle/VehGroundSkids.c b/game/Vehicle/VehGroundSkids.c index 8da2a69e9..6834e400e 100644 --- a/game/Vehicle/VehGroundSkids.c +++ b/game/Vehicle/VehGroundSkids.c @@ -225,7 +225,8 @@ static void VehGroundSkids_TryEmitSegment(struct VehGroundSkidsScratch *scratch, return; } - scratch->segmentFlagsLow = mark->flags; + // retail stores the whole word, so the upper bytes are cleared rather than left stale + scratch->segmentFlags = mark->flags; int depth = (currDepth[pointIndex] >> VEH_GROUND_SKIDS_DEPTH_SHIFT) + (mark->color << VEH_GROUND_SKIDS_OT_DEPTH_SHIFT); VehGroundSkids_Subset1(&currXY[pointIndex], &prevXY[pointIndex], depth, scratch); } From f233d99977e9e1be0a63d19186c8ce26a3e0a12c Mon Sep 17 00:00:00 2001 From: penta3 Date: Wed, 29 Jul 2026 01:44:47 -0300 Subject: [PATCH 07/12] fix(COLL): consistency with ps1 asm, fix weird landing & deltaTiming --- game/COLL.c | 15 ++++++++++----- game/MAIN/MainFrame.c | 4 +++- game/Timer.c | 15 +++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/game/COLL.c b/game/COLL.c index 7c7123815..52c62a44c 100644 --- a/game/COLL.c +++ b/game/COLL.c @@ -545,9 +545,10 @@ void COLL_FIXED_BSPLEAF_TestInstance(struct BSP *node, struct ScratchpadStruct * return; } - // check every instance hitbox until - // end of list (null flag) is found - for (/**/; bspArray->flag != 0; bspArray++) + // check every instance hitbox until end of list is found. retail loads the + // whole word at +0x0 (flag and id together) and stops on 0x00000000, so a + // zero flag alone does not terminate the list. + for (/**/; (bspArray->flag != 0) || (bspArray->id != 0); bspArray++) { struct BoundingBox *bbox = &bspArray->box; @@ -1209,8 +1210,8 @@ internal void COLL_FIXED_PlayerSearch_SetupSearch(struct ScratchpadStruct *sps, sps->Union.QuadBlockColl.searchFlags = COLL_SEARCH_HIGH_LOD; } + // retail clears only these two here; boolDidTouchHitbox is left alone. sps->boolDidTouchQuadblock = 0; - sps->boolDidTouchHitbox = 0; sps->numTrianglesTested = 0; sps->bbox.min.x = probeTop.x; @@ -1610,11 +1611,15 @@ void COLL_FIXED_PlayerSearch(struct Thread *t, struct Driver *d) { s32 screenOffset = Coll_MipsAbsS32((s8)d->Screen_OffsetY); + // retail branches to UpdateGroundOffset on both rejects here, so + // distanceFromGround keeps its previous value instead of being + // reset below. if ((screenOffset < 4) && ((d->terrainMeta1->flags & TERRAIN_FLAG_RAISE_GROUND_OFFSET) != 0)) { d->distanceFromGround = 4; - goto UpdateGroundOffset; } + + goto UpdateGroundOffset; } } diff --git a/game/MAIN/MainFrame.c b/game/MAIN/MainFrame.c index e72d5dbf4..02e6fe8b1 100644 --- a/game/MAIN/MainFrame.c +++ b/game/MAIN/MainFrame.c @@ -188,7 +188,9 @@ void MainFrame_GameLogic(struct GameTracker *gGT, struct GamepadSystem *gGamepad gGT->unk1cc4[4] = 0; iVar4 = Timer_GetTime_Elapsed(gGT->clockFrameStart, &gGT->clockFrameStart); - iVar4 = (iVar4 << 5) / 100; + // retail uses sll/div here and the bgez below shows negatives are expected, + // so the shift has to be the wrapping kind rather than C's UB on negatives + iVar4 = CTR_MipsDiv(CTR_MipsSll(iVar4, 5), 100); gGT->elapsedTimeMS = iVar4; if (iVar4 < 0) diff --git a/game/Timer.c b/game/Timer.c index a281c375d..9dbe5a10d 100644 --- a/game/Timer.c +++ b/game/Timer.c @@ -35,14 +35,17 @@ int Timer_GetTime_Total() { s32 rcntTotal = sdata->rcntTotalUnits; s32 rcnt = GetRCnt(TIMER_RCNT); - s32 sysClock = rcntTotal + rcnt; + s32 sysClock = CTR_MipsAddLo(rcntTotal, rcnt); if (rcnt < TIMER_RCNT_LOW_RECHECK_THRESHOLD) { - sysClock = sdata->rcntTotalUnits + rcnt; + sysClock = CTR_MipsAddLo(sdata->rcntTotalUnits, rcnt); } - return (sysClock * TIMER_MILLISECONDS_PER_SECOND) / TIMER_RCNT_UNITS_PER_SECOND; + // The 32-bit product is expected to wrap: the counter passes 0x7fffffff after + // roughly two minutes of uptime and Timer_GetTime_Elapsed compensates with + // TIMER_WRAP_MILLISECONDS, so the wraparound is load-bearing rather than a bug. + return CTR_MipsDiv(CTR_MipsMulLo(sysClock, TIMER_MILLISECONDS_PER_SECOND), TIMER_RCNT_UNITS_PER_SECOND); } // Usage: elapsed(frameStart, &frameStart) @@ -58,11 +61,11 @@ int Timer_GetTime_Elapsed(int oldVal, int *retVal) *retVal = newVal; } - // impossible? + // Reached every time the wrapping product in Timer_GetTime_Total rolls over. if (newVal < oldVal) { - newVal += TIMER_WRAP_MILLISECONDS; + newVal = CTR_MipsAddLo(newVal, TIMER_WRAP_MILLISECONDS); } - return newVal - oldVal; + return CTR_MipsSubLo(newVal, oldVal); } From 2aad8303f8db5b40f158ce788700d0375730e223 Mon Sep 17 00:00:00 2001 From: penta3 Date: Wed, 29 Jul 2026 11:57:31 -0300 Subject: [PATCH 08/12] fix(gte, render): avoid geometry overlapping on screen, consistency with emulators implementation --- platform/native_gpu.c | 146 +++++++++++++++++++++++++++++++++++++ platform/native_gte_core.c | 34 ++++++--- 2 files changed, 171 insertions(+), 9 deletions(-) diff --git a/platform/native_gpu.c b/platform/native_gpu.c index 3b121f590..1769844e3 100644 --- a/platform/native_gpu.c +++ b/platform/native_gpu.c @@ -1813,12 +1813,154 @@ internal int ProcessPsyXPrims(P_TAG *polyTag) // Processes primitive // returns processed primitive primLength in longs +// PSX GPU polygon size limit. +// +// Hardware renders a polygon only while the distance between its vertices stays +// within 1023 horizontally and 511 vertically; anything larger is discarded by +// the GPU without drawing a single pixel (psx-spx, "GPU Render Polygon +// Commands"). Retail depends on that: when a vertex ends up nearer than H/2 the +// GTE divide overflows and SX/SY saturate to -0400h..+03FFh, producing a +// triangle that spans the whole screen. On console those simply never appear. +// RenderBucket's per-primitive gate only rejects FLAG bit 18 (SZ3/OTZ +// saturated) - verified against retail at 0x8006a5a0 - so it does not catch +// them either; the GPU size limit is what does. +// +// Without this the GL rasteriser happily draws them, which is what shows up as +// colour streaks across the frame when the camera gets close to an instance. +#define NATIVE_GPU_MAX_POLY_SPAN_X 1023 +#define NATIVE_GPU_MAX_POLY_SPAN_Y 511 + +// Drops the just-emitted geometry that exceeds what the PSX GPU would draw. +// +// The test is per triangle, not per primitive. A quad is rasterised as two +// triangles sharing a diagonal, and hardware evaluates each one on its own, so a +// quad whose opposite corners are more than a span apart still draws both halves +// as long as neither triangle exceeds the limit by itself. Measuring the whole +// quad would throw away geometry the console renders. +// +// Surviving triangles are compacted down. Split vertex counts are derived from +// s_gpu.vertexIndex when the split is closed, so an emptied split draws nothing +// and the recorded state change stays valid for whatever follows. +// +// The drawing offset already added by MakeVertex* needs no correction: it is one +// translation applied to every vertex, so it cannot change a distance, and the +// hardware likewise measures distances between vertices. +internal void NativeGpu_ApplyGpuPolygonSizeLimit(int firstVertex) +{ + int vertexCount = s_gpu.vertexIndex - firstVertex; + + if ((vertexCount < 3) || (firstVertex < 0)) + { + return; + } + + int writeIndex = firstVertex; + int dropped = 0; + + for (int group = firstVertex; (group + 3) <= s_gpu.vertexIndex; group += 3) + { + int minX = 32767; + int maxX = -32768; + int minY = 32767; + int maxY = -32768; + + // Over three vertices the bounding box equals the largest pairwise + // distance, which is what the hardware compares. + for (int i = 0; i < 3; i++) + { + const GrVertex *v = &s_gpu.vertexBuffer[group + i]; + + minX = (v->x < minX) ? v->x : minX; + maxX = (v->x > maxX) ? v->x : maxX; + minY = (v->y < minY) ? v->y : minY; + maxY = (v->y > maxY) ? v->y : maxY; + } + + if (((maxX - minX) > NATIVE_GPU_MAX_POLY_SPAN_X) || ((maxY - minY) > NATIVE_GPU_MAX_POLY_SPAN_Y)) + { + dropped++; + continue; + } + + if (writeIndex != group) + { + memmove(&s_gpu.vertexBuffer[writeIndex], &s_gpu.vertexBuffer[group], 3 * sizeof(GrVertex)); + } + + writeIndex += 3; + } + + if (dropped != 0) + { + s_gpu.vertexIndex = writeIndex; + } +} + +// Same hardware limit, applied to lines - psx-spx says "polygons and lines". +// Each segment is expanded into its own 6-vertex quad here, so a polyline is +// filtered segment by segment (the limit is per segment on hardware, not per +// polyline) and the survivors are compacted down. +// +// Measuring the expanded quad rather than the two endpoints inflates the span by +// the line thickness, so a segment sitting exactly on the boundary could differ +// by a pixel. CTR only draws UI lines, which are bounded by the screen and never +// come close to 1023x511, so nothing real rides on that. +internal void NativeGpu_ApplyGpuLineSizeLimit(int firstVertex) +{ + int vertexCount = s_gpu.vertexIndex - firstVertex; + + if ((vertexCount < 6) || (firstVertex < 0)) + { + return; + } + + int writeIndex = firstVertex; + int dropped = 0; + + for (int group = firstVertex; (group + 6) <= s_gpu.vertexIndex; group += 6) + { + int minX = 32767; + int maxX = -32768; + int minY = 32767; + int maxY = -32768; + + for (int i = 0; i < 6; i++) + { + const GrVertex *v = &s_gpu.vertexBuffer[group + i]; + + minX = (v->x < minX) ? v->x : minX; + maxX = (v->x > maxX) ? v->x : maxX; + minY = (v->y < minY) ? v->y : minY; + maxY = (v->y > maxY) ? v->y : maxY; + } + + if (((maxX - minX) > NATIVE_GPU_MAX_POLY_SPAN_X) || ((maxY - minY) > NATIVE_GPU_MAX_POLY_SPAN_Y)) + { + dropped++; + continue; + } + + if (writeIndex != group) + { + memmove(&s_gpu.vertexBuffer[writeIndex], &s_gpu.vertexBuffer[group], 6 * sizeof(GrVertex)); + } + + writeIndex += 6; + } + + if (dropped != 0) + { + s_gpu.vertexIndex = writeIndex; + } +} + int ParsePrimitive(P_TAG *polyTag) { const int primType = polyTag->code & 0xF0; int primLength = 0; bool handledZeroLength = false; + const int sizeLimitFirstVertex = s_gpu.vertexIndex; switch (primType) { @@ -1892,18 +2034,22 @@ int ParsePrimitive(P_TAG *polyTag) case 0x20: // Flat polygons primLength = ProcessFlatPoly(polyTag); + NativeGpu_ApplyGpuPolygonSizeLimit(sizeLimitFirstVertex); break; case 0x30: // Gouraud shaded polygons primLength = ProcessGouraudPoly(polyTag); + NativeGpu_ApplyGpuPolygonSizeLimit(sizeLimitFirstVertex); break; case 0x40: // Flat (single colour) Lines primLength = ProcessFlatLines(polyTag); + NativeGpu_ApplyGpuLineSizeLimit(sizeLimitFirstVertex); break; case 0x50: // Gouraud lines primLength = ProcessGouraudLines(polyTag); + NativeGpu_ApplyGpuLineSizeLimit(sizeLimitFirstVertex); break; case 0x60: case 0x70: diff --git a/platform/native_gte_core.c b/platform/native_gte_core.c index 6aa71e084..bab81fd6d 100644 --- a/platform/native_gte_core.c +++ b/platform/native_gte_core.c @@ -246,13 +246,20 @@ internal s64 F(s64 a) C2_FLAG |= GTE_FLAG(31) | GTE_FLAG(15); } - // ...but MAC0 is a 32-bit register, so that is what gets stored and what - // every consumer reads back (SX2/SY2 via SAR 16, IR0 via Lm_H, OTZ via - // Lm_D). Keeping the untruncated 64-bit sum here sent SX2/SY2 to the - // opposite screen edge whenever IR*(H/SZ) overflowed 32 bits. + // MAC0 is a 32-bit register, so that is what the register keeps and what + // IR0 (Lm_H) and OTZ (Lm_D) read back. m_mac0 = (s32)a; - return m_mac0; + // The full-width sum is returned, NOT the truncated register. SX2/SY2 are + // produced by shifting this sum right by 16 and only then narrowing, which + // is what DuckStation does (`PushSXY(s32(Sx >> 16), ...)` on the 64-bit Sx) + // and what Amidog's GTE tests pin down on hardware. Shifting the truncated + // register instead wraps the result: OFX + IR1*(H/SZ3) crosses 2^31 as soon + // as a vertex approaches the projection plane, and a coordinate that belongs + // at a screen edge lands at an arbitrary position, which then stretches the + // triangle across the frame. The MAC0 *register* being 32-bit says nothing + // about the width of the SX/SY datapath - they are separate. + return a; } internal int Lm_G1(s64 a) @@ -343,6 +350,7 @@ int GTE_operator(int op) int cv; int mx; int h_over_sz3 = 0; + s64 dq; lm = GTE_LM(gteop(op)); m_sf = GTE_SF(gteop(op)); @@ -355,8 +363,14 @@ int GTE_operator(int op) case 0x01: h_over_sz3 = GTE_RotTransPers(0, lm); - C2_MAC0 = (int)(F((s64)C2_DQB + ((s64)C2_DQA * h_over_sz3))); - C2_IR0 = Lm_H(m_mac0, 1); + // IR0 is taken from the full-width sum, not from the truncated MAC0 + // register: same split as SX2/SY2 above. DQA*(H/SZ3) alone reaches + // 32767*1FFFFh, so the sum leaves 32-bit range whenever a vertex comes + // close, and reading the wrapped register there gives a depth-cue + // factor with no relation to the distance. + dq = F((s64)C2_DQB + ((s64)C2_DQA * h_over_sz3)); + C2_MAC0 = (int)dq; + C2_IR0 = Lm_H(dq, 1); return 1; @@ -680,8 +694,10 @@ int GTE_operator(int op) h_over_sz3 = GTE_RotTransPers(v, lm); } - C2_MAC0 = (int)(F((s64)C2_DQB + ((s64)C2_DQA * h_over_sz3))); - C2_IR0 = Lm_H(m_mac0, 1); + // Same full-width IR0 split as RTPS. + dq = F((s64)C2_DQB + ((s64)C2_DQA * h_over_sz3)); + C2_MAC0 = (int)dq; + C2_IR0 = Lm_H(dq, 1); return 1; case 0x3d: From baebb004c61a3d89f3aa362e98bd07a6f7d67c7c Mon Sep 17 00:00:00 2001 From: penta3 Date: Wed, 29 Jul 2026 13:47:54 -0300 Subject: [PATCH 09/12] headers clean up --- game/HOWL/HOWL_Engine.c | 4 +++- game/UI/UI_RenderFrame.c | 7 ++++--- include/functions.h | 2 +- include/namespace_Instance.h | 11 ++++++++++- include/namespace_Main.h | 7 +++++-- include/regionsEXE.h | 4 +++- 6 files changed, 26 insertions(+), 9 deletions(-) diff --git a/game/HOWL/HOWL_Engine.c b/game/HOWL/HOWL_Engine.c index f78afd1d9..ec91b960f 100644 --- a/game/HOWL/HOWL_Engine.c +++ b/game/HOWL/HOWL_Engine.c @@ -2,7 +2,9 @@ // Initialize car engine audio system for one driver // NOTE(aalhendi): ASM-verified NTSC-U 926 0x80028880-0x800289b0 -b32 EngineAudio_InitOnce(u32 soundID, u32 flags) +// NOTE: retail narrows the first argument to 16 bits at the VehBirth call site +// (`andi a0, a0, 0xffff`), so the parameter is a u16, not a word. +b32 EngineAudio_InitOnce(u16 soundID, u32 flags) { struct EngineFX *ptrEngineFX; struct ChannelStats *channel; diff --git a/game/UI/UI_RenderFrame.c b/game/UI/UI_RenderFrame.c index 6f72baf6d..872a1b759 100644 --- a/game/UI/UI_RenderFrame.c +++ b/game/UI/UI_RenderFrame.c @@ -12,6 +12,7 @@ void UI_RenderFrame_Racing() int partTimeVariable1; u32 *ptrColor; char *pbVar6; + u8 *spawnOrderPtr; int i; struct PushBuffer *pb; u32 partTimeVariable5; @@ -65,10 +66,10 @@ void UI_RenderFrame_Racing() { data.rankIconsTransitionTimer[i] = 0; - pbVar6 = &sdata->kartSpawnOrderArray[i]; + spawnOrderPtr = &sdata->kartSpawnOrderArray[i]; - data.rankIconsCurr[i] = (u16)*pbVar6; - data.rankIconsDesired[i] = (u16)*pbVar6; + data.rankIconsCurr[i] = (u16)*spawnOrderPtr; + data.rankIconsDesired[i] = (u16)*spawnOrderPtr; } } diff --git a/include/functions.h b/include/functions.h index 1ba47f4e3..4869f5041 100644 --- a/include/functions.h +++ b/include/functions.h @@ -148,7 +148,7 @@ void OtherFX_Stop1(int soundID_count); void OtherFX_Stop2(int soundID_count); void OtherFX_RecycleNew(u32 *soundID_Count, u32 newSoundID, u32 modifyFlags); void OtherFX_RecycleMute(u32 *soundID_Count); -b32 EngineAudio_InitOnce(u32 soundID, u32 flags); +b32 EngineAudio_InitOnce(u16 soundID, u32 flags); s16 EngineAudio_Recalculate(u32 soundID, u32 sfx); void EngineAudio_Stop(u32 soundID); void SetReverbMode(u16 newReverbMode); diff --git a/include/namespace_Instance.h b/include/namespace_Instance.h index ed6aff0c4..e554323f7 100644 --- a/include/namespace_Instance.h +++ b/include/namespace_Instance.h @@ -451,11 +451,20 @@ struct InstDef SVec3 rot; // 0x3c - int modelID; + // Retail reads this with `lh` at every site (VehBirth_TeleportSelf, + // BOTS_LevInstColl, COLL_MOVED_PlayerSearch, VehPickupItem_ShootNow): + // the field is 16-bit signed, not a word. + s16 modelID; + + // 0x3e + s16 padding_0x3e; // 0x40 -- struct size }; +CTR_STATIC_ASSERT(sizeof(struct InstDef) == 0x40); +CTR_STATIC_ASSERT(OFFSETOF(struct InstDef, modelID) == 0x3c); + struct InstDrawPerPlayer { // 0x74 diff --git a/include/namespace_Main.h b/include/namespace_Main.h index a04681fce..ce3552e7a 100644 --- a/include/namespace_Main.h +++ b/include/namespace_Main.h @@ -745,7 +745,9 @@ struct GameTracker int enabledWeapons; // 1da4 - char teamOfEachPlayer[4]; + // Retail reads this with `lb` in VehBirth_Player: signed byte. + // Plain `char` would flip meaning on targets where it is unsigned. + s8 teamOfEachPlayer[4]; // 1da8 int finishedRankOfEachTeam[4]; @@ -1442,7 +1444,8 @@ struct GameTracker // all podium related? // 2572 - u16 podiumRewardID; + // Retail loads this with `lh` at all 11 read sites; it is signed. + s16 podiumRewardID; // 2574 u8 bool_AdvHub_NeedToSwapLEV; diff --git a/include/regionsEXE.h b/include/regionsEXE.h index 5be0526bd..cc6329f66 100644 --- a/include/regionsEXE.h +++ b/include/regionsEXE.h @@ -3750,7 +3750,9 @@ struct sData int aiCollisionDelayFrameCount; // 8008d69c - char kartSpawnOrderArray[0x8]; + // Retail reads this with `lbu` (VehBirth_TeleportSelf, BOTS_GotoStartingLine): + // unsigned byte, not plain `char`. + u8 kartSpawnOrderArray[0x8]; // 8008d6a4 char unk_paddingAfterKartSpawn[0x8]; From ac5bef04daf8e672aa003d12d631dde6e7d27868 Mon Sep 17 00:00:00 2001 From: penta3 Date: Fri, 31 Jul 2026 03:10:42 -0300 Subject: [PATCH 10/12] fix(Vehicle, UI): massive clean up --- game/231/RB_Warpball.c | 4 +++- game/PlayLevel.c | 4 ++-- game/UI/UI_Map.c | 4 ++-- game/Vehicle/VehFire.c | 10 +++++----- game/Vehicle/VehLap.c | 2 +- game/Vehicle/VehPhysCrash.c | 4 +++- game/Vehicle/VehPhysGeneral.c | 6 +++++- game/Vehicle/VehPhysProc.c | 14 +++++++++++--- game/Vehicle/VehStuckProc.c | 8 ++++++-- game/Vehicle/VehTurbo.c | 2 +- include/ctr_math.h | 15 +++++++++++++++ include/namespace_Instance.h | 5 ++++- include/namespace_Main.h | 3 ++- include/namespace_Vehicle.h | 27 ++++++++++++++++++++++----- 14 files changed, 82 insertions(+), 26 deletions(-) diff --git a/game/231/RB_Warpball.c b/game/231/RB_Warpball.c index fbca9ae59..1cf4b0847 100644 --- a/game/231/RB_Warpball.c +++ b/game/231/RB_Warpball.c @@ -300,7 +300,9 @@ void RB_Warpball_SeekDriver(struct TrackerWeapon *tw, u32 checkpointIndex, struc // pointer to path node struct CheckpointNode *cn = &first[checkpointIndex]; - while ((d->distanceToFinish_curr <= (u32)(cn->distToFinish << 3)) && + // Retail compares this signed (`slt` at 0x800aed3c in overlay 231), not + // unsigned; distanceToFinish_curr is a signed word. + while ((d->distanceToFinish_curr <= (cn->distToFinish << 3)) && // node is not first node (cn != first)) diff --git a/game/PlayLevel.c b/game/PlayLevel.c index cab774f77..589a0eccb 100644 --- a/game/PlayLevel.c +++ b/game/PlayLevel.c @@ -344,7 +344,7 @@ void PlayLevel_UpdateLapStats(void) // AND // new lowest distance (max progress) - ((s32)currDriver->distanceToFinish_curr < minDistance))) + (currDriver->distanceToFinish_curr < minDistance))) { // set new min distToFinish (max progress) minDistance = currDriver->distanceToFinish_curr; @@ -406,7 +406,7 @@ void PlayLevel_UpdateLapStats(void) int currRank = currDriver->driverRank; if ((PLAYLEVEL_UNSORTED_RANK < currRank) && (PLAYLEVEL_PASS_VOICELINE_DELAY < gGT->elapsedEventTime) && - ((s8)gGT->humanPlayerPositions[driverIndex] < currRank)) + (gGT->humanPlayerPositions[driverIndex] < currRank)) { int characterID = data.characterIDs[gGT->driversInRaceOrder[currRank - 1]->driverID]; diff --git a/game/UI/UI_Map.c b/game/UI/UI_Map.c index 201abc42b..d408b35de 100644 --- a/game/UI/UI_Map.c +++ b/game/UI/UI_Map.c @@ -426,10 +426,10 @@ void UI_Map_DrawTracking(struct UIMap *map, struct Thread *bucket) // == only draw target if target exists == // flicker - targetColor = CRASH_BLUE; + targetColor = WHITE; if ((sdata->gGT->timer & 1) != 0) { - targetColor = CORTEX_RED; + targetColor = RED; } UI_Map_DrawRawIcon(map, &d->instSelf->matrix.t[0], UI_MAP_WARPBALL_TARGET_ICON, targetColor, 0, UI_MAP_ICON_SCALE); diff --git a/game/Vehicle/VehFire.c b/game/Vehicle/VehFire.c index 458ee7315..243f447e4 100644 --- a/game/Vehicle/VehFire.c +++ b/game/Vehicle/VehFire.c @@ -117,7 +117,6 @@ void VehFire_Increment(struct Driver *driver, int reserves, u32 type, int fireLe s8 count; int newFireSpeedCap; - int newFireSize; int oldOTT; u32 addFlags; @@ -379,12 +378,13 @@ void VehFire_Increment(struct Driver *driver, int reserves, u32 type, int fireLe if (turboObj != 0) { // modify, cap, and save the size of the fire - newFireSize = CTR_MipsAddLo(CTR_MipsSra(fireLevel, VEH_FIRE_SIZE_SHIFT), VEH_FIRE_SIZE_BASE); - if (newFireSize > VEH_FIRE_SIZE_MAX) + // Retail stores the size first and caps the stored s16, not the + // 32-bit intermediate, so the truncation happens before the compare. + turboObj->fireSize = (s16)CTR_MipsAddLo(CTR_MipsSra(fireLevel, VEH_FIRE_SIZE_SHIFT), VEH_FIRE_SIZE_BASE); + if (turboObj->fireSize > VEH_FIRE_SIZE_MAX) { - newFireSize = VEH_FIRE_SIZE_MAX; + turboObj->fireSize = VEH_FIRE_SIZE_MAX; } - turboObj->fireSize = (s16)newFireSize; } } diff --git a/game/Vehicle/VehLap.c b/game/Vehicle/VehLap.c index c9e8fb1ae..f24c60c25 100644 --- a/game/Vehicle/VehLap.c +++ b/game/Vehicle/VehLap.c @@ -81,7 +81,7 @@ void VehLap_UpdateProgress(struct Driver *driver) driver->distanceToFinish_curr = progress; // NOTE(aalhendi): Retail uses signed div/mfhi for this remainder. - driver->distanceToFinish_curr = progress % trackLength; + driver->distanceToFinish_curr = CTR_MipsRem(progress, trackLength); if (wrongWayTest < VEH_LAP_WRONG_WAY_DOT_LIMIT) { diff --git a/game/Vehicle/VehPhysCrash.c b/game/Vehicle/VehPhysCrash.c index fe9d45649..8ed744b85 100644 --- a/game/Vehicle/VehPhysCrash.c +++ b/game/Vehicle/VehPhysCrash.c @@ -351,7 +351,9 @@ static void VehPhysCrash_PlayHumanFeedback(struct Thread *selfThread, struct Thr // NOTE(aalhendi): ASM-verified NTSC-U 926 0x8005d404-0x8005e104 void VehPhysCrash_AnyTwoCars(struct Thread *thread, struct DriverCollisionSearch *search, Vec3 *selfVel) { - int distance = VehCalc_FastSqrt(search->bucket.bestDistSq, 0); + // Retail calls MATH_FastSqrt here (0x8005d43c), not VehCalc_FastSqrt. Both + // return floor(sqrt(n)), but retail takes prevalence. + int distance = MATH_FastSqrt(search->bucket.bestDistSq, 0); const SVec3 *dist = &search->bucket.dist; SVec3 *hitDir = &search->hitDir; diff --git a/game/Vehicle/VehPhysGeneral.c b/game/Vehicle/VehPhysGeneral.c index e941543f1..0668a108c 100644 --- a/game/Vehicle/VehPhysGeneral.c +++ b/game/Vehicle/VehPhysGeneral.c @@ -1137,7 +1137,11 @@ void VehPhysGeneral_SetHeldItem(struct Driver *driver) case ITEMSET_Race4: case ITEMSET_BattleDefault: case ITEMSET_BossRace: - driver->heldItemID = itemSetWeaponTables[itemSet][(rng * itemSetWeaponCounts[itemSet]) / ITEMSET_RNG_BUCKET_COUNT]; + // Retail divides this one WITHOUT sign (multu + srl 3/6, one inlined + // weapon count per case), and the battle-custom case below WITH sign + // (mult + sra + correction, since numWeapons is an int). rng is always + // in [0, 199] so both give the same index, but retail takes prevalence. + driver->heldItemID = itemSetWeaponTables[itemSet][((u32)rng * itemSetWeaponCounts[itemSet]) / ITEMSET_RNG_BUCKET_COUNT]; break; // uses int array instead of char, diff --git a/game/Vehicle/VehPhysProc.c b/game/Vehicle/VehPhysProc.c index ceb0265fd..4f503e0fe 100644 --- a/game/Vehicle/VehPhysProc.c +++ b/game/Vehicle/VehPhysProc.c @@ -633,8 +633,13 @@ void VehPhysProc_Driving_PhysLinear(struct Thread *thread, struct Driver *driver buttonsTapped = ptrgamepad->buttonsTapped; } - cross = buttonsHeld & BTN_CROSS; - square = buttonsHeld & BTN_SQUARE; + // Retail masks only the "_one" bits here (andi 0x10 / andi 0x20 at 0x80062030 + // and 0x80062038); the combined BTN_CROSS / BTN_SQUARE masks appear nowhere in + // the retail EXE. Both bits always travel together because the two + // gamepadMapBtn entries share the same rawInput, so this is equivalent, but + // retail takes prevalence. + cross = buttonsHeld & BTN_CROSS_one; + square = buttonsHeld & BTN_SQUARE_one; // state of kart kartState = driver->kartState; @@ -1814,7 +1819,10 @@ void VehPhysProc_PowerSlide_PhysAngular(struct Thread *th, struct Driver *driver int turnAngleStep = CTR_MipsSra(turnAngleDelta, VEH_PHYS_PROC_DRIFT_ANGLE_LERP_SHIFT); - int turnAngleStepSigned = (s16)turnAngleStep; + // Retail keeps this step 32-bit and only truncates on the store below, same + // as the ampTurnState sum further down. The result is identical either way + // (both reduce mod 2^16), but retail takes prevalence. + int turnAngleStepSigned = turnAngleStep; if (turnAngleDelta != 0) { if (turnAngleStep == 0) diff --git a/game/Vehicle/VehStuckProc.c b/game/Vehicle/VehStuckProc.c index 33c639d2e..a07db39f5 100644 --- a/game/Vehicle/VehStuckProc.c +++ b/game/Vehicle/VehStuckProc.c @@ -526,11 +526,15 @@ void VehStuckProc_MaskGrab_Animate(struct Thread *t, struct Driver *d) // set mask posZ mask->pos.z = (s16)CTR_MipsSra(d->posCurr.z, FRACTIONAL_BITS_8); + // Retail compares the s16 mask height against the untruncated + // posCurr.y >> 8 and only narrows on the store. + int driverHeight = CTR_MipsSra(d->posCurr.y, FRACTIONAL_BITS_8); + // if mask posY < driver posY - if (mask->pos.y < (s16)CTR_MipsSra(d->posCurr.y, FRACTIONAL_BITS_8)) + if (mask->pos.y < driverHeight) { // mask posY = driver posY - mask->pos.y = (s16)CTR_MipsSra(d->posCurr.y, FRACTIONAL_BITS_8); + mask->pos.y = (s16)driverHeight; d->KartStates.MaskGrab.boolLiftingPlayer = true; } diff --git a/game/Vehicle/VehTurbo.c b/game/Vehicle/VehTurbo.c index 9f9abd504..43ac0a182 100644 --- a/game/Vehicle/VehTurbo.c +++ b/game/Vehicle/VehTurbo.c @@ -288,7 +288,7 @@ void VehTurbo_ThTick(struct Thread *turboThread) // player of any kind if (instanceDriver->thread->modelIndex == DYNAMIC_PLAYER) { - int fireSfxVolume = TURBO_AUDIO_VOLUME_BASE - (u32)(instance->alphaScale >> TURBO_AUDIO_ALPHA_SHIFT); + int fireSfxVolume = TURBO_AUDIO_VOLUME_BASE - (int)(instance->alphaScale >> TURBO_AUDIO_ALPHA_SHIFT); if (fireSfxVolume < 0) { diff --git a/include/ctr_math.h b/include/ctr_math.h index d7080ada5..e31b0b1ae 100644 --- a/include/ctr_math.h +++ b/include/ctr_math.h @@ -209,6 +209,21 @@ static inline s32 CTR_MipsDiv(s32 dividend, s32 divisor) return dividend / divisor; } +// MIPS `div` + `mfhi`. Retail's compiler emits an explicit `break` for the two +// operand pairs the hardware leaves undefined, so trap on them here too instead +// of letting C's `%` invoke undefined behaviour. +static inline s32 CTR_MipsRem(s32 dividend, s32 divisor) +{ + const s32 minS32 = (-2147483647 - 1); + + if ((divisor == 0) || ((divisor == -1) && (dividend == minS32))) + { + CTR_TRAP(); + } + + return dividend % divisor; +} + static inline u32 CTR_MipsDivU(u32 dividend, u32 divisor) { if (divisor == 0) diff --git a/include/namespace_Instance.h b/include/namespace_Instance.h index e554323f7..25da4efaa 100644 --- a/include/namespace_Instance.h +++ b/include/namespace_Instance.h @@ -574,7 +574,10 @@ struct Instance SVec3 scale; // 0x22 - s16 alphaScale; + // Retail reads this only with `lhu`, and every operation on it is unsigned: + // `srl` in VehTurbo_ThTick (0x80069444, 0x800699cc) and `sltiu` at + // 0x8006990c / 0x80069adc / 0x80069b44. A signed short would give lh/sra/slt. + u16 alphaScale; // 0x24 u32 colorRGBA; diff --git a/include/namespace_Main.h b/include/namespace_Main.h index ce3552e7a..5a4f3d74d 100644 --- a/include/namespace_Main.h +++ b/include/namespace_Main.h @@ -1473,7 +1473,8 @@ struct GameTracker // 257a // only updated for human players - u8 humanPlayerPositions[8]; + // Retail's only load is `lb` (PlayLevel_UpdateLapStats 0x80041ac4): signed. + s8 humanPlayerPositions[8]; // 2582 // determines if you see Oxide Intro, diff --git a/include/namespace_Vehicle.h b/include/namespace_Vehicle.h index 5886d0c61..20df907e2 100644 --- a/include/namespace_Vehicle.h +++ b/include/namespace_Vehicle.h @@ -1601,13 +1601,21 @@ struct Driver // 0x484 - last of constants // 0x488 - u32 distanceToFinish_curr; + // Retail compares this signed at all 15 sites (`slti`/`slt`/`bgez` in + // PlayLevel_UpdateLapStats, BOTS_ThTick_Drive, HOWL and PickupBots); + // there is not a single `sltu`/`sltiu` on it. + s32 distanceToFinish_curr; // 0x48C + // Retail only ever loads/stores this as a word, never compares it on its + // own, so its signedness is not observable. Kept unsigned: the one place it + // is compared (PlayLevel_UpdateLapStats 0x800418a4) is a `sltu` against the + // unsigned track length. u32 distanceToFinish_checkpoint; // 0x490 - u32 distanceDrivenBackwards; + // Retail compares this signed (`bgez` 0x800415bc, `slti 501` 0x80053198). + s32 distanceDrivenBackwards; // 0x494 struct DriverCheckpointState checkpoint; @@ -1729,7 +1737,8 @@ struct Driver // 0x4fe // 0, 1, 2, depending on rev level - char revEngineState; + // Retail only ever touches this with sb/lbu, so it is an unsigned byte. + u8 revEngineState; // 0x4ff u8 pendingDamageType; @@ -1743,7 +1752,9 @@ struct Driver // 0x508 // backup of alpha, used for turbo fire - s16 alphaScaleBackup; + // Retail loads this only with `lhu` (BOTS_ThTick_Drive 0x800151d4, + // COLL_FIXED_PlayerSearch 0x8001e054, VehTurbo_ThTick 0x80069aa4). + u16 alphaScaleBackup; // 0x50A RainCloudEffect rainCloudEffect; @@ -1941,7 +1952,9 @@ struct Driver RevEngineLockoutFlags lockoutFlags; // 0x594 - int boolMaskGrab; + // Retail only ever touches this with sb/lbu, so it is a byte. + u8 boolMaskGrab; + u8 padding_0x595[3]; // == end == @@ -2297,6 +2310,10 @@ CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.RevEngine.releaseCooldownTi CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.RevEngine.emptyCooldownTimerMS) == 0x590); CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.RevEngine.chargeState) == 0x592); CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.RevEngine.lockoutFlags) == 0x593); +CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.RevEngine.boolMaskGrab) == 0x594); +CTR_STATIC_ASSERT(sizeof(((struct Driver *)0)->KartStates.RevEngine.boolMaskGrab) == 0x1); +CTR_STATIC_ASSERT(offsetof(struct Driver, revEngineState) == 0x4fe); +CTR_STATIC_ASSERT(sizeof(((struct Driver *)0)->revEngineState) == 0x1); CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.Warp) == 0x580); CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.Warp.dustAngle) == 0x58c); CTR_STATIC_ASSERT(offsetof(struct Driver, KartStates.Warp.beamHeight) == 0x590); From 160d85cf899e152b548fa4cb4fe477d141095d2d Mon Sep 17 00:00:00 2001 From: penta3 Date: Fri, 31 Jul 2026 05:50:28 -0300 Subject: [PATCH 11/12] fix(CAM): consistency with ps1 asm --- game/CAM.c | 100 ++++++++++++++++++++++--------------- include/namespace_Camera.h | 4 +- 2 files changed, 63 insertions(+), 41 deletions(-) diff --git a/game/CAM.c b/game/CAM.c index 87ebc02e7..c60449e82 100644 --- a/game/CAM.c +++ b/game/CAM.c @@ -18,23 +18,27 @@ static u32 CAM_SkyboxGlow_PackXY(s32 x, s32 y) return ((u32)(u16)x) | ((u32)(u16)y << 16); } +// ratio is unbounded (the gradient endpoints can sit arbitrarily close), so the +// products below rely on MIPS mult/mflo wraparound, not on C signed overflow. static u32 CAM_SkyboxGlow_LerpColor(u32 from, u32 to, s32 ratio) { - s32 r = (u8)from + ((((s32)(u8)to - (s32)(u8)from) * ratio) >> 12); - s32 g = (u8)(from >> 8) + ((((s32)(u8)(to >> 8) - (s32)(u8)(from >> 8)) * ratio) >> 12); - s32 b = (u8)(from >> 16) + ((((s32)(u8)(to >> 16) - (s32)(u8)(from >> 16)) * ratio) >> 12); + s32 r = (u8)from + CTR_MipsSra(CTR_MipsMulLo((s32)(u8)to - (s32)(u8)from, ratio), 12); + s32 g = (u8)(from >> 8) + CTR_MipsSra(CTR_MipsMulLo((s32)(u8)(to >> 8) - (s32)(u8)(from >> 8), ratio), 12); + s32 b = (u8)(from >> 16) + CTR_MipsSra(CTR_MipsMulLo((s32)(u8)(to >> 16) - (s32)(u8)(from >> 16), ratio), 12); return ((u32)(u8)r) | ((u32)(u8)g << 8) | ((u32)(u8)b << 16); } static s32 CAM_SkyboxGlow_FixedRatio(s32 numerator, s32 denominator) { - return (numerator << 12) / denominator; + // numerator is negative whenever the gradient edge is off-screen, which is + // most of the time: shifting it left is a plain sll in retail. + return CTR_MipsDiv(CTR_MipsSll(numerator, 12), denominator); } static s32 CAM_SkyboxGlow_ScreenX(s32 screenWidth, s32 ratio) { - return (screenWidth * ratio) >> 12; + return CTR_MipsSra(CTR_MipsMulLo(screenWidth, ratio), 12); } static s32 CAM_SkyboxGlow_Div2TowardZero(s32 value) @@ -45,7 +49,7 @@ static s32 CAM_SkyboxGlow_Div2TowardZero(s32 value) static s32 CAM_SkyboxGlow_CalcCenterY(struct PushBuffer *pb) { - s32 pitch = (pb->rot.x - 0x800) * 0x78; + s32 pitch = CTR_MipsMulLo(pb->rot.x - 0x800, 0x78); s32 height = (s16)pb->rect.h; if (pitch < 0) @@ -58,13 +62,15 @@ static s32 CAM_SkyboxGlow_CalcCenterY(struct PushBuffer *pb) static s32 CAM_SkyboxGlow_CalcTilt(struct PushBuffer *pb) { + // Retail calls MATH_Sin first, then MATH_Cos. + s32 sine = MATH_Sin(pb->rot.z); s32 cosine = MATH_Cos(pb->rot.z); if (cosine == 0) { cosine = 1; } - s32 ratio = (MATH_Sin(pb->rot.z) << 12) / cosine; + s32 ratio = CTR_MipsDiv(CTR_MipsSll(sine, 12), cosine); s32 shifted = (s32)((u32)ratio << 8); shifted >>= 12; @@ -300,8 +306,9 @@ void CAM_SkyboxGlow(struct SkyboxGlowGradient *grad, struct PushBuffer *pb, stru // NOTE(aalhendi): ASM-verified NTSC-U 926 0x8001861c-0x80018818 void CAM_ClearScreen(struct GameTracker *gGT) { - s8 numPlyr = gGT->numPlyrCurrGame; - s8 swap = gGT->swapchainIndex; + // Retail reads numPlyrCurrGame with lbu and swapchainIndex with lw. + u8 numPlyr = gGT->numPlyrCurrGame; + s32 swap = gGT->swapchainIndex; struct Level *level1 = gGT->level1; struct DB *backDB = gGT->backBuffer; TILE *tile = backDB->primMem.cursor; @@ -312,13 +319,16 @@ void CAM_ClearScreen(struct GameTracker *gGT) uint32_t *endOT = &pb->ptrOT[0x3FF]; s16 x = pb->rect.x; - s16 y = pb->rect.y + swap * 0x128; + // Retail builds swap*0x128 with a wrapping sll/addu chain, never a + // trapping multiply. + s16 y = (s16)CTR_MipsAddLo(pb->rect.y, CTR_MipsMulLo(swap, 0x128)); s16 w = pb->rect.w; s16 h = pb->rect.h; // cam up/down changes where the line splits. // At 0x800, camera looks straight, and line is perfectly midpoint - s32 splitLine = (((s32)pb->rot.x - 0x800) >> 3) + (h >> 1); + // Retail divides the height by 2 (truncating toward zero), it does not shift. + s32 splitLine = (((s32)pb->rot.x - 0x800) >> 3) + (h / 2); if (splitLine < 0) { @@ -387,8 +397,8 @@ void CAM_Init(struct CameraDC *cDC, s32 cameraID, struct Driver *d, struct PushB cDC->driverToFollow = d; cDC->pushBuffer = pb; - // dont set cameraMode to zero, - // memset makes it already zero + // Redundant after the memset, but retail emits the store (sh zero, 0x9a). + cDC->cameraMode = 0; cDC->flags |= CAMERA_FLAG_DIRECTION_CHANGED; } @@ -814,8 +824,8 @@ void CAM_FollowDriver_AngleAxis(struct CameraDC *cDC, struct Driver *d, struct C void CAM_StartLine_FlyIn(struct FlyInData *flyInData, s16 maxFrames, s32 frame, SVec3 *desiredPos, SVec3 *desiredRot) { struct Level *lev = sdata->gGT->level1; - s32 frameIndex = (frame << 0x10) >> 4; - s32 frameRatio = frameIndex / maxFrames; + s32 frameIndex = CTR_MipsSra(CTR_MipsSll(frame, 0x10), 4); + s32 frameRatio = CTR_MipsDiv(frameIndex, maxFrames); s32 countEnd = flyInData->frameCount1; s16 count = flyInData->frameCount2; SVECTOR local_78; @@ -842,7 +852,10 @@ void CAM_StartLine_FlyIn(struct FlyInData *flyInData, s16 maxFrames, s32 frame, else { pathEnd = (s16 *)(flyInData->ptrEnd + countEnd * 6 - 0xc); - pathRatioEnd = 0; + + // Retail parks the ratio at 0x1000 here, not 0: the `& 0xfff` below + // zeroes it either way, but keep the constant retail uses. + pathRatioEnd = 0x1000; } if (pathIndex < flyInData->frameCount2 - 1) @@ -852,7 +865,7 @@ void CAM_StartLine_FlyIn(struct FlyInData *flyInData, s16 maxFrames, s32 frame, else { pathStart = (s16 *)(flyInData->ptrStart + flyInData->frameCount2 * 6 - 0xc); - frameRatio = 0; + frameRatio = 0x1000; } s32 ratio = count * pathRatioEnd & 0xfff; @@ -894,7 +907,7 @@ void CAM_StartLine_FlyIn(struct FlyInData *flyInData, s16 maxFrames, s32 frame, s16 deltaZ = desiredPos->z - (s16)transformed.vz; desiredRot->y = (s16)ratan2(deltaX, deltaZ); - desiredRot->x = 0x800 - (s16)ratan2(deltaY, SquareRoot0(deltaX * deltaX + deltaZ * deltaZ)); + desiredRot->x = 0x800 - (s16)ratan2(deltaY, SquareRoot0_stub(deltaX * deltaX + deltaZ * deltaZ)); desiredRot->z = 0; } @@ -985,7 +998,7 @@ u32 CAM_FollowDriver_TrackPath(struct CameraDC *cDC, SVec3 *pos, s32 speed, s32 if (segmentLength != 0) { - ratio = (pathProgress << 12) / segmentLength; + ratio = CTR_MipsDiv(CTR_MipsSll(pathProgress, 12), segmentLength); } else { @@ -1022,7 +1035,7 @@ void CAM_LookAtPosition(struct CameraScratchWork *scratchWork, Vec3 *positions, cam->dir.y = dirY; cam->dir.z = dirZ; - s32 distance = SquareRoot0_stub(CAM_MulLo(dirX, dirX) + CAM_MulLo(dirZ, dirZ)); + s32 distance = SquareRoot0_stub(CTR_MipsAddLo(CAM_MulLo(dirX, dirX), CAM_MulLo(dirZ, dirZ))); // rotations desiredRot->x = 0x800 - (s16)ratan2(dirY, distance); @@ -1085,7 +1098,8 @@ void CAM_FollowDriver_Normal(struct CameraDC *cDC, struct Driver *d, SVec3 *push struct CameraScratch *cam = &scratchWork->camera; struct GameTracker *gGT = sdata->gGT; struct GamepadBuffer *pad = &sdata->gGamepads->gamepad[d->driverID]; - s8 state; + // Retail reads both kartState and terrain_type with lbu, and both are u8. + u8 state; s16 uVar8; s16 sVar10; u32 backupFlags; @@ -1445,7 +1459,7 @@ void CAM_FollowDriver_Normal(struct CameraDC *cDC, struct Driver *d, SVec3 *push else { - state = (s8)quad->terrain_type; + state = quad->terrain_type; // Mud, Water, or FastWater if (((state == 0xe) || (state == 4)) || (state == 0xd)) @@ -1695,21 +1709,22 @@ void CAM_FollowDriver_Normal(struct CameraDC *cDC, struct Driver *d, SVec3 *push if (iVar12 <= iVar14) { - x = x >> 1; + // Retail halves with a truncate-toward-zero divide, not a shift. + x = x / 2; if (iVar12 < x) { // Sine(angle) - x = MATH_Sin(0x400 - (iVar12 << 10) / x); + x = MATH_Sin(0x400 - CTR_MipsDiv(CTR_MipsSll(iVar12, 10), x)); cDC->transitionBlend = (s16)(x / 2) + 0x800; } else { - iVar14 = (iVar12 - iVar14) * 0x400; + iVar14 = CTR_MipsSll(iVar12 - iVar14, 10); // Cosine(angle) - x = MATH_Cos(iVar14 / x); + x = MATH_Cos(CTR_MipsDiv(iVar14, x)); cDC->transitionBlend = 0x800 - (s16)(x / 2); } @@ -2014,7 +2029,8 @@ void CAM_ThTick(struct Thread *t) sVar5 = psVar21[2]; sVar1 = psVar21[3]; - iVar7 = VehCalc_MapToRange((s32)sVar6 * (s32)sVar6 + (s32)sVar5 * (s32)sVar5 + (s32)sVar1 * (s32)sVar1, 0x10000, 0x190000, 0x80, 0xf0); + iVar7 = VehCalc_MapToRange(CTR_MipsAddLo(CTR_MipsAddLo(CAM_MulLo(sVar6, sVar6), CAM_MulLo(sVar5, sVar5)), CAM_MulLo(sVar1, sVar1)), 0x10000, + 0x190000, 0x80, 0xf0); cDC->angleAxisLerpRatio = (s16)iVar7; break; @@ -2134,8 +2150,8 @@ void CAM_ThTick(struct Thread *t) CTR_MipsAddLo(CTR_MipsAddLo(CAM_MulLo((s32)stackMemPos.x, (s32)stackMemPos.x), CAM_MulLo((s32)stackMemPos.y, (s32)stackMemPos.y)), CAM_MulLo((s32)stackMemPos.z, (s32)stackMemPos.z))); - iVar18 = cDC->trackPathProgress << 0xc; - iVar25 = iVar18 / iVar24; + iVar18 = CTR_MipsSll(cDC->trackPathProgress, 0xc); + iVar25 = CTR_MipsDiv(iVar18, iVar24); /* if (iVar24 == 0) { @@ -2146,7 +2162,8 @@ void CAM_ThTick(struct Thread *t) trap(0x1800); } */ - cDC->trackPathProgress = cDC->trackPathProgress + (((cDC->transitionFrame * 0x1000) / 0x1e) * iVar7 >> 0xc); + cDC->trackPathProgress = + cDC->trackPathProgress + CTR_MipsSra(CTR_MipsMulLo((cDC->transitionFrame * 0x1000) / 0x1e, iVar7), 0xc); if (iVar8 < 1) { if (iVar25 < 0x1001) @@ -2171,9 +2188,11 @@ void CAM_ThTick(struct Thread *t) { psVar21 = cDC->eorModeData.pointPath.endPos.v; } - pb->pos.x = psVar21[0] + (s16)((stackMemPos.x * iVar25) >> 0xc); - pb->pos.y = psVar21[1] + (s16)((stackMemPos.y * iVar25) >> 0xc); - pb->pos.z = psVar21[2] + (s16)((stackMemPos.z * iVar25) >> 0xc); + // iVar25 is a raw (progress << 12) / distance quotient, so these + // products rely on mult/mflo wraparound. + pb->pos.x = psVar21[0] + (s16)CTR_MipsSra(CTR_MipsMulLo(stackMemPos.x, iVar25), 0xc); + pb->pos.y = psVar21[1] + (s16)CTR_MipsSra(CTR_MipsMulLo(stackMemPos.y, iVar25), 0xc); + pb->pos.z = psVar21[2] + (s16)CTR_MipsSra(CTR_MipsMulLo(stackMemPos.z, iVar25), 0xc); goto LAB_8001c11c; } if (sVar6 == 7) @@ -2292,9 +2311,9 @@ void CAM_ThTick(struct Thread *t) iVar7 = SquareRoot0_stub(CTR_MipsAddLo(CAM_MulLo(camThTick->dir.x, camThTick->dir.x), CAM_MulLo(camThTick->dir.z, camThTick->dir.z))); iVar17 = (s32)(cDC->transitionTo).pos.x; - iVar24 = (iVar7 - (cDC->transitionTo).pos.y) * iVar17; + iVar24 = CTR_MipsMulLo(CTR_MipsSubLo(iVar7, (cDC->transitionTo).pos.y), iVar17); iVar8 = (s32)(cDC->transitionTo).pos.z; - iVar7 = iVar24 / iVar8; + iVar7 = CTR_MipsDiv(iVar24, iVar8); /* if (iVar8 == 0) { @@ -2318,12 +2337,13 @@ void CAM_ThTick(struct Thread *t) pb->distanceToScreen_PREV = pb->distanceToScreen_CURR + iVar7; } - Vec3 cameraProbePos; - cameraProbePos.x = (s32)pb->pos.x; - cameraProbePos.y = (s32)pb->pos.y; - cameraProbePos.z = (s32)pb->pos.z; + // Retail probes from camThTick->pos (scratchpad), not from a stack + // copy, same as the sibling call in CAM_FollowDriver_Normal. + camThTick->pos.x = (s32)pb->pos.x; + camThTick->pos.y = (s32)pb->pos.y; + camThTick->pos.z = (s32)pb->pos.z; - CAM_FindClosestQuadblock((struct ScratchpadStruct *)scratchWork, cDC, d, &cameraProbePos); + CAM_FindClosestQuadblock((struct ScratchpadStruct *)scratchWork, cDC, d, &camThTick->pos); goto LAB_8001c150; } } diff --git a/include/namespace_Camera.h b/include/namespace_Camera.h index 22ef356a1..d8e1615b8 100644 --- a/include/namespace_Camera.h +++ b/include/namespace_Camera.h @@ -224,7 +224,9 @@ struct CameraDC u16 mode; // 0x0A - u16 nearOrFar; + // Retail's only load of this field (CAM_ThTick 0x8001b478) is an `lh` whose + // result indexes ZoomData without being truncated back to 16 bits. + s16 nearOrFar; // 0xC u32 unk0xC; From 9d23bef719bcb969112cfe684f90010e27536a6f Mon Sep 17 00:00:00 2001 From: penta3 Date: Fri, 31 Jul 2026 06:54:26 -0300 Subject: [PATCH 12/12] fix(GAMEPAD): fix mirrored controller, accuracy with hardware --- game/GAMEPAD.c | 31 +++-- include/ctr_math.h | 12 ++ include/functions.h | 2 +- include/platform/native_input.h | 2 + platform/native_input.c | 238 +++++++++++++++++++++++--------- platform/native_libpad.c | 19 +-- 6 files changed, 223 insertions(+), 81 deletions(-) diff --git a/game/GAMEPAD.c b/game/GAMEPAD.c index de2781e0b..60333b4f2 100644 --- a/game/GAMEPAD.c +++ b/game/GAMEPAD.c @@ -40,7 +40,7 @@ void GAMEPAD_SetMainMode(void) // NOTE(aalhendi): ASM-verified NTSC-U 926 0x800252a0-0x80025410. -void GAMEPAD_ProcessState(struct GamepadBuffer *pad, int padState, s16 id) +void GAMEPAD_ProcessState(struct GamepadBuffer *pad, int padState, int id) { int iVar2; int iVar3; @@ -75,15 +75,19 @@ void GAMEPAD_ProcessState(struct GamepadBuffer *pad, int padState, s16 id) iVar2 = 2; } - // set to zero by default - CTR_WriteU16LE(&pad->motorPower[0], 0); - // loop through motors for (iVar3 = 0; iVar3 < iVar2; iVar3++) { pad->motorPower[iVar3] = (u8)PadInfoAct(id, iVar3, 4); } + // zero whatever the pad does not have, retail fills the tail + // afterwards instead of clearing both entries up front + for (iVar3 = iVar2; iVar3 < 2; iVar3++) + { + pad->motorPower[iVar3] = 0; + } + PadSetAct(id, &pad->motorSubmit[0], sizeof(pad->motorSubmit)); if (PadSetActAlign(id, &sdata->unkPadSetActAlign[0]) != 0) @@ -740,8 +744,14 @@ void GAMEPAD_ProcessMotors(struct GamepadSystem *gGS) { pad->motorDesired[0] = pad->unk45; - pad->unk46 -= gGT->elapsedTimeMS; - if (pad->unk46 < 1) + // retail keeps the subtraction in a 32-bit register and only + // stores it once the result is known positive + int remaining = pad->unk46 - gGT->elapsedTimeMS; + if (remaining > 0) + { + pad->unk46 = (s16)remaining; + } + else { pad->unk46 = 0; pad->unk45 = 0; @@ -857,11 +867,14 @@ void GAMEPAD_ProcessMotors(struct GamepadSystem *gGS) if (totalPower > 60) { int numPads = gGS->numGamepadsConnected; - int skipIndex = gGT->timer % numPads; + + // retail divides with `divu`, not `div` + int skipIndex = (int)CTR_MipsRemU((u32)gGT->timer, (u32)numPads); for (int i = skipIndex; i < skipIndex + numPads && totalPower > 60; i++) { - struct GamepadBuffer *pad = &gGS->gamepad[i % numPads]; + // retail wraps by subtracting, it never emits a second division here + struct GamepadBuffer *pad = &gGS->gamepad[(i < numPads) ? i : (i - numPads)]; if (pad->motorDesired[1] != 0) { @@ -872,7 +885,7 @@ void GAMEPAD_ProcessMotors(struct GamepadSystem *gGS) for (int i = skipIndex; i < skipIndex + numPads && totalPower > 60; i++) { - struct GamepadBuffer *pad = &gGS->gamepad[i % numPads]; + struct GamepadBuffer *pad = &gGS->gamepad[(i < numPads) ? i : (i - numPads)]; if (pad->motorDesired[0] != 0) { diff --git a/include/ctr_math.h b/include/ctr_math.h index e31b0b1ae..7560e87fd 100644 --- a/include/ctr_math.h +++ b/include/ctr_math.h @@ -234,6 +234,18 @@ static inline u32 CTR_MipsDivU(u32 dividend, u32 divisor) return dividend / divisor; } +// MIPS `divu` + `mfhi`. Same deal as CTR_MipsRem, minus the INT_MIN / -1 pair +// that only exists for the signed opcode. +static inline u32 CTR_MipsRemU(u32 dividend, u32 divisor) +{ + if (divisor == 0) + { + CTR_TRAP(); + } + + return dividend % divisor; +} + // misc // #ifndef CTR_NATIVE diff --git a/include/functions.h b/include/functions.h index 4869f5041..f83bcb741 100644 --- a/include/functions.h +++ b/include/functions.h @@ -117,7 +117,7 @@ void GAMEPAD_ProcessSticks(struct GamepadSystem *gGamepads); int GAMEPAD_ProcessTapRelease(struct GamepadSystem *gGamepads); void GAMEPAD_ProcessMotors(struct GamepadSystem *gGamepads); int GAMEPAD_ProcessAnyoneVars(struct GamepadSystem *gGamepads); -void GAMEPAD_ProcessState(struct GamepadBuffer *pad, int padState, s16 id); +void GAMEPAD_ProcessState(struct GamepadBuffer *pad, int padState, int id); void GAMEPAD_ShockForce2(struct Driver *d, int frame, int val); b32 GAMEPROG_CheckGhostsBeaten(int ghostID); diff --git a/include/platform/native_input.h b/include/platform/native_input.h index 35fbacb15..ce952b57a 100644 --- a/include/platform/native_input.h +++ b/include/platform/native_input.h @@ -25,6 +25,8 @@ int Platform_InputCycleGamepadController(void); void Platform_InputPadInit(int slot, unsigned char *padData); int Platform_InputPadGetState(int port); +int Platform_InputPadSetMainMode(int port, int offs, int lock); +int Platform_InputPadInfoAct(int port, int acno, int term); void Platform_InputPadVibrate(int port, unsigned char *table, int len); int Platform_InputCapturePadSnapshots(struct PlatformInputPadSnapshot *dst, int count); int Platform_InputInstallPadSnapshots(const struct PlatformInputPadSnapshot *src, int count); diff --git a/platform/native_input.c b/platform/native_input.c index 479ae35c2..244d00e14 100644 --- a/platform/native_input.c +++ b/platform/native_input.c @@ -20,6 +20,19 @@ #define NATIVE_INPUT_MAP_FLAG_AXIS 0x4000 #define NATIVE_INPUT_MAP_FLAG_INVERSE 0x8000 #define NATIVE_INPUT_DEFAULT_KEYBOARD_SLOT 0 +// A DualShock reports two actuators. libpad's PadInfoAct(port, act, InfoActCurr) +// gives each one's current draw in the units PadMaxCurr (60) is expressed in; +// GAMEPAD_ProcessMotors budgets against exactly that. Retail's own comment puts +// one DualShock at 30 units for both motors together, and it always sheds the +// large motor first, so the large one carries the bigger share here. The split +// itself is the one number not documented in psx-spx or in any emulator (they +// implement the SIO protocol, not libpad). +#define NATIVE_INPUT_ACTUATOR_COUNT 2 +#define NATIVE_INPUT_ACTUATOR_CURR_SMALL 10 +#define NATIVE_INPUT_ACTUATOR_CURR_LARGE 20 +#define NATIVE_INPUT_INFO_ACT_CURR 4 +#define NATIVE_INPUT_RUMBLE_DURATION_MS 500 +#define NATIVE_INPUT_RUMBLE_REARM_MS 200 // NOTE(aalhendi): Little-endian tag `CTRI` = CTR native Input snapshot. #define NATIVE_INPUT_STATE_MAGIC 0x49525443 #define NATIVE_INPUT_STATE_VERSION 1 @@ -95,6 +108,11 @@ global_variable s32 s_controllerToSlotMapping[NATIVE_INPUT_MAX_CONTROLLERS] = {- global_variable struct NativeInputController s_controllers[NATIVE_INPUT_MAX_CONTROLLERS]; global_variable struct PlatformInputPadSnapshot s_installedSnapshots[NATIVE_INPUT_MAX_CONTROLLERS]; global_variable u8 *s_padSlotData[NATIVE_INPUT_PHYSICAL_SLOT_COUNT]; +global_variable const u8 *s_padActTable[NATIVE_INPUT_MAX_CONTROLLERS]; +global_variable s32 s_padActLen[NATIVE_INPUT_MAX_CONTROLLERS]; +global_variable u16 s_padRumbleLarge[NATIVE_INPUT_MAX_CONTROLLERS]; +global_variable u16 s_padRumbleSmall[NATIVE_INPUT_MAX_CONTROLLERS]; +global_variable u64 s_padRumbleSentMs[NATIVE_INPUT_MAX_CONTROLLERS]; global_variable const bool *s_keyboardState; global_variable s32 s_inputInitialized; global_variable s32 s_installedSnapshotsActive; @@ -634,6 +652,18 @@ internal s32 NativeInput_FindSlotForDeviceIndex(Sint32 deviceIndex) { s32 slot; + // SDL queues a GAMEPAD_ADDED event for every pad that was already plugged in + // when the subsystem starts, so a pad opened by NativeInput_OpenKnownControllers + // gets announced again on the first pump. Without this check it would be handed + // a second, still-free slot and drive two players at once. + for (slot = 0; slot < NATIVE_INPUT_MAX_CONTROLLERS; slot++) + { + if ((s_controllers[slot].controller != NULL) && (s_controllers[slot].instanceId == (SDL_JoystickID)deviceIndex)) + { + return slot; + } + } + for (slot = 0; slot < NATIVE_INPUT_MAX_CONTROLLERS; slot++) { if (s_controllerToSlotMapping[slot] == deviceIndex) @@ -672,6 +702,12 @@ internal void NativeInput_CloseController(s32 slot) controller->instanceId = -1; controller->analogEnabled = 0; controller->switchingAnalog = 0; + s_controllerToSlotMapping[slot] = -1; + s_padActTable[slot] = NULL; + s_padActLen[slot] = 0; + s_padRumbleLarge[slot] = 0; + s_padRumbleSmall[slot] = 0; + s_padRumbleSentMs[slot] = 0; if (s_lastActiveControllerSlot == slot) { @@ -708,8 +744,13 @@ internal void NativeInput_OpenController(SDL_JoystickID instanceId, s32 slot) joystick = SDL_GetGamepadJoystick(controller->controller); controller->instanceId = joystick != NULL ? SDL_GetJoystickID(joystick) : instanceId; - controller->analogEnabled = 1; + + // Real pads power up in digital mode with the analog LED off; the game + // switches them over itself through PadSetMainMode, and the player can + // toggle it by hand with Select+Start, standing in for the ANALOG button. + controller->analogEnabled = 0; controller->switchingAnalog = 0; + s_controllerToSlotMapping[slot] = (s32)controller->instanceId; NativeInput_MoveKeyboardOffControllerSlot(slot); } @@ -791,6 +832,80 @@ void Platform_InputShutdown(void) s_keyboardState = NULL; } +// port encoding is libpad's: bits 4-5 pick the console socket, bits 0-1 the +// multitap position inside it. +internal s32 NativeInput_SlotForPort(int port) +{ + s32 physicalSlot = (port >> 4) & 1; + s32 tap = port & 3; + s32 slot; + + if (NativeInput_UseMultitapBus() != 0) + { + if (physicalSlot != 0) + { + return -1; + } + slot = tap; + } + else + { + if (tap != 0) + { + return -1; + } + slot = physicalSlot; + } + + if ((slot < 0) || (slot >= NATIVE_INPUT_MAX_CONTROLLERS)) + { + return -1; + } + + return slot; +} + +// The MOT bytes ride along in every poll packet, so this runs once per frame +// off whatever buffer PadSetAct latched, not once per PadSetAct call. +// Byte 0 drives the small motor and is digital: only bit0 counts. Byte 1 is the +// large motor's speed, 00h..FFh. +internal void NativeInput_PushRumble(void) +{ + u64 now = SDL_GetTicks(); + s32 slot; + + for (slot = 0; slot < NATIVE_INPUT_MAX_CONTROLLERS; slot++) + { + struct NativeInputController *nativeController = &s_controllers[slot]; + const u8 *table = s_padActTable[slot]; + s32 len = s_padActLen[slot]; + u16 large = 0; + u16 small = 0; + + if (nativeController->controller == NULL) + { + continue; + } + + if ((table != NULL) && (len > 0)) + { + small = ((table[0] & 1) != 0) ? 0xffff : 0; + large = (len > 1) ? (u16)(table[1] * 257) : 0; + } + + if ((large == s_padRumbleLarge[slot]) && (small == s_padRumbleSmall[slot]) && + ((large == 0 && small == 0) || ((now - s_padRumbleSentMs[slot]) < NATIVE_INPUT_RUMBLE_REARM_MS))) + { + continue; + } + + s_padRumbleLarge[slot] = large; + s_padRumbleSmall[slot] = small; + s_padRumbleSentMs[slot] = now; + SDL_RumbleGamepad(nativeController->controller, large, small, NATIVE_INPUT_RUMBLE_DURATION_MS); + } +} + void Platform_InputUpdate(void) { u16 keyboardButtons; @@ -824,6 +939,7 @@ void Platform_InputUpdate(void) NativeInput_ApplyKeyboard(slot, keyboardButtons); } NativeInput_WritePadBus(); + NativeInput_PushRumble(); } void Platform_InputControllerAdded(int deviceIndex) @@ -891,33 +1007,66 @@ void Platform_InputPadInit(int slot, unsigned char *padData) int Platform_InputPadGetState(int port) { - s32 physicalSlot = (port >> 4) & 1; - s32 tap = port & 3; - s32 slot; + s32 slot = NativeInput_SlotForPort(port); - if (NativeInput_UseMultitapBus() != 0) + if (slot < 0) { - if (physicalSlot != 0) - { - return PadStateDiscon; - } - slot = tap; + return PadStateDiscon; } - else + + return s_controllers[slot].snapshot.connected ? PadStateStable : PadStateDiscon; +} + +// Pad command 44h "Set LED State": offs 0 puts the pad back in digital mode +// (LED off), offs 1 switches it to analog (LED red). Real pads power up in +// digital mode, so this is what actually lights up the sticks. +int Platform_InputPadSetMainMode(int port, int offs, int lock) +{ + s32 slot = NativeInput_SlotForPort(port); + + (void)lock; + + if (slot < 0) { - if (tap != 0) - { - return PadStateDiscon; - } - slot = physicalSlot; + return 0; } - if ((slot < 0) || (slot >= NATIVE_INPUT_MAX_CONTROLLERS)) + s_controllers[slot].analogEnabled = (offs != 0); + return 1; +} + +int Platform_InputPadInfoAct(int port, int acno, int term) +{ + s32 slot = NativeInput_SlotForPort(port); + s32 count; + + if (slot < 0) { - return PadStateDiscon; + return 0; } - return s_controllers[slot].snapshot.connected ? PadStateStable : PadStateDiscon; + // A pad the host cannot shake has no actuators to report, same as a plain + // digital pad on hardware. + count = (s_controllers[slot].controller != NULL) ? NATIVE_INPUT_ACTUATOR_COUNT : 0; + + if (acno < 0) + { + return count; + } + + if (acno >= count) + { + return 0; + } + + // Only InfoActCurr is ever asked for by the game; the remaining info bytes + // would need the pad's config-mode reply, which has no host equivalent. + if (term != NATIVE_INPUT_INFO_ACT_CURR) + { + return 0; + } + + return (acno == 0) ? NATIVE_INPUT_ACTUATOR_CURR_SMALL : NATIVE_INPUT_ACTUATOR_CURR_LARGE; } int Platform_InputCapturePadSnapshots(struct PlatformInputPadSnapshot *dst, int count) @@ -1054,55 +1203,18 @@ int Platform_InputRestoreState(const void *src, int srcSize) return 1; } +// PadSetAct only registers the buffer libpad keeps transmitting; the motors are +// driven from NativeInput_PushRumble, once per poll, like the real MOT bytes. void Platform_InputPadVibrate(int port, unsigned char *table, int len) { - s32 physicalSlot = (port >> 4) & 1; - s32 tap = port & 3; - s32 slot; - struct NativeInputController *controller; - u16 freqHigh; - u16 freqLow; - - if (NativeInput_UseMultitapBus() != 0) - { - if (physicalSlot != 0) - { - return; - } - slot = tap; - } - else - { - if (tap != 0) - { - return; - } - slot = physicalSlot; - } + s32 slot = NativeInput_SlotForPort(port); - if ((slot < 0) || (slot >= NATIVE_INPUT_MAX_CONTROLLERS) || (table == NULL) || (len <= 0)) - { - return; - } - - controller = &s_controllers[slot]; - if (controller->controller == NULL) + if (slot < 0) { return; } - freqHigh = table[0] * 255; - freqLow = len > 1 ? table[1] * 255 : 0; - - if ((freqLow != 0) && (freqLow < 4096)) - { - freqLow = 4096; - } - - if ((freqHigh != 0) && (freqHigh < 4096)) - { - freqHigh = 4096; - } - - SDL_RumbleGamepad(controller->controller, freqLow, freqHigh, 200); + s_padActTable[slot] = table; + s_padActLen[slot] = (table != NULL) ? len : 0; } + diff --git a/platform/native_libpad.c b/platform/native_libpad.c index 989de5848..7fcf8634e 100644 --- a/platform/native_libpad.c +++ b/platform/native_libpad.c @@ -29,12 +29,12 @@ int PadGetState(int port) return Platform_InputPadGetState(port); } +// Retail libpad (SCUS_944.26 0x80075be0): returns the actuator count when acno +// is negative, otherwise byte (term-1) of that actuator's 5-byte info record, +// or 0 for an out-of-range actuator/term. int PadInfoAct(int port, int acno, int term) { - (void)port; - (void)acno; - (void)term; - return 0; + return Platform_InputPadInfoAct(port, acno, term); } int PadSetActAlign(int port, unsigned char *table) @@ -44,14 +44,17 @@ int PadSetActAlign(int port, unsigned char *table) return 1; } +// Retail libpad (0x80075a40) queues pad command 44h "Set LED State" and returns +// 1 when the request was accepted, 0 when the port is busy. Returning 0 +// unconditionally used to stall GAMEPAD_ProcessState at gamepadType 0, which is +// why PadSetAct was never reached and rumble never ran. int PadSetMainMode(int socket, int offs, int lock) { - (void)socket; - (void)offs; - (void)lock; - return 0; + return Platform_InputPadSetMainMode(socket, offs, lock); } +// Retail libpad (0x80075ba0) only latches the pointer and the length; the two +// MOT bytes are then transmitted inside every poll packet, not once per call. void PadSetAct(int port, unsigned char *table, int len) { Platform_InputPadVibrate(port, table, len);