Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close #471, delete the replay pipelines/shaders, and advance prepared content to recipe 9. Mutation witnesses (each restored before commit): - X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a. - X*=base alpha: same test line 175, forbidden baseTexel.a found. - CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing. - second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls. - straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed). - omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0). - omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing. - drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216. - stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5). - per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B. Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
185 lines
8.7 KiB
C#
185 lines
8.7 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.IO;
|
|
|
|
namespace AcDream.Content.Pak;
|
|
|
|
/// <summary>
|
|
/// Pak format constants shared by writer and reader.
|
|
/// </summary>
|
|
public static class PakFormat {
|
|
/// <summary>
|
|
/// The one format version this build writes and reads. PakWriter stamps
|
|
/// it unconditionally (callers cannot override it via the header
|
|
/// template); PakReader refuses to open any other version. Bump ONLY
|
|
/// with an accompanying reader migration path.
|
|
/// </summary>
|
|
public const uint CurrentFormatVersion = 2;
|
|
|
|
/// <summary>
|
|
/// Identity of the bake algorithm that produced the payloads. Version 2
|
|
/// 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. Version 4
|
|
/// adds complete immutable flat collision and EnvCell-topology payloads.
|
|
/// Version 5 (#426, 2026-08-23) extracts untextured (solid-colour)
|
|
/// positive faces that versions <=4 dropped — every GfxObj polygon
|
|
/// whose Stippling carries NoPos (NO_POS_UVS) previously extracted to
|
|
/// zero vertices on its positive side, so any pak baked by an older tool
|
|
/// is missing those faces (e.g. the Holtburg windmill axle 0x010010CE,
|
|
/// 8 polygons all NoPos + Base1Solid, extracted to a 0-vertex mesh). The
|
|
/// binary format remains version 1. Version 6 introduces pak format 2:
|
|
/// globally shared texture payloads plus independently Brotli-compressed
|
|
/// blobs with raw fallback. Mesh geometry and source texture bytes remain
|
|
/// exact; unedited DXT surfaces now retain their native BC blocks. Version
|
|
/// 7 replaces the synthetic vertex-AABB GfxObj view sphere with retail's
|
|
/// authored DrawingBSP root sphere. The binary format remains version 2,
|
|
/// but every prepared GfxObj render record must be regenerated because
|
|
/// the sphere participates in portal-view admission. Version 8 (OH2/S1,
|
|
/// docs/research/2026-09-01-overhaul/oh2-cellstruct-surface-contract.md)
|
|
/// replaces CellStruct/EnvCell extraction with retail's exact
|
|
/// surface-array-index subset construction: side candidates come only
|
|
/// from sides_type (not NoPos/NoNeg), the subset/material owner is the
|
|
/// source surface-array index rather than TextureKey/stippling/sides,
|
|
/// and built-EnvCell admission is
|
|
/// (Surface.Type & (BASE1_IMAGE|BASE1_CLIPMAP)) != 0 applied after
|
|
/// surface resolution. Version 9 appends authored surface opacity to
|
|
/// every prepared mesh texture batch. The binary format remains version
|
|
/// 2, but every prepared render record must be regenerated.
|
|
/// </summary>
|
|
public const uint CurrentBakeToolVersion = 9;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fixed 64-byte pak file header. Layout (all integers little-endian):
|
|
/// <code>
|
|
/// offset size field
|
|
/// 0 4 magic 'ACPK' (0x4B504341)
|
|
/// 4 4 formatVersion = 2
|
|
/// 8 4 portalIteration (DatCollection.Portal.Iteration)
|
|
/// 12 4 cellIteration
|
|
/// 16 4 highResIteration
|
|
/// 20 4 languageIteration
|
|
/// 24 8 tocOffset (u64)
|
|
/// 32 4 tocCount (u32)
|
|
/// 36 4 bakeToolVersion
|
|
/// 40 24 reserved (zero)
|
|
/// </code>
|
|
/// Format 1 is specified by docs/superpowers/plans/2026-07-05-mp1b-pak-and-bake.md.
|
|
/// Format 2 retains this header and adds the TOC compression flag plus global
|
|
/// texture payload entries documented in docs/plans/2026-08-27-pak-v2-resource-campaign.md.
|
|
/// </summary>
|
|
public struct PakHeader {
|
|
public const int Size = 64;
|
|
public const uint MagicValue = 0x4B504341u; // 'ACPK' little-endian
|
|
|
|
/// <summary>Always <see cref="MagicValue"/> after <see cref="ReadFrom(ReadOnlySpan{byte})"/>; not settable by callers building a header to write.</summary>
|
|
public uint Magic { get; private set; } = MagicValue;
|
|
|
|
public uint FormatVersion;
|
|
public uint PortalIteration;
|
|
public uint CellIteration;
|
|
public uint HighResIteration;
|
|
public uint LanguageIteration;
|
|
public ulong TocOffset;
|
|
public uint TocCount;
|
|
public uint BakeToolVersion;
|
|
|
|
public PakHeader() { }
|
|
|
|
public void WriteTo(Span<byte> dest) {
|
|
if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[0..4], MagicValue);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[4..8], FormatVersion);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[8..12], PortalIteration);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[12..16], CellIteration);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], HighResIteration);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], LanguageIteration);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[24..32], TocOffset);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[32..36], TocCount);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[36..40], BakeToolVersion);
|
|
dest[40..64].Clear(); // reserved, zero
|
|
}
|
|
|
|
public void WriteTo(Stream stream) {
|
|
Span<byte> buf = stackalloc byte[Size];
|
|
WriteTo(buf);
|
|
stream.Write(buf);
|
|
}
|
|
|
|
public static PakHeader ReadFrom(ReadOnlySpan<byte> src) {
|
|
if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src));
|
|
var magic = BinaryPrimitives.ReadUInt32LittleEndian(src[0..4]);
|
|
if (magic != MagicValue) {
|
|
throw new InvalidDataException($"pak header magic mismatch: expected 0x{MagicValue:X8}, got 0x{magic:X8}");
|
|
}
|
|
return new PakHeader {
|
|
FormatVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[4..8]),
|
|
PortalIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[8..12]),
|
|
CellIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[12..16]),
|
|
HighResIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]),
|
|
LanguageIteration = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]),
|
|
TocOffset = BinaryPrimitives.ReadUInt64LittleEndian(src[24..32]),
|
|
TocCount = BinaryPrimitives.ReadUInt32LittleEndian(src[32..36]),
|
|
BakeToolVersion = BinaryPrimitives.ReadUInt32LittleEndian(src[36..40]),
|
|
};
|
|
}
|
|
|
|
public static PakHeader ReadFrom(Stream stream) {
|
|
Span<byte> buf = stackalloc byte[Size];
|
|
stream.ReadExactly(buf);
|
|
return ReadFrom((ReadOnlySpan<byte>)buf);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// One 24-byte TOC entry: <c>key u64, offset u64, lengthAndFlags u32, crc32 u32</c>.
|
|
/// Entries in a pak's TOC are sorted ascending by <see cref="Key"/> to allow
|
|
/// binary-search lookup. <see cref="Crc32"/> is a corruption tripwire computed
|
|
/// over the blob bytes; the reader verifies lazily on first access.
|
|
/// </summary>
|
|
public struct PakTocEntry {
|
|
public const int Size = 24;
|
|
public const uint CompressionFlag = 0x8000_0000u;
|
|
public const uint StoredLengthMask = 0x7FFF_FFFFu;
|
|
|
|
public ulong Key;
|
|
public ulong Offset;
|
|
public uint Length;
|
|
public uint Crc32;
|
|
|
|
/// <summary>
|
|
/// Format-2 entries use the high bit of <see cref="Length"/> to mark an
|
|
/// independently Brotli-compressed blob. The remaining 31 bits are the
|
|
/// exact bytes stored in the file (including the compressed blob's
|
|
/// four-byte decoded-length prefix). Raw entries retain their historical
|
|
/// length representation and require no second copy on read.
|
|
/// </summary>
|
|
public readonly bool IsCompressed => (Length & CompressionFlag) != 0;
|
|
|
|
public readonly uint StoredLength => Length & StoredLengthMask;
|
|
|
|
public void WriteTo(Span<byte> dest) {
|
|
if (dest.Length < Size) throw new ArgumentException($"destination must be at least {Size} bytes", nameof(dest));
|
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[0..8], Key);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(dest[8..16], Offset);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[16..20], Length);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(dest[20..24], Crc32);
|
|
}
|
|
|
|
public void WriteTo(Stream stream) {
|
|
Span<byte> buf = stackalloc byte[Size];
|
|
WriteTo(buf);
|
|
stream.Write(buf);
|
|
}
|
|
|
|
public static PakTocEntry ReadFrom(ReadOnlySpan<byte> src) {
|
|
if (src.Length < Size) throw new ArgumentException($"source must be at least {Size} bytes", nameof(src));
|
|
return new PakTocEntry {
|
|
Key = BinaryPrimitives.ReadUInt64LittleEndian(src[0..8]),
|
|
Offset = BinaryPrimitives.ReadUInt64LittleEndian(src[8..16]),
|
|
Length = BinaryPrimitives.ReadUInt32LittleEndian(src[16..20]),
|
|
Crc32 = BinaryPrimitives.ReadUInt32LittleEndian(src[20..24]),
|
|
};
|
|
}
|
|
}
|