perf(render): consume prepared mesh package at runtime
This commit is contained in:
parent
c42f93b323
commit
f05afc07c1
22 changed files with 328 additions and 370 deletions
|
|
@ -25,6 +25,7 @@ internal sealed record ContentAudioGraph(
|
|||
|
||||
internal sealed record ContentEffectsAudioResult(
|
||||
IDatReaderWriter Dats,
|
||||
IPreparedAssetSource PreparedAssets,
|
||||
MagicCatalog MagicCatalog,
|
||||
IAnimationLoader AnimationLoader,
|
||||
LiveEntityCollisionBuilder CollisionBuilder,
|
||||
|
|
@ -41,6 +42,7 @@ internal sealed record ContentEffectsAudioResult(
|
|||
|
||||
internal sealed record ContentEffectsAudioDependencies(
|
||||
string DatDirectory,
|
||||
string PreparedAssetPath,
|
||||
PhysicsDataCache PhysicsDataCache,
|
||||
bool DumpMotionEnabled,
|
||||
Spellbook SpellBook,
|
||||
|
|
@ -56,6 +58,7 @@ internal sealed record ContentEffectsAudioDependencies(
|
|||
internal interface IGameWindowContentEffectsAudioPublication
|
||||
{
|
||||
void PublishDatCollection(IDatReaderWriter value);
|
||||
void PublishPreparedAssetSource(IPreparedAssetSource value);
|
||||
void PublishMagicCatalog(MagicCatalog value);
|
||||
void PublishAnimationLoader(IAnimationLoader value);
|
||||
void PublishLiveEntityCollisionBuilder(LiveEntityCollisionBuilder value);
|
||||
|
|
@ -74,6 +77,10 @@ internal interface IGameWindowContentEffectsAudioPublication
|
|||
internal interface IContentEffectsAudioCompositionFactory
|
||||
{
|
||||
IDatReaderWriter OpenDatCollection(string datDirectory);
|
||||
IPreparedAssetSource OpenPreparedAssetSource(
|
||||
string path,
|
||||
IDatReaderWriter dats,
|
||||
Action<string> diagnostic);
|
||||
MagicCatalog LoadMagicCatalog(IDatReaderWriter dats);
|
||||
void InstallSpellMetadata(Spellbook spellBook, MagicCatalog catalog);
|
||||
int GetSpellCount(MagicCatalog catalog);
|
||||
|
|
@ -116,6 +123,12 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
|||
public IDatReaderWriter OpenDatCollection(string datDirectory) =>
|
||||
RuntimeDatCollectionFactory.OpenReadOnly(datDirectory);
|
||||
|
||||
public IPreparedAssetSource OpenPreparedAssetSource(
|
||||
string path,
|
||||
IDatReaderWriter dats,
|
||||
Action<string> diagnostic) =>
|
||||
new PakPreparedAssetSource(path, dats, diagnostic);
|
||||
|
||||
public MagicCatalog LoadMagicCatalog(IDatReaderWriter dats) =>
|
||||
MagicCatalog.Load(dats);
|
||||
|
||||
|
|
@ -196,6 +209,7 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
|||
internal enum ContentEffectsAudioCompositionPoint
|
||||
{
|
||||
DatCollectionPublished,
|
||||
PreparedAssetSourcePublished,
|
||||
MagicCatalogPublished,
|
||||
SpellMetadataInstalled,
|
||||
AnimationLoaderPublished,
|
||||
|
|
@ -268,6 +282,18 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
_publication.PublishDatCollection);
|
||||
Fault(ContentEffectsAudioCompositionPoint.DatCollectionPublished);
|
||||
|
||||
IPreparedAssetSource preparedAssets = mainScope.Acquire(
|
||||
"prepared asset source",
|
||||
() => _factory.OpenPreparedAssetSource(
|
||||
_dependencies.PreparedAssetPath,
|
||||
dats,
|
||||
_dependencies.Error),
|
||||
static value => value.Dispose()).Publish(
|
||||
_publication.PublishPreparedAssetSource);
|
||||
Fault(ContentEffectsAudioCompositionPoint.PreparedAssetSourcePublished);
|
||||
_dependencies.Log(
|
||||
$"prepared assets: opened '{_dependencies.PreparedAssetPath}'");
|
||||
|
||||
MagicCatalog magic = _factory.LoadMagicCatalog(dats);
|
||||
_publication.PublishMagicCatalog(magic);
|
||||
Fault(ContentEffectsAudioCompositionPoint.MagicCatalogPublished);
|
||||
|
|
@ -354,6 +380,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
mainScope.Complete();
|
||||
return new ContentEffectsAudioResult(
|
||||
dats,
|
||||
preparedAssets,
|
||||
magic,
|
||||
animations,
|
||||
collision,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ internal interface IWorldRenderCompositionFactory
|
|||
WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement);
|
||||
TextureCache CreateTextureCache(
|
||||
GL gl,
|
||||
|
|
@ -261,10 +262,12 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
public WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
IGpuResourceRetirementQueue retirement) =>
|
||||
new(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets,
|
||||
NullLogger<WbMeshAdapter>.Instance,
|
||||
retirement);
|
||||
|
||||
|
|
@ -430,6 +433,7 @@ internal sealed class WorldRenderCompositionPhase
|
|||
() => _factory.CreateMeshAdapter(
|
||||
gl,
|
||||
content.Dats,
|
||||
content.PreparedAssets,
|
||||
_dependencies.ResourceRetirement),
|
||||
_publication.PublishWbMeshAdapter,
|
||||
WorldRenderCompositionPoint.MeshAdapterPublished);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public sealed class GameWindow :
|
|||
private Shader? _terrainModernShader;
|
||||
private CameraController? _cameraController;
|
||||
private IDatReaderWriter? _dats;
|
||||
private IPreparedAssetSource? _preparedAssets;
|
||||
private readonly AcDream.App.Input.PointerPositionState _pointerPosition = new();
|
||||
private AcDream.App.Input.CameraPointerInputController? _cameraPointerInput;
|
||||
private Shader? _meshShader;
|
||||
|
|
@ -711,6 +712,13 @@ public sealed class GameWindow :
|
|||
IDatReaderWriter value) =>
|
||||
PublishCompositionOwner(ref _dats, value, "DAT collection");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishPreparedAssetSource(
|
||||
IPreparedAssetSource value) =>
|
||||
PublishCompositionOwner(
|
||||
ref _preparedAssets,
|
||||
value,
|
||||
"prepared asset source");
|
||||
|
||||
void IGameWindowContentEffectsAudioPublication.PublishMagicCatalog(
|
||||
AcDream.App.Spells.MagicCatalog value) =>
|
||||
PublishCompositionOwner(ref _magicCatalog, value, "magic catalog");
|
||||
|
|
@ -1132,6 +1140,7 @@ public sealed class GameWindow :
|
|||
new ContentEffectsAudioCompositionPhase(
|
||||
new ContentEffectsAudioDependencies(
|
||||
_datDir,
|
||||
_options.PreparedAssetPath,
|
||||
_physicsDataCache,
|
||||
_animationDiagnostics.DumpMotionEnabled,
|
||||
SpellBook,
|
||||
|
|
@ -1612,6 +1621,7 @@ public sealed class GameWindow :
|
|||
_glConstructionCleanup),
|
||||
new PlatformShutdownRoots(
|
||||
_dats,
|
||||
_preparedAssets,
|
||||
_input,
|
||||
_gl));
|
||||
private void OnFocusChanged(bool focused)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ internal sealed record RenderShutdownRoots(
|
|||
|
||||
internal sealed record PlatformShutdownRoots(
|
||||
IDatReaderWriter? Dats,
|
||||
IPreparedAssetSource? PreparedAssets,
|
||||
IInputContext? Input,
|
||||
GL? Gl);
|
||||
|
||||
|
|
@ -460,6 +461,7 @@ internal static class GameWindowShutdownManifest
|
|||
]),
|
||||
new ResourceShutdownStage("content mappings",
|
||||
[
|
||||
Hard("prepared asset source", () => platform.PreparedAssets?.Dispose()),
|
||||
Hard("DAT collection", () => platform.Dats?.Dispose()),
|
||||
]),
|
||||
new ResourceShutdownStage("input context",
|
||||
|
|
|
|||
|
|
@ -171,7 +171,11 @@ public static class RenderBootstrap
|
|||
|
||||
// --- WbMeshAdapter (GameWindow ~2286-2287) ---
|
||||
var wbLogger = NullLogger<Wb.WbMeshAdapter>.Instance;
|
||||
var meshAdapter = new Wb.WbMeshAdapter(gl, dats, wbLogger, frameFlights);
|
||||
var meshAdapter = Wb.WbMeshAdapter.CreateWithLiveDatPreparedAssets(
|
||||
gl,
|
||||
dats,
|
||||
wbLogger,
|
||||
frameFlights);
|
||||
|
||||
// --- SequencerFactory (GameWindow ~2306-2334) ---
|
||||
var capturedDats = dats;
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
using AcDream.Core.Meshing;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// AC-specific surface render metadata that WB's <c>MeshBatchData</c>
|
||||
/// doesn't carry. Computed at mesh-extraction time and looked up by the
|
||||
/// draw dispatcher to drive translucency / sky-pass / fog behavior.
|
||||
///
|
||||
/// <para>
|
||||
/// All fields mirror those on today's <see cref="GfxObjSubMesh"/> so
|
||||
/// behavior is preserved bit-for-bit through the migration.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed record AcSurfaceMetadata(
|
||||
TranslucencyKind Translucency,
|
||||
float Luminosity,
|
||||
float Diffuse,
|
||||
float SurfOpacity,
|
||||
bool NeedsUvRepeat,
|
||||
bool DisableFog);
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
using System.Collections.Concurrent;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe side-table mapping <c>(gfxObjId, surfaceIdx)</c> to
|
||||
/// <see cref="AcSurfaceMetadata"/>. Populated when a GfxObj's mesh data
|
||||
/// is extracted; queried at draw time.
|
||||
///
|
||||
/// <para>
|
||||
/// Keyed by <c>(gfxObjId, surfaceIdx)</c> not by WB's runtime batch
|
||||
/// identity because batch objects can be evicted and re-loaded by WB's
|
||||
/// LRU; the (gfxObj, surface) pair is stable across cycles.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class AcSurfaceMetadataTable
|
||||
{
|
||||
private readonly ConcurrentDictionary<(ulong gfxObjId, int surfaceIdx), AcSurfaceMetadata> _table = new();
|
||||
|
||||
public void Add(ulong gfxObjId, int surfaceIdx, AcSurfaceMetadata meta)
|
||||
=> _table[(gfxObjId, surfaceIdx)] = meta;
|
||||
|
||||
public bool TryLookup(ulong gfxObjId, int surfaceIdx, out AcSurfaceMetadata meta)
|
||||
=> _table.TryGetValue((gfxObjId, surfaceIdx), out meta!);
|
||||
|
||||
public void Remove(ulong gfxObjId)
|
||||
{
|
||||
foreach ((ulong Id, int SurfaceIndex) key in _table.Keys)
|
||||
{
|
||||
if (key.Id == gfxObjId)
|
||||
_table.TryRemove(key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() => _table.Clear();
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ public static class EnvCellMeshPreparationScheduler
|
|||
continue;
|
||||
_ = meshManager.PrepareEnvCellGeomMeshDataAsync(
|
||||
shell.GeometryId,
|
||||
shell.CellId,
|
||||
shell.EnvironmentId,
|
||||
shell.CellStructure,
|
||||
new List<ushort>(shell.Surfaces));
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
public uint SurfaceId { get; set; }
|
||||
public TextureKey Key { get; set; }
|
||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||
public AcDream.Core.Meshing.TranslucencyKind Translucency { get; set; }
|
||||
public bool IsTransparent { get; set; }
|
||||
public bool IsAdditive { get; set; }
|
||||
public bool HasWrappingUVs { get; set; }
|
||||
|
|
@ -102,18 +103,15 @@ namespace AcDream.App.Rendering.Wb
|
|||
public class ObjectMeshManager : IDisposable
|
||||
{
|
||||
private readonly OpenGLGraphicsDevice _graphicsDevice;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly IPreparedAssetSource _preparedAssets;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// MP1a (2026-07-05): the GL-free CPU extraction half, verbatim-moved to
|
||||
/// AcDream.Content so the MP1b bake tool can run it without a GL context.
|
||||
/// Owns the dat read → mesh build → inline texture decode pipeline; this
|
||||
/// class keeps the queue/worker lifecycle and all GL upload.
|
||||
/// The immutable prepared-payload source is injected by composition.
|
||||
/// Production uses the validated pak; UI Studio explicitly supplies the
|
||||
/// live-DAT tooling adapter. This class owns queue/worker lifecycle,
|
||||
/// bounded CPU staging, and all GL upload.
|
||||
/// </summary>
|
||||
private readonly MeshExtractor _extractor;
|
||||
|
||||
internal IDatReaderWriter Dats => _dats;
|
||||
|
||||
public bool IsDisposed { get; private set; }
|
||||
private readonly object _disposeGate = new();
|
||||
|
|
@ -141,7 +139,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
private readonly Dictionary<ulong, RetryableResourceReleaseLedger> _uploadRollbacks = new();
|
||||
private readonly Queue<ulong> _uploadRollbackQueue = new();
|
||||
private readonly MeshOwnershipCounter _ownership = new();
|
||||
private readonly ConcurrentDictionary<ulong, (Vector3 Min, Vector3 Max)?> _boundsCache = new();
|
||||
private readonly ConcurrentDictionary<ulong, Task<ObjectMeshData?>> _preparationTasks = new();
|
||||
|
||||
// LRU Cache for Unused objects
|
||||
|
|
@ -280,12 +277,11 @@ namespace AcDream.App.Rendering.Wb
|
|||
internal CacheStats CpuMeshCacheStats => _cpuMeshCache.Stats;
|
||||
|
||||
/// <summary>Decoded-texture cache hit/miss/eviction counts (2026-07-24 measurement-tooling review).</summary>
|
||||
internal CacheStats DecodedTextureCacheStats => _extractor.DecodedTextureCacheStats;
|
||||
internal CacheStats DecodedTextureCacheStats =>
|
||||
_preparedAssets.DecodedTextureCacheStats;
|
||||
|
||||
private sealed class PreparationRequest(
|
||||
ulong id,
|
||||
bool isSetup,
|
||||
EnvCellGeomRequest? envCell,
|
||||
PreparedAssetRequest asset,
|
||||
ObjectMeshData? cachedData,
|
||||
TaskCompletionSource<ObjectMeshData?> completion,
|
||||
CancellationTokenSource cancellation)
|
||||
|
|
@ -296,9 +292,8 @@ namespace AcDream.App.Rendering.Wb
|
|||
private bool _disposeRequested;
|
||||
private bool _cancellationDisposed;
|
||||
|
||||
public ulong Id { get; } = id;
|
||||
public bool IsSetup { get; } = isSetup;
|
||||
public EnvCellGeomRequest? EnvCell { get; } = envCell;
|
||||
public PreparedAssetRequest Asset { get; } = asset;
|
||||
public ulong Id => Asset.RuntimeObjectId;
|
||||
public ObjectMeshData? CachedData { get; } = cachedData;
|
||||
public TaskCompletionSource<ObjectMeshData?> Completion { get; } = completion;
|
||||
public CancellationTokenSource Cancellation { get; } = cancellation;
|
||||
|
|
@ -402,26 +397,18 @@ namespace AcDream.App.Rendering.Wb
|
|||
public bool IsQueued { get; set; }
|
||||
}
|
||||
|
||||
public ObjectMeshManager(OpenGLGraphicsDevice graphicsDevice, IDatReaderWriter dats, ILogger<ObjectMeshManager> logger)
|
||||
public ObjectMeshManager(
|
||||
OpenGLGraphicsDevice graphicsDevice,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<ObjectMeshManager> logger)
|
||||
{
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_dats = dats;
|
||||
_logger = logger;
|
||||
// Side-stage sink: particle-preload meshes staged mid-extraction go
|
||||
// straight onto the staged-upload queue, exactly as the pre-MP1a code
|
||||
// did — immediate enqueue, surviving a later throw in the same
|
||||
// Prepare* call. ConcurrentQueue.Enqueue is thread-safe for the
|
||||
// extractor's up-to-4 concurrent decode workers.
|
||||
_graphicsDevice = graphicsDevice
|
||||
?? throw new ArgumentNullException(nameof(graphicsDevice));
|
||||
_preparedAssets = preparedAssets
|
||||
?? throw new ArgumentNullException(nameof(preparedAssets));
|
||||
_logger = logger
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
_cpuMeshCache = new CpuMeshUploadCache(_maxCpuCacheSize);
|
||||
_extractor = new MeshExtractor(_dats, _logger, data =>
|
||||
{
|
||||
// Side-staged particle/setup dependencies obey the same bounded
|
||||
// producer policy as primary results. Keeping them in the CPU
|
||||
// cache makes a later explicit owner able to re-arm upload.
|
||||
_cpuMeshCache.Store(data);
|
||||
if (_ownership.IsOwned(data.ObjectId))
|
||||
_stagedMeshData.Stage(data);
|
||||
});
|
||||
_useModernRendering = _graphicsDevice.HasOpenGL43 && _graphicsDevice.HasBindless;
|
||||
if (_useModernRendering)
|
||||
{
|
||||
|
|
@ -809,6 +796,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
|
||||
public struct EnvCellGeomRequest
|
||||
{
|
||||
public uint SourceCellId;
|
||||
public uint EnvironmentId;
|
||||
public ushort CellStructure;
|
||||
public List<ushort> Surfaces;
|
||||
|
|
@ -817,12 +805,19 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// <summary>
|
||||
/// Phase 1 (Background Thread): Prepare CPU-side mesh data for deduplicated EnvCell geometry.
|
||||
/// </summary>
|
||||
public Task<ObjectMeshData?> PrepareEnvCellGeomMeshDataAsync(ulong geomId, uint environmentId, ushort cellStructure, List<ushort> surfaces, CancellationToken ct = default)
|
||||
public Task<ObjectMeshData?> PrepareEnvCellGeomMeshDataAsync(
|
||||
ulong geomId,
|
||||
uint sourceCellId,
|
||||
uint environmentId,
|
||||
ushort cellStructure,
|
||||
List<ushort> surfaces,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
if (IsDisposed || HasRenderData(geomId)) return Task.FromResult<ObjectMeshData?>(null);
|
||||
|
||||
var envCell = new EnvCellGeomRequest
|
||||
{
|
||||
SourceCellId = sourceCellId,
|
||||
EnvironmentId = environmentId,
|
||||
CellStructure = cellStructure,
|
||||
Surfaces = surfaces
|
||||
|
|
@ -874,9 +869,12 @@ namespace AcDream.App.Rendering.Wb
|
|||
var cancellation = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
_preparationTasks[geomId] = task;
|
||||
var request = new PreparationRequest(
|
||||
geomId,
|
||||
false,
|
||||
envCell,
|
||||
PreparedAssetRequest.EnvCellGeometry(
|
||||
sourceCellId,
|
||||
geomId,
|
||||
environmentId,
|
||||
cellStructure,
|
||||
surfaces),
|
||||
deferredCachedData,
|
||||
tcs,
|
||||
cancellation);
|
||||
|
|
@ -933,10 +931,18 @@ namespace AcDream.App.Rendering.Wb
|
|||
EnvCellGeomRequest? envCell = _envCellDescriptors.TryGetValue(id, out EnvCellGeomRequest descriptor)
|
||||
? descriptor
|
||||
: null;
|
||||
PreparedAssetRequest asset = envCell is { } env
|
||||
? PreparedAssetRequest.EnvCellGeometry(
|
||||
env.SourceCellId,
|
||||
id,
|
||||
env.EnvironmentId,
|
||||
env.CellStructure,
|
||||
env.Surfaces)
|
||||
: isSetup
|
||||
? PreparedAssetRequest.Setup(checked((uint)id))
|
||||
: PreparedAssetRequest.GfxObj(checked((uint)id));
|
||||
var request = new PreparationRequest(
|
||||
id,
|
||||
isSetup,
|
||||
envCell,
|
||||
asset,
|
||||
deferredCachedData,
|
||||
tcs,
|
||||
cancellation);
|
||||
|
|
@ -956,7 +962,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
lock (_pendingRequests)
|
||||
{
|
||||
// IsDisposed re-check lets Dispose cancel and join every
|
||||
// tracked worker before the DAT mappings are released.
|
||||
// tracked worker before prepared content mappings are released.
|
||||
// Exit WITHOUT resetting the shared manual-reset event:
|
||||
// Dispose sets it once to wake all four persistent workers.
|
||||
// If the first worker reset it, the remaining three slept
|
||||
|
|
@ -991,7 +997,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
ulong id = request.Id;
|
||||
bool isSetup = request.IsSetup;
|
||||
TaskCompletionSource<ObjectMeshData?> tcs = request.Completion;
|
||||
CancellationToken ct = request.Cancellation.Token;
|
||||
|
||||
|
|
@ -1010,38 +1015,21 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
|
||||
ObjectMeshData? data = request.CachedData;
|
||||
if (data is null && request.EnvCell is { } req)
|
||||
if (data is null)
|
||||
{
|
||||
uint envId = 0x0D000000u | req.EnvironmentId;
|
||||
if (_dats.Portal.TryGet<DatReaderWriter.DBObjs.Environment>(envId, out var environment))
|
||||
PreparedAssetReadResult read =
|
||||
_preparedAssets.Read(request.Asset, ct);
|
||||
data = read.Data;
|
||||
if (read.Status != PreparedAssetReadStatus.Loaded)
|
||||
{
|
||||
if (environment.Cells.TryGetValue(req.CellStructure, out var cellStruct))
|
||||
{
|
||||
data = _extractor.PrepareCellStructMeshData(id, cellStruct, req.Surfaces, Matrix4x4.Identity, ct);
|
||||
// TEMP diagnostic #105 (strip with fix): a null prep here means
|
||||
// this deduplicated cell geometry will NEVER render anywhere.
|
||||
if (data == null)
|
||||
Console.WriteLine($"[geom-null] prepare-null geom=0x{id:X10} env=0x{envId:X8} cs=0x{req.CellStructure:X4}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[geom-null] cellstruct-missing geom=0x{id:X10} env=0x{envId:X8} cs=0x{req.CellStructure:X4}");
|
||||
}
|
||||
_logger.LogError(
|
||||
"Prepared mesh {Status} for {Type} source " +
|
||||
"0x{SourceId:X8}, runtime 0x{RuntimeId:X10}",
|
||||
read.Status,
|
||||
request.Asset.Type,
|
||||
request.Asset.SourceFileId,
|
||||
request.Asset.RuntimeObjectId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[geom-null] env-read-failed geom=0x{id:X10} env=0x{envId:X8}");
|
||||
}
|
||||
}
|
||||
else if (data is null)
|
||||
{
|
||||
// TEMP diagnostic #105 (strip with fix): an EnvCell geom id (bit 33)
|
||||
// whose pending request vanished gets misrouted to the generic path,
|
||||
// where its hash-derived low bits resolve to nothing -> silent null.
|
||||
if ((id & 0x2_0000_0000UL) != 0)
|
||||
Console.WriteLine($"[geom-misroute] envcell geom 0x{id:X10} had no pending request — generic path will null it");
|
||||
// If it's a direct setup or gfxobj, make sure background loads don't abort half-way
|
||||
data = _extractor.PrepareMeshData(id, isSetup, ct);
|
||||
}
|
||||
if (ct.IsCancellationRequested)
|
||||
{
|
||||
|
|
@ -1168,7 +1156,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
/// Readiness barrier used by the streaming publisher. Missing owned
|
||||
/// data is re-armed through its retained schema, so synthetic EnvCell
|
||||
/// IDs can never fall into generic GfxObj decoding after cancellation.
|
||||
/// A deterministic DAT failure is retained until the next explicit
|
||||
/// A deterministic prepared-payload failure is retained until the next explicit
|
||||
/// ownership schedule instead of being retried every render frame.
|
||||
/// </summary>
|
||||
internal bool EnsureRenderDataReady(ulong id)
|
||||
|
|
@ -1194,6 +1182,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
{
|
||||
_ = PrepareEnvCellGeomMeshDataAsync(
|
||||
id,
|
||||
req.SourceCellId,
|
||||
req.EnvironmentId,
|
||||
req.CellStructure,
|
||||
req.Surfaces);
|
||||
|
|
@ -1206,16 +1195,16 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase 1 (Background Thread): Prepare CPU-side mesh data from DAT.
|
||||
/// This loads vertices, indices, and texture data but creates NO GPU resources.
|
||||
/// Thread-safe: only reads from DAT files.
|
||||
///
|
||||
/// MP1a (2026-07-05): delegates to <see cref="MeshExtractor.PrepareMeshData"/>,
|
||||
/// the verbatim-moved GL-free extraction dispatcher. See <see cref="_extractor"/>.
|
||||
/// Synchronously reads an immutable prepared payload without creating
|
||||
/// GPU resources. The production source is the validated pak; explicit
|
||||
/// tooling adapters may use live DAT extraction.
|
||||
/// </summary>
|
||||
public ObjectMeshData? PrepareMeshData(ulong id, bool isSetup, CancellationToken ct = default)
|
||||
{
|
||||
return _extractor.PrepareMeshData(id, isSetup, ct);
|
||||
PreparedAssetRequest request = isSetup
|
||||
? PreparedAssetRequest.Setup(checked((uint)id))
|
||||
: PreparedAssetRequest.GfxObj(checked((uint)id));
|
||||
return _preparedAssets.Read(request, ct).Data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1709,86 +1698,6 @@ namespace AcDream.App.Rendering.Wb
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets bounding box for an object (for frustum culling).
|
||||
/// </summary>
|
||||
public (Vector3 Min, Vector3 Max)? GetBounds(ulong id, bool isSetup)
|
||||
{
|
||||
if (_boundsCache.TryGetValue(id, out var cachedBounds))
|
||||
{
|
||||
return cachedBounds;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
(Vector3 Min, Vector3 Max)? result = null;
|
||||
uint datId = (uint)(id & 0xFFFFFFFFu);
|
||||
var resolutions = _dats.ResolveId(datId).ToList();
|
||||
var selectedResolution = resolutions.OrderByDescending(r => r.Database == _dats.Portal).FirstOrDefault();
|
||||
if (selectedResolution == null) return null;
|
||||
|
||||
var type = selectedResolution.Type;
|
||||
var db = selectedResolution.Database;
|
||||
|
||||
if (type == DBObjType.Setup)
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
bool hasBounds = false;
|
||||
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
|
||||
|
||||
_extractor.CollectParts(datId, Matrix4x4.Identity, parts, ref min, ref max, ref hasBounds, CancellationToken.None);
|
||||
result = hasBounds ? (min, max) : null;
|
||||
}
|
||||
else if (type == DBObjType.EnvCell)
|
||||
{
|
||||
if (!db.TryGet<EnvCell>(datId, out var envCell)) return null;
|
||||
|
||||
// If bit 32 is set, this is a request for the cell's synthetic geometry only
|
||||
if ((id & 0x1_0000_0000UL) != 0)
|
||||
{
|
||||
uint envId = 0x0D000000u | envCell.EnvironmentId;
|
||||
if (_dats.Portal.TryGet<DatReaderWriter.DBObjs.Environment>(envId, out var environment))
|
||||
{
|
||||
if (environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct))
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
foreach (var vert in cellStruct.VertexArray.Vertices.Values)
|
||||
{
|
||||
min = Vector3.Min(min, vert.Origin);
|
||||
max = Vector3.Max(max, vert.Origin);
|
||||
}
|
||||
result = (min, max);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var min = new Vector3(float.MaxValue);
|
||||
var max = new Vector3(float.MinValue);
|
||||
bool hasBounds = false;
|
||||
var parts = new List<(ulong GfxObjId, Matrix4x4 Transform)>();
|
||||
|
||||
_extractor.CollectParts(datId, Matrix4x4.Identity, parts, ref min, ref max, ref hasBounds, CancellationToken.None);
|
||||
result = hasBounds ? (min, max) : null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!db.TryGet<GfxObj>(datId, out var gfxObj)) return null;
|
||||
result = _extractor.ComputeBounds(gfxObj, Vector3.One);
|
||||
}
|
||||
_boundsCache[id] = result;
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error computing bounds for 0x{Id:X8}", id);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#region Private: Background Preparation
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1981,6 +1890,7 @@ namespace AcDream.App.Rendering.Wb
|
|||
TextureIndex = textureIndex,
|
||||
TextureSize = (format.Width, format.Height),
|
||||
TextureFormat = format.Format,
|
||||
Translucency = batch.Translucency,
|
||||
IsTransparent = batch.IsTransparent,
|
||||
IsAdditive = batch.IsAdditive,
|
||||
HasWrappingUVs = batch.HasWrappingUVs,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// <summary>
|
||||
/// Draws entities using WB's <see cref="ObjectRenderData"/> (a single global
|
||||
/// VAO/VBO/IBO under modern rendering) with acdream's <see cref="TextureCache"/>
|
||||
/// for bindless texture resolution and <see cref="AcSurfaceMetadataTable"/> for
|
||||
/// translucency classification.
|
||||
/// for bindless texture resolution. Exact pass classification travels with
|
||||
/// each immutable prepared mesh batch.
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Atlas-tier</b> entities (<c>ServerGuid == 0</c>): mesh data comes from WB's
|
||||
|
|
@ -1359,7 +1359,6 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
foreach (InstanceGroup group in _groups.Values)
|
||||
group.ClearPerInstanceData();
|
||||
|
||||
var metaTable = _meshAdapter.MetadataTable;
|
||||
uint anyVao = 0;
|
||||
|
||||
// Project the 5-tuple enumerable into LandblockEntry records for WalkEntities.
|
||||
|
|
@ -1787,7 +1786,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
opacityMultiplier = 1f - translucencyValue; // CMaterial::SetTranslucencySimple 0x005396f0
|
||||
}
|
||||
|
||||
if (!ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, paletteIdentity, metaTable, restPose, opacityMultiplier, collector))
|
||||
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector))
|
||||
currentEntityIncomplete = true;
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
|
|
@ -1817,7 +1816,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (!fullyInvisible)
|
||||
{
|
||||
var model = meshRef.PartTransform * entityWorld;
|
||||
if (!ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, paletteIdentity, metaTable, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector))
|
||||
if (!ClassifyBatches(renderData, model, entity, meshRef, paletteIdentity, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector))
|
||||
currentEntityIncomplete = true;
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
|
|
@ -3239,12 +3238,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
private bool ClassifyBatches(
|
||||
ObjectRenderData renderData,
|
||||
ulong gfxObjId,
|
||||
Matrix4x4 model,
|
||||
WorldEntity entity,
|
||||
MeshRef meshRef,
|
||||
PaletteCompositeIdentity paletteIdentity,
|
||||
AcSurfaceMetadataTable metaTable,
|
||||
Matrix4x4 restPose,
|
||||
float opacityMultiplier = 1.0f,
|
||||
List<CachedBatch>? collector = null)
|
||||
|
|
@ -3254,17 +3251,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
var batch = renderData.Batches[batchIdx];
|
||||
|
||||
TranslucencyKind translucency;
|
||||
if (metaTable.TryLookup(gfxObjId, batchIdx, out var meta))
|
||||
{
|
||||
translucency = meta.Translucency;
|
||||
}
|
||||
else
|
||||
{
|
||||
translucency = batch.IsAdditive ? TranslucencyKind.Additive
|
||||
: batch.IsTransparent ? TranslucencyKind.AlphaBlend
|
||||
: TranslucencyKind.Opaque;
|
||||
}
|
||||
TranslucencyKind translucency = batch.Translucency;
|
||||
|
||||
// #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap
|
||||
// must route through the alpha-blend pass so mesh_modern.frag's
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Rendering;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Silk.NET.OpenGL;
|
||||
|
|
@ -48,9 +46,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||
private readonly ObjectMeshManager? _meshManager;
|
||||
private readonly AcDream.App.Rendering.IGpuResourceRetirementQueue? _resourceRetirement;
|
||||
private readonly IDatReaderWriter? _dats;
|
||||
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
||||
private readonly HashSet<ulong> _metadataPopulated = new();
|
||||
private readonly IPreparedAssetSource? _ownedPreparedAssets;
|
||||
private readonly MeshUploadFrameBudget _uploadBudget = new(new MeshUploadBudgetLimits(
|
||||
MaximumUploadsPerFrame,
|
||||
MaximumUploadBytesPerFrame,
|
||||
|
|
@ -95,35 +91,72 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
_meshManager?.CpuCacheDiagnostics ?? default;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs the full WB pipeline: OpenGLGraphicsDevice → shared bounded
|
||||
/// DAT facade → ObjectMeshManager.
|
||||
/// Constructs the UI-Studio/tooling WB pipeline. Production composition
|
||||
/// supplies the validated pak source through the internal overload below;
|
||||
/// this explicit tooling seam retains live-DAT extraction.
|
||||
/// </summary>
|
||||
/// <param name="gl">Active Silk.NET GL context. Must be bound to the current
|
||||
/// thread (construction runs GL queries; call from OnLoad).</param>
|
||||
/// <param name="dats">acdream's shared runtime DAT facade, used to populate the surface
|
||||
/// metadata side-table via <c>GfxObjMesh.Build</c>. Shares file handles with
|
||||
/// the rest of the client; read-only access from the render thread.</param>
|
||||
/// <param name="dats">acdream's shared runtime DAT facade. Tooling uses it
|
||||
/// through an explicitly owned <see cref="DatPreparedAssetSource"/>.</param>
|
||||
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
|
||||
/// NullLogger internally.</param>
|
||||
public WbMeshAdapter(GL gl, IDatReaderWriter dats, ILogger<WbMeshAdapter> logger)
|
||||
: this(gl, dats, logger, AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance)
|
||||
: this(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets: null,
|
||||
logger,
|
||||
AcDream.App.Rendering.ImmediateGpuResourceRetirementQueue.Instance,
|
||||
ownsPreparedAssets: true)
|
||||
{
|
||||
}
|
||||
|
||||
internal static WbMeshAdapter CreateWithLiveDatPreparedAssets(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement) =>
|
||||
new(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets: null,
|
||||
logger,
|
||||
resourceRetirement,
|
||||
ownsPreparedAssets: true);
|
||||
|
||||
internal WbMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement)
|
||||
: this(
|
||||
gl,
|
||||
dats,
|
||||
preparedAssets,
|
||||
logger,
|
||||
resourceRetirement,
|
||||
ownsPreparedAssets: false)
|
||||
{
|
||||
}
|
||||
|
||||
private WbMeshAdapter(
|
||||
GL gl,
|
||||
IDatReaderWriter dats,
|
||||
IPreparedAssetSource? preparedAssets,
|
||||
ILogger<WbMeshAdapter> logger,
|
||||
AcDream.App.Rendering.IGpuResourceRetirementQueue resourceRetirement,
|
||||
bool ownsPreparedAssets)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
_dats = dats;
|
||||
_resourceRetirement = resourceRetirement;
|
||||
var resources = new AcDream.App.Rendering.ResourceCleanupGroup();
|
||||
OpenGLGraphicsDevice? graphicsDevice = null;
|
||||
IPreparedAssetSource? resolvedPreparedAssets = preparedAssets;
|
||||
ObjectMeshManager? meshManager = null;
|
||||
try
|
||||
{
|
||||
|
|
@ -145,11 +178,20 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
}
|
||||
});
|
||||
resources.Add("WB graphics device", graphicsDeviceRelease.Run);
|
||||
if (resolvedPreparedAssets is null)
|
||||
{
|
||||
resolvedPreparedAssets = new DatPreparedAssetSource(
|
||||
dats,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
resources.Add(
|
||||
"WB tooling prepared asset source",
|
||||
resolvedPreparedAssets.Dispose);
|
||||
}
|
||||
// ConsoleErrorLogger surfaces WB's silently-caught exceptions
|
||||
// (ObjectMeshManager.PrepareMeshData try/catch at line ~589).
|
||||
meshManager = new ObjectMeshManager(
|
||||
graphicsDevice,
|
||||
dats,
|
||||
resolvedPreparedAssets,
|
||||
new ConsoleErrorLogger<ObjectMeshManager>());
|
||||
resources.Add("WB object mesh manager", meshManager.Dispose);
|
||||
resources.TransferAll();
|
||||
|
|
@ -163,6 +205,9 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
|
||||
_graphicsDevice = graphicsDevice;
|
||||
_meshManager = meshManager;
|
||||
_ownedPreparedAssets = ownsPreparedAssets
|
||||
? resolvedPreparedAssets
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -213,13 +258,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
/// underlying mesh manager. Public methods are all no-ops.</summary>
|
||||
public static WbMeshAdapter CreateUninitialized() => new();
|
||||
|
||||
/// <summary>
|
||||
/// The surface metadata side-table populated on each first
|
||||
/// <see cref="IncrementRefCount"/>. Queried by the draw dispatcher
|
||||
/// to determine translucency, luminosity, and fog behavior per batch.
|
||||
/// </summary>
|
||||
public AcSurfaceMetadataTable MetadataTable => _metadataTable;
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 (2026-05-28): exposes the underlying <see cref="ObjectMeshManager"/>
|
||||
/// so <c>EnvCellRenderer</c> can share the same global mesh buffer (VAO/VBO/IBO).
|
||||
|
|
@ -265,13 +303,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
if (_isUninitialized || _meshManager is null) return;
|
||||
_meshManager.IncrementRefCount(id);
|
||||
|
||||
bool metadataClaimed = false;
|
||||
try
|
||||
{
|
||||
metadataClaimed = _metadataPopulated.Add(id);
|
||||
if (metadataClaimed)
|
||||
PopulateMetadata(id);
|
||||
|
||||
// WB's IncrementRefCount alone only bumps a usage counter; it does
|
||||
// NOT trigger mesh loading. We must explicitly call PrepareMeshDataAsync
|
||||
// so the background workers actually decode the GfxObj. The result
|
||||
|
|
@ -281,9 +314,8 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// skips the entity — standard streaming flicker.
|
||||
//
|
||||
// #128 (2026-06-11): Prepare must RE-ARM whenever the id has no render
|
||||
// data — NOT only on the first-ever registration. The old
|
||||
// first-ever-only gate (`if (_metadataPopulated.Add(id))`) permanently
|
||||
// lost any id whose initial decode was cancelled before completing
|
||||
// data — NOT only on the first-ever registration. A first-only gate
|
||||
// permanently lost any id whose initial read was cancelled before completing
|
||||
// (landblock unload → CancelStagedUploads during login/teleport
|
||||
// churn) or whose upload was later LRU-evicted: every subsequent
|
||||
// registration skipped Prepare, so the mesh stayed invisible for the
|
||||
|
|
@ -299,19 +331,13 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
// isSetup: false — acdream's MeshRefs already carry expanded
|
||||
// per-part GfxObj ids (0x01XXXXXX). WB's Setup-expansion path is
|
||||
// unused.
|
||||
if (metadataClaimed || _meshManager.TryGetRenderData(id) is null)
|
||||
if (_meshManager.TryGetRenderData(id) is null)
|
||||
_meshManager.PrepareMeshDataAsync(id, isSetup: false);
|
||||
}
|
||||
catch (Exception acquireFailure)
|
||||
{
|
||||
if (metadataClaimed)
|
||||
{
|
||||
_metadataPopulated.Remove(id);
|
||||
_metadataTable.Remove(id);
|
||||
}
|
||||
|
||||
// IncrementRefCount is a public ownership boundary. Metadata/DAT
|
||||
// preparation happens after ObjectMeshManager commits its count,
|
||||
// IncrementRefCount is a public ownership boundary. Prepared-data
|
||||
// acquisition happens after ObjectMeshManager commits its count,
|
||||
// so compensate before reporting failure. ObjectMeshManager's
|
||||
// decrement has a strong exception guarantee; if compensation
|
||||
// itself fails, expose that the increment remains committed so a
|
||||
|
|
@ -612,21 +638,6 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
$" (arraysBefore={before.ArraysWithPending})");
|
||||
}
|
||||
|
||||
private void PopulateMetadata(ulong id)
|
||||
{
|
||||
if (_dats is null) return;
|
||||
if (!_dats.Portal.TryGet<GfxObj>((uint)id, out var gfxObj)) return;
|
||||
|
||||
var subMeshes = GfxObjMesh.Build(gfxObj, _dats);
|
||||
for (int i = 0; i < subMeshes.Count; i++)
|
||||
{
|
||||
var sm = subMeshes[i];
|
||||
_metadataTable.Add(id, i, new AcSurfaceMetadata(
|
||||
sm.Translucency, sm.Luminosity, sm.Diffuse,
|
||||
sm.SurfOpacity, sm.NeedsUvRepeat, sm.DisableFog));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
@ -646,6 +657,7 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
|||
frameFlights.WaitForSubmittedWork();
|
||||
},
|
||||
() => _meshManager?.Dispose(),
|
||||
() => _ownedPreparedAssets?.Dispose(),
|
||||
() => DrainGraphicsQueue("publishing mesh resource retirements"),
|
||||
() =>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ public sealed class LandblockBuildFactory
|
|||
|
||||
// Hydrate the stabs: same logic as the old OnLoad preload. Each stab
|
||||
// entity from LandblockLoader carries a SourceGfxObjOrSetupId that we
|
||||
// expand into per-part MeshRefs via SetupMesh.Flatten / GfxObjMesh.Build.
|
||||
// expand into per-part MeshRefs via SetupMesh.Flatten.
|
||||
// GPU upload happens later through the render-thread presentation
|
||||
// pipeline, never in this worker-side build.
|
||||
var hydrated = new List<AcDream.Core.World.WorldEntity>(baseLoaded.Entities.Count);
|
||||
|
|
@ -364,9 +364,6 @@ public sealed class LandblockBuildFactory
|
|||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
|
||||
// Sub-meshes are CPU-built here; the presentation pipeline
|
||||
// defers upload to the render thread.
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
|
||||
|
|
@ -384,7 +381,6 @@ public sealed class LandblockBuildFactory
|
|||
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
|
||||
if (gfx is null) continue;
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
// Compose: part's own transform, then the spawn's scale.
|
||||
var partXf = mr.PartTransform * scaleMat;
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
|
|
@ -692,7 +688,6 @@ public sealed class LandblockBuildFactory
|
|||
if (gfx is not null)
|
||||
{
|
||||
_physicsDataCache.CacheGfxObj(stab.Id, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
|
||||
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
|
||||
|
|
@ -733,7 +728,6 @@ public sealed class LandblockBuildFactory
|
|||
continue;
|
||||
}
|
||||
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
|
||||
_ = AcDream.Core.Meshing.GfxObjMesh.Build(gfx, _dats);
|
||||
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
|
||||
if (pb is not null) interiorBounds.Add(mr.PartTransform, pb.Value);
|
||||
meshRefs.Add(mr);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.Core.Rendering.Wb;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Content.Vfx;
|
||||
using BCnEncoder.Decoder;
|
||||
using BCnEncoder.ImageSharp;
|
||||
|
|
@ -569,6 +570,9 @@ public sealed class MeshExtractor {
|
|||
TextureData = textureData!,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType,
|
||||
Translucency =
|
||||
TranslucencyKindExtensions.FromSurfaceType(
|
||||
surface.Type),
|
||||
IsTransparent = isTransparent,
|
||||
IsAdditive = isAdditive
|
||||
};
|
||||
|
|
@ -919,6 +923,9 @@ public sealed class MeshExtractor {
|
|||
TextureData = textureData!,
|
||||
UploadPixelFormat = uploadPixelFormat,
|
||||
UploadPixelType = uploadPixelType,
|
||||
Translucency =
|
||||
TranslucencyKindExtensions.FromSurfaceType(
|
||||
surface.Type),
|
||||
IsTransparent = isTransparent,
|
||||
IsAdditive = isAdditive
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Chorizite.Core.Lib;
|
||||
using Chorizite.Core.Render.Enums;
|
||||
using AcDream.Core.Meshing;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using System;
|
||||
|
|
@ -149,6 +150,8 @@ public class TextureBatchData {
|
|||
public UploadPixelType? UploadPixelType { get; set; }
|
||||
public List<ushort> Indices { get; set; } = new();
|
||||
public DatReaderWriter.Enums.CullMode CullMode { get; set; }
|
||||
public TranslucencyKind Translucency { get; set; } =
|
||||
TranslucencyKind.Opaque;
|
||||
public bool IsTransparent { get; set; }
|
||||
public bool IsAdditive { get; set; }
|
||||
public bool HasWrappingUVs { get; set; }
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ public static class ObjectMeshDataSerializer {
|
|||
WriteNullableInt32Enum(w, batch.UploadPixelType.HasValue, batch.UploadPixelType is { } upt ? (int)upt : 0);
|
||||
WriteUInt16List(w, batch.Indices);
|
||||
w.Write((int)batch.CullMode);
|
||||
w.Write((int)batch.Translucency);
|
||||
w.Write(batch.IsTransparent);
|
||||
w.Write(batch.IsAdditive);
|
||||
w.Write(batch.HasWrappingUVs);
|
||||
|
|
@ -182,6 +183,8 @@ public static class ObjectMeshDataSerializer {
|
|||
batch.UploadPixelType = ReadNullableInt32Enum(r, v => (UploadPixelType)v);
|
||||
batch.Indices = ReadUInt16List(r);
|
||||
batch.CullMode = (CullMode)r.ReadInt32();
|
||||
batch.Translucency =
|
||||
(AcDream.Core.Meshing.TranslucencyKind)r.ReadInt32();
|
||||
batch.IsTransparent = r.ReadBoolean();
|
||||
batch.IsAdditive = r.ReadBoolean();
|
||||
batch.HasWrappingUVs = r.ReadBoolean();
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ public static class PakFormat {
|
|||
|
||||
/// <summary>
|
||||
/// Identity of the bake algorithm that produced the payloads. Version 2
|
||||
/// introduces content-identity EnvCell blobs with ordinary TOC aliases.
|
||||
/// introduced content-identity EnvCell blobs with ordinary TOC aliases;
|
||||
/// version 3 embeds exact render-pass translucency in each texture batch
|
||||
/// so production never rebuilds surface metadata from live DAT.
|
||||
/// The binary format remains version 1.
|
||||
/// </summary>
|
||||
public const uint CurrentBakeToolVersion = 2;
|
||||
public const uint CurrentBakeToolVersion = 3;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue