mirror of
https://github.com/par274/sharpemu.git
synced 2026-08-01 15:39:47 +08:00
[AGC] Quake rendering progress: WAIT_REG_MEM, draw fixes, VideoOut, and HLE improvements (#68)
* [agc] WAIT_REG_MEM suspend/resume, draw packet fixes, new HLE exports, debug cleanup
Rebased onto upstream 79a7437 (par274/sharpemu, rewritten history).
- GpuWaitRegistry: DCBs suspended on unsatisfied WAIT_REG_MEM are re-polled
against guest memory on every submit; fixed 64-bit and standard packet parse
offsets, apply the mask, treat PM4 compare function 0 as "always".
- TryReadSubmittedDrawCount: accept the 5-dword ItDrawIndex2 form emitted by
DcbDrawIndex (count at +4); menu draws were silently discarded before.
- sceAgcDriverSubmitMultiDcbs: reversed ABI (rdi=address array, rsi=dword
sizes, rdx=count).
- VideoOut: vblank events, sceVideoOutGetFlipStatus, buffers registered via
sceVideoOutRegisterBuffers are valid flip targets.
- New HLE: libc stdio (fopen/fread/fseek/ftell/fclose/fgets), Dinkumware
_Getpctype ctype table, NpTrophy2 stubs, AMPR PAK sequential-read tracker,
MsgDialog lifecycle, NGS2 alt NIDs + dummy vtable for handle objects,
guarded memset intrinsic, abort()/strcasecmp null-arg recovery.
- Removed investigation-only code (INT3 breakpoints, qfont/mcpp dumps,
error-candidate printf traces, unconditional debug logs).
First rendered frame: Quake (PPSA01880) presents a 1920x1080 guest frame.
* Implemented a guarded native intrinsic (rep movsb) in DirectExecutionBackend to bypass HLE dispatch overhead, while preserving memory safety checks.
This commit is contained in:
@@ -267,19 +267,53 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||
ulong bytesRead = 0;
|
||||
|
||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
||||
// speculative reads and only consume the bytes on success paths.
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
if (destination != 0 && size > 0)
|
||||
{
|
||||
int chunkSize = (int)Math.Min(size, 4096);
|
||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||
zeros.Clear();
|
||||
while (bytesRead < size)
|
||||
{
|
||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += (ulong)currentChunk;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||
// Offset -1 means "continue after the previous read of this file id".
|
||||
if (fileOffset == unchecked((ulong)(long)-1))
|
||||
{
|
||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||
}
|
||||
else if (fileOffset > long.MaxValue)
|
||||
{
|
||||
fileOffset = 0;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||
|
||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ampr;
|
||||
|
||||
/// <summary>
|
||||
/// sceAmprAprCommandBufferReadFile carries -1 instead of an absolute offset whenever the guest
|
||||
/// wants "the next sequential chunk" of a file. For an id Software PACK archive that sequence is
|
||||
/// always header -> directory table -> individual archived files, and the guest never tells us
|
||||
/// which archived file it wants next - only the byte count it expects, taken from the very
|
||||
/// directory entry it already parsed on its side. This tracks a per-fileId read cursor, and once
|
||||
/// the directory table has streamed past, parses it so later reads can be matched back to their
|
||||
/// real absolute offset by the size the guest asks for.
|
||||
/// </summary>
|
||||
internal static class PakDirectoryTracker
|
||||
{
|
||||
private const int DirectoryEntrySize = 64;
|
||||
private const int DirectoryEntryNameLength = 56;
|
||||
|
||||
private static readonly bool _trace =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal) ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class DirectoryEntry(string name, uint filePos, uint fileLen)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public uint FilePos { get; } = filePos;
|
||||
public uint FileLen { get; } = fileLen;
|
||||
public bool Consumed { get; set; }
|
||||
}
|
||||
|
||||
private sealed class FileState
|
||||
{
|
||||
public ulong NextOffset;
|
||||
public bool ExpectingDirectory;
|
||||
public List<DirectoryEntry>? Directory;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, FileState> _state = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves what "read the next sequential chunk of size <paramref name="requestedSize"/>"
|
||||
/// really means for this fileId: an unconsumed directory entry whose length matches, if the
|
||||
/// directory has already been parsed, otherwise the tracked linear cursor.
|
||||
/// </summary>
|
||||
public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
|
||||
{
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.Directory is { } entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
||||
{
|
||||
entry.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
|
||||
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
|
||||
}
|
||||
|
||||
return entry.FilePos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state.NextOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feeds back a completed read so the tracker can advance its cursor, recognize a PACK
|
||||
/// header and jump straight to its directory table, or parse the directory table itself
|
||||
/// once it streams past.
|
||||
/// </summary>
|
||||
public static void OnReadCompleted(CpuContext ctx, uint fileId, ulong destination, ulong fileOffset, ulong bytesRead)
|
||||
{
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.ExpectingDirectory && fileOffset == state.NextOffset)
|
||||
{
|
||||
state.Directory = TryParseDirectory(ctx, destination, bytesRead);
|
||||
state.ExpectingDirectory = false;
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_parsed: id=0x{fileId:X8} entries={state.Directory?.Count ?? 0}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileOffset == 0 && bytesRead >= 12 && TryReadPackHeader(ctx, destination, out var dirOffset))
|
||||
{
|
||||
state.NextOffset = dirOffset;
|
||||
state.ExpectingDirectory = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
}
|
||||
|
||||
private static bool TryReadPackHeader(CpuContext ctx, ulong destination, out ulong dirOffset)
|
||||
{
|
||||
dirOffset = 0;
|
||||
Span<byte> header = stackalloc byte[12];
|
||||
if (!ctx.Memory.TryRead(destination, header) ||
|
||||
header[0] != (byte)'P' || header[1] != (byte)'A' || header[2] != (byte)'C' || header[3] != (byte)'K')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dirOffset = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<DirectoryEntry>? TryParseDirectory(CpuContext ctx, ulong destination, ulong length)
|
||||
{
|
||||
var count = (int)(length / DirectoryEntrySize);
|
||||
if (count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entries = new List<DirectoryEntry>(count);
|
||||
Span<byte> record = stackalloc byte[DirectoryEntrySize];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(destination + (ulong)(i * DirectoryEntrySize), record))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var nameBytes = record[..DirectoryEntryNameLength];
|
||||
var nullIndex = nameBytes.IndexOf((byte)0);
|
||||
var name = Encoding.ASCII.GetString(nullIndex >= 0 ? nameBytes[..nullIndex] : nameBytes);
|
||||
var filePos = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(56, 4));
|
||||
var fileLen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(60, 4));
|
||||
entries.Add(new DirectoryEntry(name, filePos, fileLen));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user