mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-16 05:21:32 +00:00
* [Tests] Add SharpEmu.Libs.Tests project Introduce an xunit project for the HLE libs with a minimal ICpuMemory fake, so library-level exports and helpers can be exercised without a live guest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * [Ampr] Disambiguate pak size-collisions by read locality PakDirectoryTracker resolves a sequential AMPR read (offset -1) back to an absolute pak offset by matching the requested byte count against the PACK directory. When several files share that byte count it took the first unconsumed match in directory order, which mis-resolves out-of-order reads: progs/h_ogre.mdl and bots/navigation/death32c.nav are both 0x3A34 bytes, and death32c.nav sits earlier in the directory and is never read during Quake's intro demo, so requesting h_ogre.mdl returned the nav file's bytes. The engine then parsed "NAV2" as a brush model, failed the version check and aborted. Pick the unconsumed same-size entry nearest the running read cursor instead. id archives cluster related assets and the guest streams them with locality, so this lands on the intended file; contiguous same-size runs (the gfx/weapons/ww_*.lmp icons) still resolve in packed order. Verified against a Quake dump: the abort is gone, h_ogre.mdl reads correctly, and the intro demo reaches its main loop and renders instead of dying at the error dialog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
109 lines
5.1 KiB
C#
109 lines
5.1 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Text;
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Ampr;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Ampr;
|
|
|
|
// PakDirectoryTracker reconstructs the absolute pak offset for a "next sequential chunk" read
|
|
// (offset -1) from the requested byte count. When several archived files share that byte count,
|
|
// picking by directory order alone mis-resolves out-of-order reads: Quake requested progs/h_ogre.mdl
|
|
// (0x3A34 bytes) but the tracker returned bots/navigation/death32c.nav, which shares the size and
|
|
// sits earlier in the directory, so the guest parsed NAV2 data as a brush model and aborted.
|
|
public sealed class PakDirectoryTrackerTests
|
|
{
|
|
private const int EntrySize = 64;
|
|
private const int NameLength = 56;
|
|
|
|
private readonly record struct PakEntry(string Name, uint FilePos, uint FileLen);
|
|
|
|
[Fact]
|
|
public void ResolveSequentialOffset_SizeCollision_PicksEntryNearestReadCursor()
|
|
{
|
|
const uint fileId = 0x5AA5_0001;
|
|
const uint collidingLen = 0x3A34;
|
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
// "far" collides in size but lives early in the pak; "near" is what the guest actually wants.
|
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
|
{
|
|
new PakEntry("far.dat", FilePos: 0x1000, FileLen: collidingLen),
|
|
new PakEntry("near.dat", FilePos: 0x9000, FileLen: collidingLen),
|
|
});
|
|
|
|
// Advance the read cursor next to the "near" entry, mimicking a burst of nearby asset reads.
|
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: 0x8800, bytesRead: 0x400);
|
|
|
|
Assert.Equal(0x9000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
|
// Once the near entry is consumed, the same size resolves to the remaining match.
|
|
Assert.Equal(0x1000UL, PakDirectoryTracker.ResolveSequentialOffset(fileId, collidingLen));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveSequentialOffset_ContiguousSameSizeRun_StaysInOrder()
|
|
{
|
|
const uint fileId = 0x5AA5_0002;
|
|
const uint runLen = 0x608;
|
|
var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000);
|
|
var ctx = new CpuContext(memory, Generation.Gen5);
|
|
|
|
// A contiguous run of equal-size lumps (like Quake's gfx/weapons/ww_*.lmp) must still resolve
|
|
// in packed order when the guest streams them sequentially.
|
|
LoadDirectory(ctx, fileId, memory, 0x1_0000_0000, dirFileOffset: 0x400, new[]
|
|
{
|
|
new PakEntry("lump_a", FilePos: 0x1000, FileLen: runLen),
|
|
new PakEntry("lump_b", FilePos: 0x1000 + runLen, FileLen: runLen),
|
|
new PakEntry("lump_c", FilePos: 0x1000 + (runLen * 2), FileLen: runLen),
|
|
});
|
|
|
|
var first = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: first, bytesRead: runLen);
|
|
var second = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination: 0x1_0000_0000, fileOffset: second, bytesRead: runLen);
|
|
var third = PakDirectoryTracker.ResolveSequentialOffset(fileId, runLen);
|
|
|
|
Assert.Equal(0x1000UL, first);
|
|
Assert.Equal(0x1000UL + runLen, second);
|
|
Assert.Equal(0x1000UL + (runLen * 2), third);
|
|
}
|
|
|
|
// Feeds the tracker a synthetic PACK header + directory table exactly as the AMPR read path does:
|
|
// first the 12-byte header (which arms directory parsing), then the directory records themselves.
|
|
private static void LoadDirectory(
|
|
CpuContext ctx,
|
|
uint fileId,
|
|
FakeCpuMemory memory,
|
|
ulong destination,
|
|
ulong dirFileOffset,
|
|
PakEntry[] entries)
|
|
{
|
|
Span<byte> header = stackalloc byte[12];
|
|
header[0] = (byte)'P';
|
|
header[1] = (byte)'A';
|
|
header[2] = (byte)'C';
|
|
header[3] = (byte)'K';
|
|
BinaryPrimitives.WriteUInt32LittleEndian(header[4..8], (uint)dirFileOffset);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(header[8..12], (uint)(entries.Length * EntrySize));
|
|
memory.TryWrite(destination, header);
|
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset: 0, bytesRead: 12);
|
|
|
|
var table = new byte[entries.Length * EntrySize];
|
|
for (var i = 0; i < entries.Length; i++)
|
|
{
|
|
var record = table.AsSpan(i * EntrySize, EntrySize);
|
|
var name = Encoding.ASCII.GetBytes(entries[i].Name);
|
|
name.AsSpan(0, Math.Min(name.Length, NameLength)).CopyTo(record);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(56, 4), entries[i].FilePos);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(60, 4), entries[i].FileLen);
|
|
}
|
|
|
|
memory.TryWrite(destination, table);
|
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, dirFileOffset, (ulong)table.Length);
|
|
}
|
|
}
|