diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index e6917041..b64177a2 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -598,7 +598,7 @@ public sealed partial class DirectExecutionBackend } *(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub); - if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal)) + if (_logFiber) { Console.Error.WriteLine( $"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " + @@ -1292,7 +1292,7 @@ public sealed partial class DirectExecutionBackend } int returnValue; - if (IsNoBlockLeafImport(importStubEntry.Nid)) + if (importStubEntry.IsNoBlockLeaf) { cpuContext.ClearRaxWriteFlag(); returnValue = export.Function(cpuContext); diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index df700987..3e51f180 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -48,6 +48,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I // hashing are hoisted to stub-setup time. public bool IsLeaf { get; } + public bool IsNoBlockLeaf { get; } + public bool SuppressStrlenTrace { get; } public bool IsLoopGuardBoundary { get; } @@ -59,6 +61,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I string nid, ExportedFunction? export, bool isLeaf, + bool isNoBlockLeaf, bool suppressStrlenTrace, bool isLoopGuardBoundary, ulong nidHash) @@ -67,6 +70,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I Nid = nid; Export = export; IsLeaf = isLeaf; + IsNoBlockLeaf = isNoBlockLeaf; SuppressStrlenTrace = suppressStrlenTrace; IsLoopGuardBoundary = isLoopGuardBoundary; NidHash = nidHash; @@ -318,6 +322,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I private bool _logUsleep; + private bool _logFiber; + private bool _logBootstrap; private bool _logAllImports; @@ -1067,6 +1073,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I _ignoredGuestInt41Count = 0; _logGuestThreads = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal); _logUsleep = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal); + _logFiber = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal); _logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal); _logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal); _logImportFrames = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal); @@ -1164,6 +1171,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I text2, resolvedExport, IsLeafImport(text2), + IsNoBlockLeafImport(text2), ShouldSuppressStrlenTrace(text2), IsImportLoopGuardBoundary(text2), StableHash64(text2)); @@ -3264,31 +3272,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { Pump(callerContext, reason); - var threads = SnapshotGuestThreads(); - if (threads.Length == 0) + // Tally run states under the lock without allocating a snapshot every + // spin (this loop can iterate rapidly); the full snapshot is only + // materialized for the gated diagnostic dump below. + GetGuestThreadActivity(out var threadCount, out var hasReadyThread, out var hasRunningThread, out var hasBlockedThread); + if (threadCount == 0) { return; } - var hasReadyThread = false; - var hasRunningThread = false; - var hasBlockedThread = false; - foreach (var thread in threads) - { - switch (thread.State) - { - case GuestThreadRunState.Ready: - hasReadyThread = true; - break; - case GuestThreadRunState.Running: - hasRunningThread = true; - break; - case GuestThreadRunState.Blocked: - hasBlockedThread = true; - break; - } - } - if (hasReadyThread) { continue; @@ -3301,7 +3293,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I if (_logGuestThreads && Stopwatch.GetTimestamp() >= nextSnapshotTimestamp) { - foreach (var thread in threads) + foreach (var thread in SnapshotGuestThreads()) { Console.Error.WriteLine( $"[LOADER][TRACE] guest_thread.idle_wait reason={reason} handle=0x{thread.ThreadHandle:X16} " + @@ -3321,7 +3313,36 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I { using (LockGate("SnapshotGuestThreads")) { - return _guestThreads.Values.ToArray(); + var snapshot = new GuestThreadState[_guestThreads.Count]; + _guestThreads.Values.CopyTo(snapshot, 0); + return snapshot; + } + } + + // Allocation-free run-state tally for the idle spin loop. + private void GetGuestThreadActivity(out int count, out bool hasReady, out bool hasRunning, out bool hasBlocked) + { + hasReady = false; + hasRunning = false; + hasBlocked = false; + using (LockGate("GetGuestThreadActivity")) + { + count = _guestThreads.Count; + foreach (var thread in _guestThreads.Values) + { + switch (thread.State) + { + case GuestThreadRunState.Ready: + hasReady = true; + break; + case GuestThreadRunState.Running: + hasRunning = true; + break; + case GuestThreadRunState.Blocked: + hasBlocked = true; + break; + } + } } } diff --git a/src/SharpEmu.Libs/Agc/AgcExports.cs b/src/SharpEmu.Libs/Agc/AgcExports.cs index c7c8fc7b..9a8f1b37 100644 --- a/src/SharpEmu.Libs/Agc/AgcExports.cs +++ b/src/SharpEmu.Libs/Agc/AgcExports.cs @@ -5937,16 +5937,30 @@ public static partial class AgcExports // draw a multi-render-target G-buffer (up to eight slots) in one pass. // Fall back to slot 0 if we cannot match any export to a bound target. var allBoundTargets = GetRenderTargets(state.CxRegisters); - var renderTargets = allBoundTargets - .Where(target => HasPixelColorExport(pixelState, target.Slot)) - .OrderBy(target => target.Slot) - .ToArray(); - if (renderTargets.Length == 0) + // At most 8 slots; a manual filter avoids the per-draw LINQ iterator/ + // closure allocations. Slots are distinct, so sorting by slot is stable. + var selectedTargets = new List(allBoundTargets.Count); + foreach (var target in allBoundTargets) { - renderTargets = allBoundTargets - .Where(target => target.Slot == 0) - .ToArray(); + if (HasPixelColorExport(pixelState, target.Slot)) + { + selectedTargets.Add(target); + } } + + if (selectedTargets.Count == 0) + { + foreach (var target in allBoundTargets) + { + if (target.Slot == 0) + { + selectedTargets.Add(target); + } + } + } + + selectedTargets.Sort(static (left, right) => left.Slot.CompareTo(right.Slot)); + var renderTargets = selectedTargets.ToArray(); if (_traceAgcShader && allBoundTargets.Count > 1) { TraceAgcShader( @@ -6398,12 +6412,23 @@ public static partial class AgcExports private static bool HasPixelColorExport(Gen5ShaderState state, uint target) => GetPixelColorExportMask(state, target) != 0; - private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) => - state.Program.Instructions - .Select(instruction => instruction.Control) - .OfType() - .Where(export => export.Target == target) - .Aggregate(0u, (mask, export) => mask | (export.EnableMask & 0xFu)); + private static uint GetPixelColorExportMask(Gen5ShaderState state, uint target) + { + // Called per render target (twice per draw via CreateRenderState + + // HasPixelColorExport); a manual scan avoids the per-call LINQ iterator + // and closure allocations. Same result as the previous + // Select/OfType/Where/Aggregate chain. + var mask = 0u; + foreach (var instruction in state.Program.Instructions) + { + if (instruction.Control is Gen5ExportControl export && export.Target == target) + { + mask |= export.EnableMask & 0xFu; + } + } + + return mask; + } private static uint GetInterpolatedAttributeCount(Gen5ShaderState state) { diff --git a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs index 552df525..d993c960 100644 --- a/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelEventFlagCompatExports.cs @@ -25,6 +25,11 @@ public static class KernelEventFlagCompatExports private static readonly ConcurrentDictionary _eventFlags = new(); private static long _nextEventFlagHandle = 1; + // Cached once: gating every call site avoids building the interpolated + // trace string (and FormatFrameChain/FormatGuestWaitObject) when disabled. + private static readonly bool _traceEventFlag = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal); + private sealed class EventFlagState { public required string Name { get; init; } @@ -79,7 +84,7 @@ public static class KernelEventFlagCompatExports return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}"); + if (_traceEventFlag) TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } @@ -101,7 +106,7 @@ public static class KernelEventFlagCompatExports Monitor.PulseAll(state.Gate); } - TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'"); + if (_traceEventFlag) TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } @@ -124,7 +129,7 @@ public static class KernelEventFlagCompatExports { state.Bits |= pattern; Monitor.PulseAll(state.Gate); - TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); + if (_traceEventFlag) TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); } _ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle)); @@ -148,7 +153,7 @@ public static class KernelEventFlagCompatExports lock (state.Gate) { state.Bits &= pattern; - TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}"); + if (_traceEventFlag) TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}"); } return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); @@ -189,7 +194,7 @@ public static class KernelEventFlagCompatExports } ApplyClearMode(state, pattern, waitMode); - TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}"); + if (_traceEventFlag) TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } } @@ -287,8 +292,8 @@ public static class KernelEventFlagCompatExports return true; }, deadline); - TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); - TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); + if (_traceEventFlag) TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}"); + if (_traceEventFlag) TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}"); if (!requestedBlock) { var scheduler = GuestThreadExecution.Scheduler; @@ -298,7 +303,7 @@ public static class KernelEventFlagCompatExports } state.WaitingThreads++; - TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); + if (_traceEventFlag) TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); var releaseWaiter = true; try { @@ -318,7 +323,7 @@ public static class KernelEventFlagCompatExports { state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); releaseWaiter = false; - TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}"); + if (_traceEventFlag) TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}"); return SetReturn(ctx, pumpedWaitResult); } @@ -329,7 +334,7 @@ public static class KernelEventFlagCompatExports releaseWaiter = false; _ = TryWriteUInt32(ctx, timeoutAddress, 0); _ = TryWriteResultPattern(ctx, resultAddress, state.Bits); - TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); + if (_traceEventFlag) TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT); } @@ -346,7 +351,7 @@ public static class KernelEventFlagCompatExports } state.WaitingThreads++; - TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); + if (_traceEventFlag) TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}"); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK); } finally @@ -381,7 +386,7 @@ public static class KernelEventFlagCompatExports state.Bits = setPattern; state.WaitingThreads = 0; Monitor.PulseAll(state.Gate); - TraceEventFlag( + if (_traceEventFlag) TraceEventFlag( $"cancel handle=0x{handle:X16} bits=0x{setPattern:X16} " + $"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} ret=0x{GetCurrentReturnRip():X16}"); } @@ -476,7 +481,7 @@ public static class KernelEventFlagCompatExports } state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1); - TraceEventFlag( + if (_traceEventFlag) TraceEventFlag( $"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}"); return true; } @@ -568,7 +573,7 @@ public static class KernelEventFlagCompatExports private static void TraceEventFlag(string message) { - if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal)) + if (_traceEventFlag) { Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}"); } diff --git a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs index 4a658346..c7a86301 100644 --- a/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs +++ b/src/SharpEmu.Libs/Kernel/KernelMemoryCompatExports.cs @@ -102,7 +102,11 @@ public static partial class KernelMemoryCompatExports private static readonly object _guestMountGate = new(); private static readonly Dictionary _directAllocations = new(); private static readonly Dictionary _libcAllocations = new(); - private static readonly Dictionary _mappedRegions = new(); + // Keyed by (and kept sorted on) region base address so VirtualQuery can find a + // containing/next region with a binary search instead of an O(n) scan. Every + // write uses the region's own Address as the key (see AddMappedRegionSliceLocked + // and the mmap sites), so Values enumerate in ascending address order. + private static readonly SortedList _mappedRegions = new(); private static readonly Dictionary _mappedRegionNames = new(); private static readonly Dictionary _guestMounts = new(StringComparer.OrdinalIgnoreCase); private static readonly HashSet _tracedStatResults = new(StringComparer.Ordinal); @@ -5477,7 +5481,9 @@ public static partial class KernelMemoryCompatExports var affected = new List(); var cursor = address; - foreach (var region in _mappedRegions.Values.OrderBy(static region => region.Address)) + // _mappedRegions is a SortedList keyed by address, so Values already + // enumerate in ascending address order. + foreach (var region in _mappedRegions.Values) { if (!TryAddU64(region.Address, region.Length, out var regionEnd) || regionEnd <= cursor) { @@ -5638,9 +5644,33 @@ public static partial class KernelMemoryCompatExports private static bool TryFindVirtualQueryRegionLocked(ulong queryAddress, bool findNext, out MappedRegion region) { region = default; - var foundNext = false; - foreach (var candidate in _mappedRegions.Values) + var keys = _mappedRegions.Keys; + var values = _mappedRegions.Values; + var count = keys.Count; + + // First index whose region address is >= queryAddress. + var lo = 0; + var hi = count; + while (lo < hi) { + var mid = (int)(((uint)lo + (uint)hi) >> 1); + if (keys[mid] < queryAddress) + { + lo = mid + 1; + } + else + { + hi = mid; + } + } + + // Regions do not overlap, so only the one with the greatest base address + // <= queryAddress can contain it — index lo when it starts exactly at + // queryAddress, otherwise lo - 1. + var floorIndex = (lo < count && keys[lo] == queryAddress) ? lo : lo - 1; + if (floorIndex >= 0) + { + var candidate = values[floorIndex]; if (TryAddU64(candidate.Address, candidate.Length, out var candidateEnd) && queryAddress >= candidate.Address && queryAddress < candidateEnd) @@ -5648,20 +5678,16 @@ public static partial class KernelMemoryCompatExports region = candidate; return true; } - - if (!findNext || candidate.Address < queryAddress) - { - continue; - } - - if (!foundNext || candidate.Address < region.Address) - { - region = candidate; - foundNext = true; - } } - return foundNext; + // findNext: the region with the smallest base address >= queryAddress. + if (findNext && lo < count) + { + region = values[lo]; + return true; + } + + return false; } private static void TraceDirectMemoryCall( @@ -5690,10 +5716,12 @@ public static partial class KernelMemoryCompatExports $"[LOADER][TRACE] {operation}: ret=0x{returnRip:X16} len=0x{length:X16} align=0x{alignment:X16} type=0x{memoryType:X8} out=0x{outAddress:X16} selected=0x{selectedAddress:X16} result={result?.ToString() ?? ""}"); } - private static bool ShouldTraceDirectMemory() - { - return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal); - } + // Cached once so the ~8 direct-memory call sites don't each do a + // GetEnvironmentVariable P/Invoke per operation. + private static readonly bool _traceDirectMemory = string.Equals( + Environment.GetEnvironmentVariable("SHARPEMU_LOG_DIRECT_MEMORY"), "1", StringComparison.Ordinal); + + private static bool ShouldTraceDirectMemory() => _traceDirectMemory; private static bool TryReleaseDirectMemoryRangeLocked(ulong start, ulong length) { diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 9479e23d..41c57a0c 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -608,6 +608,42 @@ internal static unsafe class VulkanVideoPresenter shaderAddress); } + // Manual scans (targets are <= 8) so the per-draw validation does not + // allocate LINQ iterators/closures or a Distinct HashSet. + private static bool AnyRenderTargetInvalid(IReadOnlyList targets) + { + foreach (var target in targets) + { + if (target.Address == 0 || target.Width == 0 || target.Height == 0) + { + return true; + } + } + + return false; + } + + private static bool RenderTargetsMismatchedOrAliased(IReadOnlyList targets, GuestRenderTarget first) + { + for (var i = 0; i < targets.Count; i++) + { + if (targets[i].Width != first.Width || targets[i].Height != first.Height) + { + return true; + } + + for (var j = i + 1; j < targets.Count; j++) + { + if (targets[i].Address == targets[j].Address) + { + return true; + } + } + } + + return false; + } + public static void SubmitOffscreenTranslatedDraw( byte[] pixelSpirv, IReadOnlyList textures, @@ -627,16 +663,13 @@ internal static unsafe class VulkanVideoPresenter if (pixelSpirv.Length == 0 || targets.Count == 0 || targets.Count > 8 || - targets.Any(target => - target.Address == 0 || target.Width == 0 || target.Height == 0)) + AnyRenderTargetInvalid(targets)) { return; } var firstTarget = targets[0]; - if (targets.Any(target => - target.Width != firstTarget.Width || target.Height != firstTarget.Height) || - targets.Select(target => target.Address).Distinct().Count() != targets.Count) + if (RenderTargetsMismatchedOrAliased(targets, firstTarget)) { Console.Error.WriteLine( "[LOADER][WARN] Vulkan skipped MRT draw with mismatched dimensions or aliased targets."); @@ -654,9 +687,11 @@ internal static unsafe class VulkanVideoPresenter var effectiveRenderState = renderState ?? GuestRenderState.Default; if (effectiveRenderState.Blends.Count == 1 && targets.Count > 1) { + var broadcastBlends = new GuestBlendState[targets.Count]; + Array.Fill(broadcastBlends, effectiveRenderState.Blends[0]); effectiveRenderState = effectiveRenderState with { - Blends = Enumerable.Repeat(effectiveRenderState.Blends[0], targets.Count).ToArray(), + Blends = broadcastBlends, }; } lock (_gate)