diff --git a/docs/dev/release-completion.md b/docs/dev/release-completion.md index 7188fd8ce..9151ca171 100644 --- a/docs/dev/release-completion.md +++ b/docs/dev/release-completion.md @@ -39,6 +39,7 @@ is `docs/dev/macos-moltenvk-decision.md`. ## Ready For Changelog +- [x] Traces against animated two-sided models now resolve joint weights through each generated surface's stable ID. Generated backfaces and reordered or omitted snapshot surfaces therefore map to their authored MD5/MD5R mesh instead of terminating the level with `NearestJoint: surfaceNum > meshes.Num()`. This resolves GitHub issue #151. - [x] Machinegun scope markings now follow the interpolated camera that is actually displayed, rather than the weapon's last 60 Hz simulation pose. Turning while zoomed therefore stays aligned at high refresh rates in both single-player and multiplayer, and the same presentation-state route is available to other weapon zoom GUIs. - [x] Asset parsing and GUI state handling now fail safely at several legacy edge cases. Default parser tokens initialize all metadata, lexer file loads reject impossible lengths, allocation failures, and short reads before touching the buffer, and GUI cursor/save values reject NaN or infinity. Relative input, absolute input, demos, and save restores now share one finite, aspect-aware cursor-bounds path instead of allowing invalid or off-canvas state to persist. - [x] Corrupt auxiliary data now stops at a safe boundary instead of leaking state into later rendering or restore work. Engine and GameLib Base64 paths agree on bounded allocation, null handling, whitespace, and invalid characters; GUI timelines reject malformed or overflowing timestamps and clean up failed scripts; GUI window saves validate writes, structure, identity, and finite layout values; and malformed GUI/decal demo payloads or undefined decal projections are rejected before they can produce invalid geometry. diff --git a/docs/dev/releases/v0.12.0.md b/docs/dev/releases/v0.12.0.md index d577329a7..8c5899169 100644 --- a/docs/dev/releases/v0.12.0.md +++ b/docs/dev/releases/v0.12.0.md @@ -4,6 +4,7 @@ - **Controller menu navigation no longer fights the mouse.** It now clears stale mouse highlights, hides the idle pointer, and traverses visible controls in both directions. Moving or clicking the mouse restores pointer control. - **Light-grid preload is now easy to find.** Players can reduce traversal upload stutter through Settings > Display > Post FX > Preload Light Grids. The option applies on the next map load and explains its loading-time and video-memory costs; the default remains off. +- **Animated two-sided models no longer end a level during a trace.** Generated backfaces now map to the same authored mesh as their front surface, preventing the `NearestJoint: surfaceNum > meshes.Num()` crash reported during the `airdefense1` intro. - **High-refresh gameplay looks smoother without changing game logic.** The first-person camera and weapon, skeletal animation, projectiles, eligible moving-world entities, lights, client effects, and moving-world attachments now sample a presentation pose between authoritative game ticks. Animated cockpits, held weapons, and joint-bound effects stay aligned with the pose actually drawn, including the lightning gun beam, while simulation, networking, collision, demos, and savegames retain their original timing. Multiplayer actor bodies remain on the network/simulation clock; this includes the local body and world-weapon stencil shadows visible in first person. - **Experimental temporal AA can keep the 3D scene steadier while resolution follows GPU load.** `r_temporalAA 1` enables native-history TAA/TAAU on OpenGL or Vulkan, and `r_rendererDynamicResolution 1` adds a delayed, non-blocking GPU-time controller. HUD and menus stay native-sized, camera cuts and captures start from clean history, unsupported moving effects are rejected conservatively, and the established SMAA path remains one setting away. @@ -73,6 +74,7 @@ - Added independent, default-off OpenGL/Vulkan froxel-volumetric, SSR, and SSGI leaves to the native scene presentation pass, bounded respectively to 16 view-depth slices, 16 depth-normal ray steps, and eight diffuse-GI taps. Effect-only use does not allocate temporal history, engine captures retain the result, and missing resources preserve the established presentation owner. - Added presentation-only interpolation for high-refresh rendering of eligible SP actors, skeletal animation, movers, first-person weapons, projectiles, lights, and client effects while preserving authoritative tick behavior; multiplayer actor bodies and their stencil shadows remain authoritative. - Aligned zoom-scope yaw with the interpolated camera actually presented in both single-player and multiplayer, preventing machinegun scope markings from stepping behind high-refresh view rotation. +- Resolved animated-model joint traces by passing stable generated-surface IDs to MD5 and MD5R source models, including the offset IDs used by generated backfaces and snapshots whose visible surface order differs from their authored mesh order. - Removed presentation interpolation work from non-presented cinematic fast-forward ticks and limited visible-frame pose sampling to active movers, their bounded physics-team members, and cleanup members, improving `airdefense1` loading/settle time and CPU frame pacing without changing its authoritative 60 Hz endpoint. - Avoided constructing bake/debug-only fallback light-grid layouts during ordinary play on maps without baked grids; bake commands still generate the required layout on demand. - Corrected OpenGL and Vulkan shadow-map caster coverage with topology-aware face culling, near-shell depth for sealed geometry, two-sided depth for open geometry, bounded huge-light point bias, tighter 9-tap balanced filters, consistent PCSS blocker sampling, exact world/pass/storage cache identity, bind-time atlas and point-cube provenance, live point-format and projected-atlas generation validation, newest-compatible stale reuse that rejects moved or rescaled point lights, dynamic and partial-map stencil supplements, translucent-only GLOBAL ownership, hardware atlas bounds, and safely nested OpenGL GPU timing. diff --git a/src/renderer/Model.cpp b/src/renderer/Model.cpp index 7472ac302..9f62a4006 100644 --- a/src/renderer/Model.cpp +++ b/src/renderer/Model.cpp @@ -1927,7 +1927,7 @@ const idJointQuat *idRenderModelStatic::GetDefaultPose( void ) const { idRenderModelStatic::NearestJoint ================ */ -int idRenderModelStatic::NearestJoint( int surfaceNum, int a, int b, int c ) const { +int idRenderModelStatic::NearestJoint( int surfaceId, int a, int b, int c ) const { return INVALID_JOINT; } diff --git a/src/renderer/Model_local.h b/src/renderer/Model_local.h index b82e7acb2..259a76373 100644 --- a/src/renderer/Model_local.h +++ b/src/renderer/Model_local.h @@ -154,7 +154,7 @@ class idRenderModelStatic : public idRenderModel { virtual jointHandle_t GetJointHandle( const char *name ) const; virtual const char * GetJointName( jointHandle_t handle ) const; virtual const idJointQuat * GetDefaultPose( void ) const; - virtual int NearestJoint( int surfaceNum, int a, int b, int c ) const; + virtual int NearestJoint( int surfaceId, int a, int b, int c ) const; virtual bool HasCollisionSurface( const struct renderEntity_s *ent ) const; virtual idBounds Bounds( const struct renderEntity_s *ent ) const; virtual void SetBounds( const idBounds &newBounds ) { bounds = newBounds; } @@ -286,7 +286,7 @@ class idRenderModelMD5 : public idRenderModelStatic { virtual const char * GetJointName( jointHandle_t handle ) const; virtual const idJointQuat * GetDefaultPose( void ) const; virtual const idJointMat * GetSkinSpaceToLocalMats( void ) const; - virtual int NearestJoint( int surfaceNum, int a, int b, int c ) const; + virtual int NearestJoint( int surfaceId, int a, int b, int c ) const; virtual int GetSurfaceMask( const char *surface ) const; virtual renderModelCacheType_t LevelLoadCachePayloadType() const; virtual bool WriteLevelLoadCachePayload( idFile &file ) const; @@ -569,7 +569,7 @@ class rvRenderModelMD5R : public idRenderModelStatic { virtual const char * GetJointName( jointHandle_t handle ) const; virtual const idJointQuat * GetDefaultPose( void ) const; virtual const idJointMat * GetSkinSpaceToLocalMats( void ) const; - virtual int NearestJoint( int surfaceNum, int a, int b, int c ) const; + virtual int NearestJoint( int surfaceId, int a, int b, int c ) const; virtual int GetSurfaceMask( const char *surface ) const; virtual int Memory() const; virtual renderModelCacheType_t LevelLoadCachePayloadType() const; diff --git a/src/renderer/Model_md5.cpp b/src/renderer/Model_md5.cpp index aefa9158b..e9c2e04ff 100644 --- a/src/renderer/Model_md5.cpp +++ b/src/renderer/Model_md5.cpp @@ -2002,20 +2002,20 @@ const char *idRenderModelMD5::GetJointName( jointHandle_t handle ) const { idRenderModelMD5::NearestJoint ==================== */ -int idRenderModelMD5::NearestJoint( int surfaceNum, int a, int b, int c ) const { - int i; - const idMD5Mesh *mesh; - - if ( surfaceNum > meshes.Num() ) { - common->Error( "idRenderModelMD5::NearestJoint: surfaceNum > meshes.Num()" ); +int idRenderModelMD5::NearestJoint( int surfaceId, int a, int b, int c ) const { + // Dynamic snapshots retain the source mesh number as their stable surface ID. + // Two-sided materials add a reversed surface with the same vertices and an + // offset ID, so both generated surfaces resolve to the same authored mesh. + int meshIndex = surfaceId; + if ( meshIndex >= MD5_BackSideSurfaceIdOffset ) { + meshIndex -= MD5_BackSideSurfaceIdOffset; } - for ( mesh = meshes.Ptr(), i = 0; i < meshes.Num(); i++, mesh++ ) { - if ( mesh->surfaceNum == surfaceNum ) { - return mesh->NearestJoint( a, b, c ); - } + if ( meshIndex < 0 || meshIndex >= meshes.Num() ) { + return 0; } - return 0; + + return meshes[ meshIndex ].NearestJoint( a, b, c ); } /* diff --git a/src/renderer/Model_md5r.cpp b/src/renderer/Model_md5r.cpp index 766f7b1d1..bbaaf0f02 100644 --- a/src/renderer/Model_md5r.cpp +++ b/src/renderer/Model_md5r.cpp @@ -6114,16 +6114,13 @@ const idJointMat *rvRenderModelMD5R::GetSkinSpaceToLocalMats() const { rvRenderModelMD5R::NearestJoint ======================== */ -int rvRenderModelMD5R::NearestJoint( int surfaceNum, int a, int b, int c ) const { - if ( surfaceNum > meshes.Num() ) { - common->Error( "rvRenderModelMD5R::NearestJoint: surfaceNum > meshes.Num()" ); - } - +int rvRenderModelMD5R::NearestJoint( int surfaceId, int a, int b, int c ) const { const idList &vertexBuffers = GetVertexBuffers(); for ( int meshIndex = 0; meshIndex < meshes.Num(); ++meshIndex ) { const rvMD5RMesh &mesh = meshes[ meshIndex ]; - if ( mesh.surfaceNum != surfaceNum ) { + if ( surfaceId != mesh.meshIdentifier + && surfaceId != mesh.meshIdentifier + MD5R_BackSideSurfaceIdOffset ) { continue; } diff --git a/src/renderer/RenderWorld.cpp b/src/renderer/RenderWorld.cpp index d41244132..0be8a1eb5 100644 --- a/src/renderer/RenderWorld.cpp +++ b/src/renderer/RenderWorld.cpp @@ -1601,7 +1601,7 @@ bool idRenderWorldLocal::ModelTrace( modelTrace_t &trace, qhandle_t entityHandle // jmarshall trace.materialType = R_GetMaterialTypeForTrace( trace.material, surf->geometry, localTrace ); // jmarshall end - trace.jointNumber = refEnt->hModel->NearestJoint( i, localTrace.indexes[0], localTrace.indexes[1], localTrace.indexes[2] ); + trace.jointNumber = refEnt->hModel->NearestJoint( surf->id, localTrace.indexes[0], localTrace.indexes[1], localTrace.indexes[2] ); } } @@ -1759,8 +1759,9 @@ bool idRenderWorldLocal::Trace( modelTrace_t &trace, const idVec3 &start, const trace.material = shader; trace.materialType = R_GetMaterialTypeForTrace( trace.material, surf->geometry, localTrace ); trace.entity = &def->parms; - // Dynamic snapshots don't own joint-weight metadata; ask the source model. - trace.jointNumber = def->parms.hModel->NearestJoint( j, localTrace.indexes[0], localTrace.indexes[1], localTrace.indexes[2] ); + // Dynamic snapshots don't own joint-weight metadata; use their stable + // surface ID to ask the source model for the matching authored mesh. + trace.jointNumber = def->parms.hModel->NearestJoint( surf->id, localTrace.indexes[0], localTrace.indexes[1], localTrace.indexes[2] ); traceBounds.Clear(); traceBounds.AddPoint( start );