fix(render) #443: private viewports take the ring transform path
The paperdoll was visible only in portal space. Root cause: the classic WbDrawDispatcher.Draw path appended its transforms into the SHARED world transform frame (WorldTransformFrameArena.Append) with a non-zero base instance, but the default mesh shaders index every parallel per-instance array - clip slots, light sets, indoor, OPACITY, selection lighting, detail category - zero-based; only the packed world submission's shader convention subtracts the shared-arena prefix. With a world frame active the doll drew all instances at per-instance opacity 0 into a cleared target: counted draws, blank pixels, deterministic. Portal space worked because no world transform frame is active there, so the same code took the ring path with base 0. The private viewports are the only production consumers of the classic path, hiding the defect everywhere else. Fix: WbDrawDispatcher.NextClassicDrawIsPrivatePass - the private viewport renderer marks its draw and WriteWorldTransformSection routes private passes onto the plain ring path unconditionally (self-contained render state: the private pass owns its own camera, lighting, and target, and must not depend on the world frame's pose address space). Also landed, each independently justified: - Per-GPU-flight-slot private targets (PrivateViewportFlightTargets), restoring the pre-f6fe0f2a design: that revert's claim that frame submission order protects the single target's write->sample transition is not guaranteed across Vulkan command buffers. Per-slot completed scenes fix the cleared-sibling-after-reveal wart the old attempt had. - Paperdoll resource preparation moved to the frame resource phase (IPrivateEntityViewportResourcePreparation) before world draws consume the bounded composite-upload budget. - The presenter redresses on every dirty edge (an appearance-equal clone can pin retired readiness across generations; the renderer's two-phase promote keeps the last completed image visible during replacement), publishes only non-zero handles, and clears the viewport exactly once at the explicit character-session boundary. Verified live on the clean build: doll visible in the NORMAL world, visible through portal space, and still visible after arrival - the exact reported repro cycle. 26 paperdoll/private-viewport/preparation tests plus 60 renderer-suite tests pass; owner visual gate pending. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc30285fd7
commit
cd1cdee0e5
9 changed files with 439 additions and 209 deletions
|
|
@ -24,6 +24,8 @@ internal interface IPaperdollFrameView
|
|||
bool TryGetVisibleSize(out int width, out int height);
|
||||
|
||||
void SetTextureHandle(uint textureHandle);
|
||||
|
||||
void ClearTextureHandle();
|
||||
}
|
||||
|
||||
internal interface IPaperdollInventoryVisibility
|
||||
|
|
@ -51,7 +53,9 @@ internal interface IPaperdollPoseApplicator
|
|||
/// presentation edge. The renderer remains a borrowed resource disposed by
|
||||
/// the existing window shutdown transaction.
|
||||
/// </summary>
|
||||
internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
||||
internal sealed class PaperdollFramePresenter :
|
||||
IPrivateEntityViewportFrame,
|
||||
IPrivateEntityViewportResourcePreparation
|
||||
{
|
||||
private readonly IPaperdollDollRenderer _renderer;
|
||||
private readonly IPaperdollFrameView _view;
|
||||
|
|
@ -73,22 +77,30 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
|
||||
public void MarkDirty() => _dirty = true;
|
||||
|
||||
public void Render()
|
||||
/// <summary>
|
||||
/// Pre-world resource phase: rebuild/redress the private clone and advance
|
||||
/// its mesh/composite readiness BEFORE world draws can consume the
|
||||
/// bounded per-frame composite-upload budget. In a dense scene the late
|
||||
/// presentation phase never wins that budget, which is why the doll was
|
||||
/// visible only while portal space quiesced the world (#443).
|
||||
/// </summary>
|
||||
public void PrepareResources()
|
||||
{
|
||||
if (_dirty)
|
||||
{
|
||||
if (_factory.TryBuild(out WorldEntity? doll))
|
||||
{
|
||||
// Same-generation CreateObject refreshes can repeat the exact
|
||||
// player ObjDesc at a portal boundary. Retail redresses its
|
||||
// private inventory object in place; releasing and reacquiring
|
||||
// an identical synthetic owner briefly blanks the viewport and
|
||||
// churns its texture composites.
|
||||
if (!HasEquivalentAppearance(_doll, doll))
|
||||
{
|
||||
_renderer.SetDoll(doll);
|
||||
_doll = doll;
|
||||
}
|
||||
// Redress every accepted live-player refresh, including an
|
||||
// appearance-equivalent ObjDesc after portal/relogin. The
|
||||
// private clone belongs to the current presentation
|
||||
// generation; retaining the old object merely because its
|
||||
// pixels compare equal can leave it attached to retired
|
||||
// mesh/composite readiness that never completes again.
|
||||
// PrivateEntityViewportRenderer promotes replacements in two
|
||||
// phases, so the last completed target remains visible until
|
||||
// this fresh clone is completely drawable.
|
||||
_renderer.SetDoll(doll);
|
||||
_doll = doll;
|
||||
_dirty = false;
|
||||
}
|
||||
else
|
||||
|
|
@ -96,16 +108,25 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
// gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its
|
||||
// private m_pInventoryObject intact when the SmartBox player
|
||||
// is temporarily unavailable. Keep the successful doll and
|
||||
// retry this dirty redress on the next visible frame.
|
||||
// retry this dirty redress on the next frame.
|
||||
}
|
||||
}
|
||||
|
||||
_renderer.Prepare();
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
if (!_view.TryGetVisibleSize(out int width, out int height))
|
||||
return;
|
||||
|
||||
_view.SetTextureHandle(_renderer.Render(width, height));
|
||||
// Zero is a transient not-ready result, not a request to erase a
|
||||
// previously completed paperdoll. Session reset clears explicitly in
|
||||
// ResetSession; ordinary mesh/composite upload latency keeps the last
|
||||
// good image instead of intermittently blanking the viewport.
|
||||
uint textureHandle = _renderer.Render(width, height);
|
||||
if (textureHandle != 0u)
|
||||
_view.SetTextureHandle(textureHandle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -115,80 +136,10 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
public void ResetSession()
|
||||
{
|
||||
_renderer.SetDoll(null);
|
||||
_view.ClearTextureHandle();
|
||||
_doll = null;
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private static bool HasEquivalentAppearance(
|
||||
WorldEntity? current,
|
||||
WorldEntity? candidate)
|
||||
{
|
||||
if (current is null || candidate is null)
|
||||
return ReferenceEquals(current, candidate);
|
||||
if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId
|
||||
|| current.Scale != candidate.Scale
|
||||
|| current.HiddenPartsMask != candidate.HiddenPartsMask
|
||||
|| current.MeshRefs.Count != candidate.MeshRefs.Count
|
||||
|| current.PartOverrides.Count != candidate.PartOverrides.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < current.MeshRefs.Count; i++)
|
||||
{
|
||||
MeshRef left = current.MeshRefs[i];
|
||||
MeshRef right = candidate.MeshRefs[i];
|
||||
if (left.GfxObjId != right.GfxObjId
|
||||
|| left.PartTransform != right.PartTransform
|
||||
|| !DictionaryEquals(
|
||||
left.SurfaceOverrides,
|
||||
right.SurfaceOverrides))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < current.PartOverrides.Count; i++)
|
||||
{
|
||||
if (current.PartOverrides[i] != candidate.PartOverrides[i])
|
||||
return false;
|
||||
}
|
||||
|
||||
PaletteOverride? leftPalette = current.PaletteOverride;
|
||||
PaletteOverride? rightPalette = candidate.PaletteOverride;
|
||||
if (leftPalette is null || rightPalette is null)
|
||||
return leftPalette is null && rightPalette is null;
|
||||
if (leftPalette.BasePaletteId != rightPalette.BasePaletteId
|
||||
|| leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < leftPalette.SubPalettes.Count; i++)
|
||||
{
|
||||
if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DictionaryEquals(
|
||||
IReadOnlyDictionary<uint, uint>? left,
|
||||
IReadOnlyDictionary<uint, uint>? right)
|
||||
{
|
||||
if (left is null || right is null)
|
||||
return left is null && right is null;
|
||||
if (left.Count != right.Count)
|
||||
return false;
|
||||
foreach ((uint key, uint value) in left)
|
||||
{
|
||||
if (!right.TryGetValue(key, out uint rightValue)
|
||||
|| rightValue != value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>
|
||||
|
|
@ -226,6 +177,9 @@ internal sealed class RetailPaperdollFrameView : IPaperdollFrameView
|
|||
/// </summary>
|
||||
public void SetTextureHandle(uint textureHandle) =>
|
||||
_viewport.TextureSlot = UiTextureTableHandle.ToSlot(textureHandle);
|
||||
|
||||
public void ClearTextureHandle() =>
|
||||
_viewport.TextureSlot = GpuTextureSlot.Unassigned;
|
||||
}
|
||||
|
||||
/// <summary>Narrow visibility adapter for the paperdoll's inventory host.</summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue