diff --git a/CMakeLists.txt b/CMakeLists.txt index c740d201..618486ca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,12 @@ else() set(PORT_ADDR_BASE 0x0 CACHE STRING "Host base of the N64 address window") endif() +# Debug gate: validate every u32 -> host-pointer conversion against the regions +# the port actually maps, and report offenders at the conversion site with a +# backtrace. Off by default (zero cost); enable with -DPORT_ADDR_STRICT=ON. +# Catches the D299-D303 truncated/never-rebased-pointer family early. +option(PORT_ADDR_STRICT "Validate N64->host address conversions at runtime" OFF) + include(TargetArch) include(FindPkgConfig) @@ -259,6 +265,10 @@ add_definitions( -DPORT_ADDR_BASE=${PORT_ADDR_BASE} ${REGION_DEFS} ) +if(PORT_ADDR_STRICT) + add_definitions(-DPORT_ADDR_STRICT=1) + message(STATUS "PORT_ADDR_STRICT enabled: N64->host conversions are validated at runtime") +endif() if(TARGET_IS_64BIT) add_definitions(-DPLATFORM_64BIT=1) endif() diff --git a/docs/building.md b/docs/building.md index 6d12f91a..694a7e05 100644 --- a/docs/building.md +++ b/docs/building.md @@ -187,6 +187,32 @@ runtime. `ge007.ini` is written under `data/` on first launch. On macOS the binary is `./build-pc/ge007.aarch64` (Apple Silicon) or `./build-pc/ge007.x86_64` (Intel). +### Debugging address bugs (`PORT_ADDR_STRICT`) + +The port realises the N64 address space at a host base (`PORT_ADDR_BASE`, +non-zero only on arm64 macOS). The recurring failure mode is a 32-bit value +that is *not* a valid N64 address reaching `portN64ToHost()` — a truncated host +pointer, or one that was never re-based. The resulting fault usually happens +far from the cause, and sometimes there is no fault at all, just silent +corruption. + +```sh +cmake -S . -B build-strict -DROMID=ntsc-final -DPORT_ADDR_STRICT=ON +cmake --build build-strict -j +``` + +Every conversion is then validated against the mapped regions and offenders are +reported at the conversion site with a backtrace naming the caller. It is +**off by default and compiled out entirely**, so normal builds pay nothing. + +To prove the detector is live rather than merely silent: + +```sh +GE_ADDRSTRICT_SELFTEST=1 ./build-strict/ge007.aarch64 -level_09 +``` + +See findings D299–D305 for the bug family this exists to catch. + ### Packaging a macOS app ```sh diff --git a/docs/dev/GE-ENV-PROBES.md b/docs/dev/GE-ENV-PROBES.md index 90bdaccf..1b4c18d5 100644 --- a/docs/dev/GE-ENV-PROBES.md +++ b/docs/dev/GE-ENV-PROBES.md @@ -130,6 +130,7 @@ Two classes: | `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) | | `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) | | `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) | +| `GE_ADDRSTRICT_SELFTEST` | `port/src/port_addr.c` (`portAddrStrictSelfTest`) | **Negative self-test for `PORT_ADDR_STRICT` (D305).** Feeds `portN64ToHost()` a deliberately-unmapped address (`0x30000000`) at boot so a run can prove the validator is live rather than merely silent. Requires a build configured with `-DPORT_ADDR_STRICT=ON`; a no-op otherwise. | **live** (D305) | | `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") | | `GE_D305` | `src/snd.c:420` (`sndHandleEvent` desync branch, `#ifdef PORT`) | D305: logs the sound-preemption desync state when it is detected (after the NULL-guard fix made the crash itself impossible) — confirms whether `Game.AllUnlocked` correlates or was coincidental, and whether it recurs. Inert unset. | **live** (D305 monitoring) | | `GE_D309` | `src/game/chrai.c:810,3424`, `src/game/chraction.c:10139` (all `#ifdef PORT`) | D309 freeze probe: logs AI label-not-found restarts (`chrai.c`) and the run-to-bond persistent-list step-on path (`chraction.c`, capped 400 lines) — the diagnostics used to root-cause the elusive complete freeze (D309/D310). Inert unset. | **live** (D309/D310 fixed — kept for recurrence) | diff --git a/port/fast3d/gfx_opengl.cpp b/port/fast3d/gfx_opengl.cpp index c3881e12..9c433d56 100644 --- a/port/fast3d/gfx_opengl.cpp +++ b/port/fast3d/gfx_opengl.cpp @@ -1222,11 +1222,44 @@ static int gfx_opengl_create_framebuffer() { return i; } +/* video.c documents MSAA as "snaps down to the highest supported level", but + * nothing ever queried the limit: gfx_msaa_level went to + * glRenderbufferStorageMultisample() verbatim, and a request above + * GL_MAX_SAMPLES leaves the renderbuffer unallocated -> incomplete FBO -> + * a completely BLACK screen (MSAA=8 on Apple Silicon, where GL_MAX_SAMPLES + * is 4). Clamp here, where the GL limit actually matters. */ +static uint32_t gfx_opengl_clamp_msaa(uint32_t level) +{ + static GLint max_samples = 0; + static uint32_t warned_level = 0; + + if (level <= 1) { + return 1; + } + if (max_samples == 0) { + glGetIntegerv(GL_MAX_SAMPLES, &max_samples); + if (max_samples < 1) { + max_samples = 1; + } + } + if ((GLint)level > max_samples) { + if (warned_level != level) { + sysLogPrintf(LOG_WARNING, + "MSAA: %u samples requested but GL_MAX_SAMPLES is %d; using %d", + level, (int)max_samples, (int)max_samples); + warned_level = level; + } + return (uint32_t)max_samples; + } + return level; +} + static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, uint32_t height, uint32_t msaa_level, bool opengl_invert_y, bool render_target, bool has_depth_buffer, bool can_extract_depth) { Framebuffer& fb = framebuffers[fb_id]; + msaa_level = gfx_opengl_clamp_msaa(msaa_level); width = max(width, 1U); height = max(height, 1U); @@ -1340,7 +1373,12 @@ void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) { glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo); glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT, GL_NEAREST); - /* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */ + /* Restore whatever was bound before the resolve. `current_framebuffer` is + * an INDEX into framebuffers[], NOT a GL framebuffer name -- binding it + * directly named an unrelated GL object (or nothing) and left the wrong + * target bound for the rest of the frame. With MSAA on that produced a + * completely black screen. Use the same fb_id==0 -> GL name 0 rule as + * gfx_opengl_bind_framebuffer above. */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glEnable(GL_SCISSOR_TEST); } @@ -1405,6 +1443,10 @@ void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST); + /* Same index-vs-name bug as the MSAA resolve above: for current_framebuffer + * == 0 this bound framebuffers[0].fbo, which is a generated but + * attachment-less FBO (incomplete on a strict GL implementation) -- the + * window's default framebuffer is GL name 0. */ glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo); glReadBuffer(GL_BACK); diff --git a/port/include/port_addr.h b/port/include/port_addr.h index 25f668e3..2c58e2e6 100644 --- a/port/include/port_addr.h +++ b/port/include/port_addr.h @@ -71,6 +71,17 @@ extern "C" { extern uintptr_t g_portImageBase; extern int g_portUseImageRel; +#ifdef PORT_ADDR_STRICT +/* Debug gate (-DPORT_ADDR_STRICT=1). Validates that a u32 being turned into a + * host pointer actually names one of the N64 regions the port maps. The whole + * D299/D300/D301/D302 bug family shares one signature -- a truncated or + * never-rebased address reaching portN64ToHost -- and it is almost always + * diagnosed from a fault somewhere far away, long after the bad value was + * created. This reports it AT THE CONVERSION, with a backtrace naming the + * caller. Compiled out entirely by default; costs one call when enabled. */ +void portAddrStrictCheck(uint32_t a); +#endif + /* Reserve the window (macOS only) and record the image base. Must run before * any of the port's fixed mappings and before large allocations. */ void portAddrInit(void); @@ -79,9 +90,25 @@ void portAddrInit(void); } #endif -/* u32 N64/physical address -> live host pointer. */ +/* u32 N64/physical address -> live host pointer. + * + * N64 address 0 maps to NULL, NOT to PORT_ADDR_BASE. Offset 0 is never a + * mapped address (the window's lowest live region is the cart at + * 0x1000_0000), so base+0 lands in the reserved PROT_NONE area and faults. + * More importantly, game code routinely signals failure by returning an N64 + * address of 0 and then testing the converted pointer for NULL -- + * `p = PORT_N64PTR(T, f()); if (p != NULL) *p = ...` (e.g. + * chrCreateBloodStain(), objDeform()). With base+0 that test passes and the + * very next store faults (D301). Returning NULL restores the decomp's + * intended semantics. */ static inline void *portN64ToHost(uint32_t a) { + if (a == 0) { + return NULL; + } +#ifdef PORT_ADDR_STRICT + portAddrStrictCheck(a); +#endif if (g_portUseImageRel && a >= 0x40000000u && a < 0x70000000u) { return (void *)(g_portImageBase + (uintptr_t)(a - 0x40000000u)); } diff --git a/port/src/port_addr.c b/port/src/port_addr.c index 3ea1ad26..e3a80740 100644 --- a/port/src/port_addr.c +++ b/port/src/port_addr.c @@ -12,10 +12,14 @@ */ #include +#include #include "platform.h" #include "system.h" #include "port_addr.h" +#ifdef PORT_ADDR_STRICT +#include +#endif #if defined(PLATFORM_MACOS) && defined(PLATFORM_ARM) #include @@ -64,6 +68,87 @@ static void portAddrSelfTest(void) ok ? "ALL PASS" : "FAILURES"); } +#ifdef PORT_ADDR_STRICT +/* + * Strict address validation (-DPORT_ADDR_STRICT=1, off by default). + * + * Every bug in the D299-D303 family reaches portN64ToHost() holding a value + * that is not a real N64 address -- either a host pointer truncated to 32 bits + * or an address that was never re-based. The fault then happens later, in + * unrelated code, which is what made these expensive to find. This checks the + * value against the regions the port actually maps and reports the bad ones + * at the point of conversion, naming the caller. + * + * Regions (see port/include/port_addr.h and port/src/dram.c): + * 0x10000000..0x1fffffff cart image + pcmodels/pccg sidecars + * 0x40000000..0x6fffffff image-relative encoding (exe globals) + * 0x70000000..0x707fffff DRAM V1 + * 0x80000000..0x807fffff DRAM V2 (KSEG0 mirror of V1) + * 0xa0000000..0xbfffffff game-thread stacks + * Segmented DL addresses (0x00000000..0x0fffffff) never name host memory and + * are resolved by fast3d's seg_addr(), not here -- so they are reported too. + */ +static int portAddrRegionOk(uint32_t a) +{ + if (a >= 0x10000000u && a <= 0x1fffffffu) return 1; /* cart + sidecars */ + if (a >= 0x40000000u && a <= 0x6fffffffu) return 1; /* image-relative */ + if (a >= 0x70000000u && a <= 0x707fffffu) return 1; /* DRAM V1 */ + if (a >= 0x80000000u && a <= 0x807fffffu) return 1; /* DRAM V2 */ + if (a >= 0xa0000000u && a <= 0xbfffffffu) return 1; /* thread stacks */ + return 0; +} + +/* Negative self-test: GE_ADDRSTRICT_SELFTEST=1 feeds the validator a value + * that is deliberately not a mapped N64 address, so a run can prove the + * detector is actually live rather than merely silent. */ +void portAddrStrictSelfTest(void) +{ + if (getenv("GE_ADDRSTRICT_SELFTEST")) { + sysLogPrintf(LOG_INFO, "PORT_ADDR_STRICT: self-test, expect one report below"); + portAddrStrictCheck(0x30000000u); /* in the gap between cart and image-rel */ + } +} + +void portAddrStrictCheck(uint32_t a) +{ + /* Report each distinct bad value once -- a bad pointer in a per-frame path + * would otherwise emit thousands of identical lines and bury the first. */ + enum { SEEN_MAX = 64 }; + static uint32_t seen[SEEN_MAX]; + static int seenCount; + int i; + + if (portAddrRegionOk(a)) { + return; + } + + for (i = 0; i < seenCount; i++) { + if (seen[i] == a) { + return; + } + } + if (seenCount < SEEN_MAX) { + seen[seenCount++] = a; + } + + sysLogPrintf(LOG_ERROR, + "PORT_ADDR_STRICT: 0x%08x is not a mapped N64 address " + "(-> %p). Truncated host pointer, or an address that was " + "never re-based (D299-D303 class).", + (unsigned)a, (void *)((uintptr_t)PORT_ADDR_BASE + (uintptr_t)a)); +#if defined(PLATFORM_MACOS) || defined(__APPLE__) || defined(__linux__) + { + void *bt[24]; + int n = backtrace(bt, (int)(sizeof(bt) / sizeof(bt[0]))); + /* Skip frame 0 (this function); the caller is what matters. */ + if (n > 1) { + backtrace_symbols_fd(bt + 1, n - 1, 2); + } + } +#endif +} +#endif /* PORT_ADDR_STRICT */ + void portAddrInit(void) { g_portImageBase = sysImageBase(); @@ -107,4 +192,8 @@ void portAddrInit(void) (unsigned long long)g_portImageBase, g_portUseImageRel); portAddrSelfTest(); + +#ifdef PORT_ADDR_STRICT + portAddrStrictSelfTest(); +#endif } diff --git a/src/bondtypes.h b/src/bondtypes.h index c8283e37..46ffb1ba 100644 --- a/src/bondtypes.h +++ b/src/bondtypes.h @@ -745,11 +745,17 @@ typedef union /* PC port (D43/D45): stored as a raw vma (0x05xxxxxx) instead of a real * pointer so sizeof(Vertex) stays 16 bytes — the gSPVertex stride is * hardcoded to 16B and propobj.c copies vertices with sizeof(Vertex). - * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32; chr.c casts - * back to ModelNode* at the use site. N64 layout unchanged (4B either way). */ + * modelPromoteNodeOffsetsToPointers rebases it via PROMOTE32, which is + * `var = (u32)((u32)var + diff)` — note that unlike PROMOTE it keeps the + * field a 32-bit N64 ADDRESS, it does NOT produce a host pointer. + * D301: consumers must therefore re-base it (PORT_N64PTR), never cast it + * straight to a pointer — a bare cast leaves 0x7070_xxxx and faults on + * arm64, where the window is based at PORT_ADDR_BASE. + * N64 layout unchanged (4B either way). */ u32 LinkedTo; - /* Collision vertices only: points to related model node (raw vma). */ + /* Collision vertices only: related model node, as a raw vma — same union + * storage as LinkedTo above, so the same PORT_N64PTR rule applies. */ u32 CollisionRelatedNode; /* 0x8 */ #else struct Vertex *LinkedTo; diff --git a/src/game/bg.c b/src/game/bg.c index 5373b102..389e7a85 100644 --- a/src/game/bg.c +++ b/src/game/bg.c @@ -5289,7 +5289,20 @@ void bgRoomCalcBB(s32 room) // canonical name limits.maxY = -0x7fff; limits.maxZ = -0x7fff; +#ifdef PORT + /* D302: the induction variable is a full host pointer, so truncating the + * bound to (s32) makes the comparison false on the very first iteration + * once the window is based at PORT_ADDR_BASE -- the loop body never runs, + * `limits` keeps its sentinel init, and every room ends up with an + * INVERTED bounding box (min = pos + 0x7fff, max = pos - 0x7fff) below. + * Silent: no crash, just wrong culling/visibility for every room of every + * level. Byte arithmetic must be preserved (usize_point_index_binary is a + * byte count), so widen the cast rather than doing pointer arithmetic. + * Mirrors the same fix already applied at bg.c:3399. */ + for (; vertices < (Vtx *) ((uintptr_t) g_BgRoomInfo[room].vertices + g_BgRoomInfo[room].usize_point_index_binary); vertices++) +#else for (; vertices < (Vtx *) ((s32) g_BgRoomInfo[room].vertices + g_BgRoomInfo[room].usize_point_index_binary); vertices++) +#endif { for (j = 0; j < 3; j++) { diff --git a/src/game/bondview2.c b/src/game/bondview2.c index 50dbd487..678be949 100644 --- a/src/game/bondview2.c +++ b/src/game/bondview2.c @@ -8236,7 +8236,7 @@ void bondviewMovePlayerUpdateViewport(s8 stick_x, s8 stick_y, u16 buttons) if ((cameraBufferToggle != 0) && (viGetFrameBuf2() == (u8*)(cfb_16[1]))) { - viSetFrameBuf2((u8 *) resolution); + viSetFrameBuf2(PORT_N64PTR(u8, resolution)); } #ifdef VERSION_EU @@ -8967,7 +8967,17 @@ void mp_respawn_handler(void) { coord3d start_pos = ZeroCoordSpawnPos; f32 start_look_angle; +#ifdef PORT + /* D302: PadRecord.stan is a StandTile* host pointer. An s32 local + * truncates it, and every consumer below (bondviewYPositionRelated, + * change_player_pos_to_target, prop->stan) rebuilds it unbased and + * dereferences it. Local only -- no struct/ABI impact -- and the s32 pad + * below keeps the N64 stack layout identical. The solo twin + * (bondview_r.c:102) already declares this StandTile*. */ + StandTile *start_stan; +#else s32 start_stan; +#endif s32 pad; f32 stan_height; s32 var_v0; @@ -9059,7 +9069,7 @@ void mp_respawn_handler(void) switch (intro_record->type) { case 0: // INTROTYPE_SPAWN - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSpawn)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSpawn)); break; case 1: // INTROTYPE_ITEM if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) { @@ -9069,28 +9079,28 @@ void mp_respawn_handler(void) bondinvAddInvItem(((struct SetupIntroItem*)intro_record)->item_right); } } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroItem)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroItem)); break; case 2: // INTROTYPE_AMMO if (check_ramrom_flags() == ((struct SetupIntroAmmo*)intro_record)->is_demo_playback) { give_cur_player_ammo(((struct SetupIntroAmmo*)intro_record)->ammo_type, ((struct SetupIntroAmmo*)intro_record)->ammo_amount); } - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAmmo)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAmmo)); break; case 3: // INTROTYPE_SWIRL - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroSwirl)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroSwirl)); break; case 4: // INTROTYPE_ANIM - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroAnim)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroAnim)); break; case 5: // INTROTYPE_CUFF - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCuff)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCuff)); break; case 6: // INTROTYPE_CAMERA - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroCamera)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroCamera)); break; default: // INTROTYPE_WATCH, INTROTYPE_CREDITS - intro_record = (struct SetupIntroEmpty*)((s32)intro_record + sizeof(struct SetupIntroEmpty)); + intro_record = (struct SetupIntroEmpty*)((uintptr_t)intro_record + sizeof(struct SetupIntroEmpty)); break; } #ifdef DEBUG @@ -11188,7 +11198,7 @@ join_768: if (ppointers[index]->bodyModel->anim2 == NULL) { startframe = (0.0f <= frame) ? (frame) : (0.0f); - modelSetAnimation(ppointers[index]->bodyModel, (ModelAnimation *) anim, 0, startframe, angle, 16.0f); + modelSetAnimation(ppointers[index]->bodyModel, PORT_N64PTR(ModelAnimation, anim), 0, startframe, angle, 16.0f); ppointers[index]->players_cur_animation = anim; ppointers[index]->field_1288 = angle; diff --git a/src/game/chr.c b/src/game/chr.c index 606b652a..af2a01b7 100644 --- a/src/game/chr.c +++ b/src/game/chr.c @@ -3584,10 +3584,15 @@ after_opcode: } #ifdef PORT - /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. */ - relatednode = (ModelNode *)(uintptr_t)((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode; + /* PC port (D43/D45): CollisionRelatedNode is a raw vma (u32), not a pointer. + * D301: that vma is an N64 address (the port stores window pointers as + * 32-bit values, and PORT_ADDR_BASE is 4 GiB-aligned, so the low 32 bits + * ARE the N64 address), so it must be re-based like every other + * address-carrying field -- a bare cast leaves it as 0x7070_xxxx and the + * &relatednode->Data deref below faults. Test the re-based pointer. */ + relatednode = PORT_N64PTR(ModelNode, ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode); - if (((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode != 0) + if (relatednode != NULL) #else relatednode = (ModelNode *) ((ModelRoData_DisplayList_CollisionRecord *) node)->CollisionVertices[bestindex].CollisionRelatedNode; diff --git a/src/game/cleanup_objects.c b/src/game/cleanup_objects.c index e5016467..ce3342c4 100644 --- a/src/game/cleanup_objects.c +++ b/src/game/cleanup_objects.c @@ -23,6 +23,7 @@ void cleanupObjects(s32 stage) { +#ifdef PORT /* D300: the decomp's `(u32)g_CurrentSetup.propDefs` converts the pointer * to a 32-bit integer and back into a `u32 *` -- a no-op where pointers * are 32-bit (N64), but on the 64-bit port it TRUNCATES the host pointer @@ -32,6 +33,9 @@ void cleanupObjects(s32 stage) * pointer-to-pointer and keep `u32 *` for sizepropdef()'s 4-byte-unit * increment. Same class as D3x; semantics unchanged. */ u32 *obj = (u32 *)g_CurrentSetup.propDefs; +#else + u32 *obj = (u32)g_CurrentSetup.propDefs; +#endif if (obj) { diff --git a/src/game/image.c b/src/game/image.c index 44a1b869..c00ad6bd 100644 --- a/src/game/image.c +++ b/src/game/image.c @@ -1739,9 +1739,22 @@ void texReadAlphaBits(u8 *image,s32 count) */ s32 texReadUncompressed(u8 *dst, s32 width, s32 height, s32 format) { +#ifdef PORT + /* D303: aligning a host pointer through u32 truncates it -- on arm64 dst is + * ~0x1000_70xx_xxxx and these would become unbased 0x70xx_xxxx, then be + * written through. Align at full pointer width instead. Identical result + * where pointers are 32-bit. NOTE: currently unreachable on the PC port -- + * textures come from the pre-converted pccg sidecars, and a GE_D303 probe + * over a full level boot recorded zero calls -- so this is latent, fixed + * defensively rather than in response to an observed fault. */ + u32 *dst32 = (u32 *)(((uintptr_t)dst + 0xf) & ~(uintptr_t)0xf); + u16 *dst16 = (u16 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7); + u8 *dst8 = (u8 *)(((uintptr_t)dst + 7) & ~(uintptr_t)7); +#else u32 *dst32 = (u32 *)(((u32)dst + 0xf) & ~0xf); u16 *dst16 = (u16 *)(((u32)dst + 7) & ~7); u8 *dst8 = (u8 *)(((u32)dst + 7) & ~7); +#endif s32 x; s32 y; diff --git a/src/game/model.c b/src/game/model.c index f4dd7b27..12b63583 100644 --- a/src/game/model.c +++ b/src/game/model.c @@ -1924,18 +1924,18 @@ void process_03_unknown(ModelRenderData *renderData, Model *model, ModelNode *no jointIndex = rodata->JointID; skeleton = model->obj->Skeleton; - angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk34); + angle = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk34)); if (model->unk2c != 0.0f) { - tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, (u8 *)model->unk38); + tmp = sub_GAME_7F06E540(jointIndex, model->gunhand, skeleton, model->anim, PORT_N64PTR(u8, model->unk38)); angle = sub_GAME_7F06D0CC(angle, tmp, model->unk2c); } if (model->unk84 != 0.0f) { - tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk64); + tmp = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk64)); if (model->unk5c != 0.0f) { - tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, (u8 *)model->unk68); + tmp2 = sub_GAME_7F06E540(jointIndex, model->unk25, skeleton, model->anim2, PORT_N64PTR(u8, model->unk68)); tmp = sub_GAME_7F06D0CC(tmp, tmp2, model->unk5c); } diff --git a/src/game/options.c b/src/game/options.c index 98bdd938..9552b6b2 100644 --- a/src/game/options.c +++ b/src/game/options.c @@ -3490,7 +3490,7 @@ Gfx *draw_watch_controller(Gfx *gdl) } else { - gdl = watchRenderControllerOpaque(gdl, &finalmtx, 1, (s32) (&table2), &contpadnum1); + gdl = watchRenderControllerOpaque(gdl, &finalmtx, 1, (WatchContButtonPositions *) (&table2), &contpadnum1); } }