Files
sharpemu/src/SharpEmu.HLE/ModuleManager.cs
T
kostyaff c0fd6a80e8 Astro Bot shader type 4, pthread_cond_timedwait, and HLE/memory/cpu bug fixes (#40)
* [agc] Add shader type 4 (GS) and register defaults v13 support

Astro Bot (#11) crashes on boot due to two missing GPU features:

1. Shader type 4 (Geometry Shader) — SPI_SHADER_PGM_LO/HI register
   offsets 0x8A/0x8B were missing. Added constants and switch cases
   for shader type 4 in GetExpectedSpiShaderPgmLo/Hi. Also added
   type 4 to IsEsGeometryShaderType (2 or 4 or 6).

2. Register defaults version 13 — was not recognized as supported.
   Added RegisterDefaultsVersion13 constant and included it in
   IsSupportedRegisterDefaultsVersion.

* [kernel] Add POSIX pthread_cond_timedwait export

SILENT HILL (#4) and Poppy Playtime (#3) crash on boot due to
missing POSIX pthread_cond_timedwait (NID 27bAgiJmOh0).

The Sony wrapper scePthreadCondTimedwait (NID BmMjYxmew1w) was
already implemented, but the raw POSIX symbol was not exported.
Added [SysAbiExport] for pthread_cond_timedwait delegating to
existing PthreadCondWaitCore with timed: true.

* [memory] Fix FlushInstructionCache null process handle

PhysicalVirtualMemory.cs called FlushInstructionCache with null as the
process handle in two places (SetProtection and TryWriteExclusive).
On Windows, a null handle does not reliably resolve to the current
process — the correct call is GetCurrentProcess() (pseudo-handle -1).

Also corrected the P/Invoke signature:
- Changed return type from void to bool with [return: MarshalAs(Bool)]
- Added SetLastError = true
- Added GetCurrentProcess() P/Invoke import

This matches the pattern already used in DirectExecutionBackend.cs
which correctly passes GetCurrentProcess() to all FlushInstructionCache
calls.

* [hle] Distinguish NOT_FOUND from NOT_IMPLEMENTED and log duplicate NIDs

Three diagnostic improvements to the HLE dispatch path:

1. ModuleManager.RegisterFromAssembly — duplicate NID registration was
   silently skipped (dispatchTable first-wins, exportTable last-wins,
   causing metadata divergence). Now logs a warning with the NID and
   export name so conflicts are visible.

2. ModuleManager.TryDispatch — generation mismatch returned
   ORBIS_GEN2_ERROR_NOT_FOUND, conflating 'function does not exist'
   with 'function exists but not for this generation'. Now returns
   ORBIS_GEN2_ERROR_NOT_IMPLEMENTED for generation mismatch, matching
   the existing convention in CpuDispatcher. Also adds debug logging
   for both NOT_FOUND and NOT_IMPLEMENTED paths.

3. DirectExecutionBackend.Imports.cs — the import dispatch else-branch
   (the actual hot path that bypasses ModuleManager.TyDispatch via
   cached export) had the same conflation. Split into:
   - else if (export exists but generation mismatch) → NOT_IMPLEMENTED
   - else (no export at all) → NOT_FOUND
   This makes runtime diagnostics correctly distinguish missing exports
   from generation-unsupported exports.

* [cpu] Check VirtualProtect return values in all stub creation paths

9 VirtualProtect calls in DirectExecutionBackend.cs had unchecked
return values. If VirtualProtect silently fails, memory protection
remains incorrect — stubs allocated with PAGE_EXECUTE_READWRITE (0x40)
never get downgraded to PAGE_EXECUTE_READ (0x20), or guest thread
entry stubs never get upgraded to writable. This causes access
violations on next execution or silent data corruption.

Fixed all 9 sites with proper error handling:
- 6 stub creation methods (return 0 on failure + log error)
- 2 guest thread entry methods (set reason + return Exception)
- 1 guest entry method (set LastError + return MEMORY_FAULT)

Stub creation sites fixed:
- CreateImportDispatchStub (line ~1683)
- EnsureTlsHandler (void, log + return)
- CreateUnresolvedReturnStub (return 0)
- CreateGuestReturnStub (return 0)
- CreateExceptionHandlerTrampoline (return 0)
- CreateTlsStoreHelperStub (return 0)

Guest thread entry sites fixed:
- StartGuestThreadNativeCall (return Exception)
- StartGuestContinuationNativeCall (return Exception)
- RunGuestEntryPoint (return MEMORY_FAULT)

* [kernel] Remove unused duplicate _nextFileDescriptor field

KernelExports.cs declared _nextFileDescriptor but never used it.
The actual field used for file descriptor allocation lives in
KernelMemoryCompatExports.cs (lines 1314, 1337). This was a dead
duplicate causing CS0414 warning.

Build is now 0 errors, 0 warnings.

---------

Co-authored-by: Hermes Atlas <hermesatlas@example.com>
2026-07-10 21:48:50 +03:00

240 lines
8.5 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Reflection;
namespace SharpEmu.HLE;
public sealed class ModuleManager : IModuleManager
{
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
private readonly object _registrationGate = new();
private bool _isFrozen;
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
{
ArgumentNullException.ThrowIfNull(assembly);
lock (_registrationGate)
{
if (_isFrozen)
{
throw new InvalidOperationException("Module registration is frozen.");
}
var registeredCount = 0;
var instances = new Dictionary<Type, object>();
foreach (var type in assembly.GetTypes())
{
foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static))
{
var exportAttribute = method.GetCustomAttribute<SysAbiExportAttribute>(inherit: false);
if (exportAttribute is null)
{
continue;
}
var exportInfo = ResolveExportInfo(exportAttribute, method, generation, symbolCatalog);
if (exportInfo is null)
{
continue;
}
var handler = CreateHandler(type, method, instances);
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
{
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
continue;
}
_exportTable[exportInfo.Value.Nid] = new ExportedFunction(
exportInfo.Value.LibraryName,
exportInfo.Value.Nid,
exportInfo.Value.ExportName,
exportInfo.Value.Target,
(SysAbiFunction)handler);
_exportNameTable.TryAdd(exportInfo.Value.ExportName, _exportTable[exportInfo.Value.Nid]);
registeredCount++;
}
}
return registeredCount;
}
}
public void Freeze()
{
lock (_registrationGate)
{
_isFrozen = true;
}
}
public bool TryGetFunction(string nid, out Delegate function)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
return _dispatchTable.TryGetValue(nid, out function!);
}
public bool TryGetExport(string nid, out ExportedFunction export)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
return _exportTable.TryGetValue(nid, out export!);
}
public bool TryGetExportByName(string exportName, out ExportedFunction export)
{
ArgumentException.ThrowIfNullOrWhiteSpace(exportName);
return _exportNameTable.TryGetValue(exportName, out export!);
}
public OrbisGen2Result Dispatch(string nid, CpuContext context)
{
TryDispatch(nid, context, out var result);
return result;
}
public bool TryDispatch(string nid, CpuContext context, out OrbisGen2Result result)
{
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
ArgumentNullException.ThrowIfNull(context);
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
{
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
}
if ((export.Target & context.TargetGeneration) == 0)
{
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
return false;
}
context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context);
if (!context.WasRaxWritten)
{
context[CpuRegister.Rax] = unchecked((ulong)ret);
}
result = (OrbisGen2Result)ret;
return true;
}
private static Delegate CreateHandler(Type ownerType, MethodInfo method, IDictionary<Type, object> instances)
{
ValidateSignature(method);
object? target = null;
if (!method.IsStatic)
{
if (!instances.TryGetValue(ownerType, out target))
{
target = Activator.CreateInstance(ownerType)
?? throw new InvalidOperationException($"Cannot instantiate module type: {ownerType.FullName}");
instances.Add(ownerType, target);
}
}
var parameterCount = method.GetParameters().Length;
if (parameterCount == 0)
{
var noArg = method.IsStatic
? (Func<int>)method.CreateDelegate(typeof(Func<int>))
: (Func<int>)method.CreateDelegate(typeof(Func<int>), target!);
SysAbiFunction adapter = _ => noArg();
return adapter;
}
return method.IsStatic
? method.CreateDelegate(typeof(SysAbiFunction))
: method.CreateDelegate(typeof(SysAbiFunction), target!);
}
private static void ValidateSignature(MethodInfo method)
{
if (method.ReturnType != typeof(int))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must return int.");
}
var parameters = method.GetParameters();
if (parameters.Length == 0)
{
return;
}
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(CpuContext))
{
return;
}
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must accept no arguments or one {nameof(CpuContext)} argument.");
}
private static ExportInfo? ResolveExportInfo(
SysAbiExportAttribute exportAttribute,
MethodInfo method,
Generation generation,
ISymbolCatalog? symbolCatalog)
{
var target = exportAttribute.Target == Generation.None
? generation
: exportAttribute.Target;
if ((target & generation) == 0)
{
return null;
}
var nid = exportAttribute.Nid;
var exportName = exportAttribute.ExportName;
if (string.IsNullOrWhiteSpace(nid) && !string.IsNullOrWhiteSpace(exportName) && symbolCatalog?.TryGetByExportName(exportName, out var byName) == true)
{
nid = byName.Nid;
}
if (!string.IsNullOrWhiteSpace(nid) && symbolCatalog?.TryGetByNid(nid, out var byNid) == true)
{
exportName = string.IsNullOrWhiteSpace(exportName) ? byNid.ExportName : exportName;
target = exportAttribute.Target == Generation.None ? byNid.Target : target;
}
if (string.IsNullOrWhiteSpace(nid))
{
throw new InvalidOperationException(
$"Method {method.DeclaringType?.FullName}.{method.Name} must define a NID or match one in symbols catalog.");
}
if (string.IsNullOrWhiteSpace(exportName))
{
exportName = method.Name;
}
if ((target & generation) == 0)
{
return null;
}
var libraryName = string.IsNullOrWhiteSpace(exportAttribute.LibraryName) ? "libKernel" : exportAttribute.LibraryName;
return new ExportInfo(nid, exportName, libraryName, target);
}
private readonly record struct ExportInfo(string Nid, string ExportName, string LibraryName, Generation Target);
}