Compare commits

...

4 Commits

Author SHA1 Message Date
Berk 459ae7e3f7 Docs/readme images (#302)
* [readme] update showcase images

* [readme] update readme images and change version to 0.0.2
2026-07-17 02:44:28 +03:00
kuba 8f405caebe Vulkan: reduce guest buffer upload allocations (#301) 2026-07-17 02:36:39 +03:00
kuba dabf723b3e [Vulkan] Honor guest depth clear state (#290) 2026-07-17 02:29:53 +03:00
AlexC 2129a12684 [GUI] Last commit SHA (id) displayed on about section (#279)
* Latest commit info axamal structure

* Link latest commit to text

* Added localization for About>Last commit info

* Made the commit hash a button that redirects you to the commit in github

* Reorder about section

* Commit and update icon in about section to have consistency in section
2026-07-17 02:25:01 +03:00
17 changed files with 394 additions and 110 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.1</SharpEmuVersion> <SharpEmuVersion>0.0.2</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version> <Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot> <RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
+6 -14
View File
@@ -90,21 +90,13 @@ release includes the MoltenVK Vulkan implementation.
## Games Tested ## Games Tested
* **Demon's Souls Remake** | Demons Souls Remake | Dreaming Sarah |
* [Demon's Souls [PPSA01341]](https://github.com/sharpemu/sharpemu/issues/2) | :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
* Demon's Souls is now video loop. Shaders are ready to be converted to SPIR-V/Vulkan. We are continuing our work on this. | ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
![DeS videoOut submit first frame](./.github/images/des-videoout-shaders.jpg)
* **Poppy Playtime Chapter 1** | Void Terrarium | Dead Cells |
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3) | :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
* **SILENT HILL: The Short Message**
* [SILENT HILL: The Short Message [PPSA10112]](https://github.com/sharpemu/sharpemu/issues/4)
* **Dreaming Sarah**
* [Dreaming Sarah [PPSA02929]](https://github.com/sharpemu/sharpemu/issues/9)
* Real texture rendering for this game;
![Splash texture](./.github/images/dreaming-sarah.jpg)
> [!IMPORTANT] > [!IMPORTANT]
Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+2
View File
@@ -141,6 +141,8 @@
"Options.About" : "About", "Options.About" : "About",
"About.Github.Label": "GitHub", "About.Github.Label": "GitHub",
"About.Github.Desc": "Source code, issues and project development.", "About.Github.Desc": "Source code, issues and project development.",
"About.Github.LatestCommitLabel": "Latest Commit",
"About.Github.LatestCommitDescription": "Latest commit on the main branch",
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.", "About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!", "About.GithubButton": "Contribute in GitHub!",
+2
View File
@@ -131,6 +131,8 @@
"About.Github.Label": "GitHub", "About.Github.Label": "GitHub",
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.", "About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Github.LatestCommitLabel": "Último Commit",
"About.Github.LatestCommitDescription": "Último commit en la rama main",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.", "About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!", "About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!" "About.DiscordButton": "Únete a nuestro Discord!"
+25 -1
View File
@@ -333,12 +333,36 @@ SPDX-License-Identifier: GPL-2.0-or-later
Classes="sectionTitle" Classes="sectionTitle"
Text="ABOUT" /> Text="ABOUT" />
<!--Latest commit info-->
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center"> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/commit-icon.png"
Width="24" Height="24" VerticalAlignment="Center" />
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock x:Name="LatestCommitLabel" Text="Latest commit" FontSize="13"/>
<TextBlock x:Name="LatestCommitDescription" Text="Latest commit on the main branch" FontSize="11" Foreground="{StaticResource MutedBrush}"/>
</StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="LatestCommitHashText" Classes="ghost"
Content="Loading…" FontSize="12" FontFamily="Consolas,monospace"
VerticalAlignment="Center" IsEnabled="False" />
</Grid>
<!--Update-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
<Image Source="avares://SharpEmu.GUI/Assets/update-icon.png"
Width="24"
Height="24"
VerticalAlignment="Center" />
<StackPanel Spacing="2" VerticalAlignment="Center">
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" /> <TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11" <TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" /> Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost" <Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
Content="Check for updates" VerticalAlignment="Center" /> Content="Check for updates" VerticalAlignment="Center" />
</Grid> </Grid>
+109
View File
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Net.Http.Headers;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
@@ -78,6 +79,10 @@ public partial class MainWindow : Window
private long _navUpNextAt; private long _navUpNextAt;
private long _navDownNextAt; private long _navDownNextAt;
//Github http client for latest commit
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
private string? _latestCommitSha;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
@@ -195,6 +200,21 @@ public partial class MainWindow : Window
UseShellExecute = true UseShellExecute = true
}); });
}; };
LatestCommitHashText.Click += (_, _) =>
{
if (string.IsNullOrWhiteSpace(_latestCommitSha))
{
return;
}
Process.Start(new ProcessStartInfo
{
FileName =
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
UseShellExecute = true
});
};
} }
/// <summary> /// <summary>
@@ -236,6 +256,91 @@ public partial class MainWindow : Window
} }
} }
// ---- Github http client config ----
// This is for getting lash commit id
private static HttpClient CreateGithubHttpClient()
{
var client = new HttpClient
{
Timeout = TimeSpan.FromSeconds(15)
};
client.DefaultRequestHeaders.UserAgent.ParseAdd("SharpEmu/1.0");
client.DefaultRequestHeaders.Accept.Add(
new MediaTypeWithQualityHeaderValue("application/vnd.github.sha"));
client.DefaultRequestHeaders.Add(
"X-GitHub-Api-Version",
"2026-03-10");
return client;
}
private async Task LoadLatestCommitAsync()
{
const string apiUrl =
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
_latestCommitSha = null;
LatestCommitHashText.Content = "Loading…";
LatestCommitHashText.IsEnabled = false;
try
{
using var response = await GithubHttpClient.GetAsync(apiUrl);
var responseBody =
(await response.Content.ReadAsStringAsync()).Trim();
if (!response.IsSuccessStatusCode)
{
LatestCommitHashText.Content =
$"HTTP {(int)response.StatusCode}";
ToolTip.SetTip(
LatestCommitHashText,
string.IsNullOrWhiteSpace(responseBody)
? response.ReasonPhrase
: responseBody);
return;
}
if (responseBody.Length < 7)
{
LatestCommitHashText.Content = "Invalid response";
ToolTip.SetTip(LatestCommitHashText, responseBody);
return;
}
// Keep the complete SHA for the URL.
_latestCommitSha = responseBody;
// Display only the short SHA.
LatestCommitHashText.Content =
responseBody[..Math.Min(7, responseBody.Length)];
LatestCommitHashText.IsEnabled = true;
ToolTip.SetTip(
LatestCommitHashText,
$"Open commit {_latestCommitSha}");
}
catch (TaskCanceledException ex)
{
LatestCommitHashText.Content = "Timeout";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
catch (HttpRequestException ex)
{
LatestCommitHashText.Content = "Connection error";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
catch (Exception ex)
{
LatestCommitHashText.Content = "Error";
ToolTip.SetTip(LatestCommitHashText, ex.Message);
}
}
// ---- Controller navigation ---- // ---- Controller navigation ----
private void PollGamepad() private void PollGamepad()
@@ -381,6 +486,8 @@ public partial class MainWindow : Window
ApplySettingsToControls(); ApplySettingsToControls();
LocateEmulator(); LocateEmulator();
UpdateDiscordPresence(); UpdateDiscordPresence();
_ = LoadLatestCommitAsync();
if (_settings.CheckForUpdatesOnStartup) if (_settings.CheckForUpdatesOnStartup)
{ {
_ = CheckForUpdatesAsync(); _ = CheckForUpdatesAsync();
@@ -516,6 +623,8 @@ public partial class MainWindow : Window
GithubButton.Content = loc.Get("About.GithubButton"); GithubButton.Content = loc.Get("About.GithubButton");
DiscordButton.Content = loc.Get("About.DiscordButton"); DiscordButton.Content = loc.Get("About.DiscordButton");
UpdateLabel.Text = loc.Get("Updater.Label"); UpdateLabel.Text = loc.Get("Updater.Label");
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
RefreshUpdateText(); RefreshUpdateText();
UpdateEmptyStateTexts(); UpdateEmptyStateTexts();
+2
View File
@@ -32,6 +32,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" /> <AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
<AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" /> <AvaloniaResource Include="..\..\assets\images\github.png" Link="Assets/github.png" />
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" /> <AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+14 -10
View File
@@ -121,6 +121,7 @@ public static partial class AgcExports
private const uint CbBlend0Control = 0x1E0; private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292; private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4). // GFX10 DB context registers (register byte address minus 0x28000, / 4).
private const uint DbRenderControl = 0x000;
private const uint DbDepthView = 0x002; private const uint DbDepthView = 0x002;
private const uint DbDepthSizeXy = 0x007; private const uint DbDepthSizeXy = 0x007;
private const uint DbDepthClear = 0x00B; private const uint DbDepthClear = 0x00B;
@@ -5291,7 +5292,7 @@ public static partial class AgcExports
var hasDepthOnlyCandidate = hasExportShader && var hasDepthOnlyCandidate = hasExportShader &&
!hasPixelShader && !hasPixelShader &&
depthTarget is not null && depthTarget is not null &&
(depthState.TestEnable || depthState.WriteEnable); (depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
if (hasDepthOnlyCandidate && if (hasDepthOnlyCandidate &&
TryCreateTranslatedDepthOnlyGuestDraw( TryCreateTranslatedDepthOnlyGuestDraw(
ctx, ctx,
@@ -6652,27 +6653,30 @@ public static partial class AgcExports
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE // DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
// bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering). // bit2, ZFUNC bits[6:4] (GCN compare, matches Vulkan CompareOp ordering).
// DB_RENDER_CONTROL (context register 0x000): DEPTH_CLEAR_ENABLE bit0.
private const uint DbDepthControl = 0x200; private const uint DbDepthControl = 0x200;
private static GuestDepthState DecodeDepthState( internal static GuestDepthState DecodeDepthState(
IReadOnlyDictionary<uint, uint> registers) IReadOnlyDictionary<uint, uint> registers)
{ {
if (!registers.TryGetValue(DbDepthControl, out var control)) var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
{ registers.TryGetValue(DbRenderControl, out var renderControl);
return GuestDepthState.Default;
}
var testEnable = (control & 0x2u) != 0; var testEnable = (control & 0x2u) != 0;
var writeEnable = (control & 0x4u) != 0; var writeEnable = (control & 0x4u) != 0;
var compareOp = (control >> 4) & 0x7u; var compareOp = hasDepthControl
return new GuestDepthState(testEnable, writeEnable, compareOp); ? (control >> 4) & 0x7u
: GuestDepthState.Default.CompareOp;
var clearEnable = (renderControl & 0x1u) != 0;
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
} }
private static GuestDepthTarget? DecodeDepthTarget( private static GuestDepthTarget? DecodeDepthTarget(
IReadOnlyDictionary<uint, uint> registers) IReadOnlyDictionary<uint, uint> registers)
{ {
var depthState = DecodeDepthState(registers); var depthState = DecodeDepthState(registers);
if (!depthState.TestEnable && !depthState.WriteEnable) if (!depthState.TestEnable &&
!depthState.WriteEnable &&
!depthState.ClearEnable)
{ {
return null; return null;
} }
+3 -2
View File
@@ -91,9 +91,10 @@ internal readonly record struct GuestRasterState(
internal readonly record struct GuestDepthState( internal readonly record struct GuestDepthState(
bool TestEnable, bool TestEnable,
bool WriteEnable, bool WriteEnable,
uint CompareOp) uint CompareOp,
bool ClearEnable = false)
{ {
public static GuestDepthState Default { get; } = new(false, false, 7); public static GuestDepthState Default { get; } = new(false, false, 7, false);
} }
/// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the /// <summary>Factors/funcs are raw guest CB_BLEND*_CONTROL register bitfields; the
@@ -2213,6 +2213,12 @@ internal static unsafe class VulkanVideoPresenter
return keys; return keys;
} }
internal static bool ShouldAttachGuestDepth(
GuestDepthTarget? target,
GuestDepthState state) =>
target is not null &&
(state.TestEnable || state.WriteEnable || state.ClearEnable);
private readonly record struct Presentation( private readonly record struct Presentation(
byte[]? Pixels, byte[]? Pixels,
uint Width, uint Width,
@@ -4399,23 +4405,29 @@ internal static unsafe class VulkanVideoPresenter
CollectCompletedGuestSubmissions(waitForOldest: false); CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) * var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency; 1000.0 / System.Diagnostics.Stopwatch.Frequency;
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " + $"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " + $"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}"); $"waited_ms={waitedMs:F3}");
} }
}
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work) private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
{ {
WaitForActiveGuestQueueSubmissionsForCpuVisibility(); WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action(); work.Action();
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.ordered_action queue={_activeGuestQueue.Name} " + $"vk.ordered_action queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " + $"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'"); $"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
} }
}
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work) private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
{ {
@@ -7523,21 +7535,43 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint)); var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size); var endAddress = checked(guestBuffer.BaseAddress + size);
var allocation = _guestBufferAllocations GuestBufferAllocation? allocation = null;
.Where(candidate => var allocationPriority = -1;
candidate.BaseAddress <= guestBuffer.BaseAddress && // This runs for every bound global buffer. Preserve the previous
candidate.BaseAddress + candidate.Size >= endAddress) // stable queue preference without allocating LINQ sort state.
.OrderByDescending(candidate => foreach (var candidate in _guestBufferAllocations)
string.Equals( {
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
candidate.BaseAddress + candidate.Size < endAddress)
{
continue;
}
var candidatePriority = string.Equals(
candidate.QueueName, candidate.QueueName,
_activeGuestQueue.Name, _activeGuestQueue.Name,
StringComparison.Ordinal)) StringComparison.Ordinal)
.ThenByDescending(candidate => ? 2
string.Equals( : string.Equals(
candidate.QueueName, candidate.QueueName,
SharedReadOnlyGuestBufferQueue, SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal)) StringComparison.Ordinal)
.First(); ? 1
: 0;
if (candidatePriority > allocationPriority)
{
allocation = candidate;
allocationPriority = candidatePriority;
}
}
if (allocation is null)
{
throw new InvalidOperationException(
$"no Vulkan guest buffer allocation covers " +
$"0x{guestBuffer.BaseAddress:X16}-0x{endAddress:X16}");
}
var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress; var guestOffset = guestBuffer.BaseAddress - allocation.BaseAddress;
var descriptorOffset = guestOffset & var descriptorOffset = guestOffset &
~(GuestStorageBufferOffsetAlignment - 1); ~(GuestStorageBufferOffsetAlignment - 1);
@@ -7586,16 +7620,15 @@ internal static unsafe class VulkanVideoPresenter
WaitForActiveGuestQueueSubmissionsForCpuVisibility(); WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name); WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
} }
var live = new byte[guestBuffer.Length]; var mapped = new Span<byte>(
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == true) (void*)(allocation.Mapped + checked((nint)guestOffset)),
guestBuffer.Length);
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, mapped) != true)
{ {
source = live; source.CopyTo(mapped);
} }
source.CopyTo(new Span<byte>( mapped.CopyTo(shadow);
(void*)(allocation.Mapped + checked((nint)guestOffset)),
source.Length));
source.CopyTo(shadow);
} }
if (ShouldTraceVulkanResources() && if (ShouldTraceVulkanResources() &&
@@ -7605,14 +7638,6 @@ internal static unsafe class VulkanVideoPresenter
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " + $"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
$"bytes={guestBuffer.Length}"); $"bytes={guestBuffer.Length}");
} }
if (_setDebugUtilsObjectName is not null)
{
SetDebugName(
ObjectType.Buffer,
allocation.Buffer.Handle,
$"SharpEmu global 0x{guestBuffer.BaseAddress:X16} {guestBuffer.Length}b");
}
if (guestBuffer.Pooled) if (guestBuffer.Pooled)
{ {
GuestDataPool.Return(guestBuffer.Data); GuestDataPool.Return(guestBuffer.Data);
@@ -9440,6 +9465,7 @@ internal static unsafe class VulkanVideoPresenter
{ {
var extent = new Extent2D(firstTarget.Width, firstTarget.Height); var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
var draw = work.Draw; var draw = work.Draw;
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable) if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
{ {
draw = draw with draw = draw with
@@ -9452,9 +9478,10 @@ internal static unsafe class VulkanVideoPresenter
} }
GuestDepthResource? depth = null; GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null; DepthFramebufferResource? depthFramebuffer = null;
if (work.DepthTarget is { } depthTarget && if (ShouldAttachGuestDepth(
(draw.RenderState.Depth.TestEnable || work.DepthTarget,
draw.RenderState.Depth.WriteEnable)) draw.RenderState.Depth) &&
work.DepthTarget is { } depthTarget)
{ {
var effectiveDepthTarget = depthTarget; var effectiveDepthTarget = depthTarget;
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height) if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
@@ -9501,21 +9528,45 @@ internal static unsafe class VulkanVideoPresenter
depth = GetOrCreateGuestDepth(effectiveDepthTarget); depth = GetOrCreateGuestDepth(effectiveDepthTarget);
PrepareFirstUseDepth(depth, draw.RenderState.Depth); PrepareFirstUseDepth(depth, draw.RenderState.Depth);
if (clearDepthForDraw)
{
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
}
if (targets.Length == 1) if (targets.Length == 1)
{ {
depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth); depthFramebuffer = GetOrCreateDepthFramebuffer(firstTarget, depth);
} }
} }
if (clearDepthForDraw)
{
// DB_RENDER_CONTROL.DEPTH_CLEAR_ENABLE makes this a DB
// clear operation. The draw still produces color, but its
// interpolated vertex Z is not the guest clear value.
draw = draw with
{
RenderState = draw.RenderState with
{
Depth = draw.RenderState.Depth with
{
TestEnable = false,
WriteEnable = false,
ClearEnable = false,
},
},
};
}
var renderPass = depthFramebuffer is null var renderPass = depthFramebuffer is null
? firstTarget.Initialized ? firstTarget.Initialized
? firstTarget.RenderPass ? firstTarget.RenderPass
: firstTarget.InitialRenderPass : firstTarget.InitialRenderPass
: firstTarget.Initialized : firstTarget.Initialized
? depth!.Initialized ? depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.LoadRenderPass ? depthFramebuffer.LoadRenderPass
: depthFramebuffer.DepthClearRenderPass : depthFramebuffer.DepthClearRenderPass
: depth!.Initialized : depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.ColorClearRenderPass ? depthFramebuffer.ColorClearRenderPass
: depthFramebuffer.BothClearRenderPass; : depthFramebuffer.BothClearRenderPass;
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer; var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
@@ -9531,7 +9582,7 @@ internal static unsafe class VulkanVideoPresenter
targets.Select(target => targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(), target.Initialized || target.InitialUploadPending).ToArray(),
depth, depth,
depth?.Initialized == true); depth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass; transientRenderPass = renderPass;
transientFramebuffer = framebuffer; transientFramebuffer = framebuffer;
} }
@@ -9719,7 +9770,12 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null) if (depth is not null)
{ {
depth.Initialized = true; depth.Initialized = true;
if (draw.RenderState.Depth.WriteEnable) depth.Layout = ImageLayout.DepthStencilAttachmentOptimal;
if (clearDepthForDraw)
{
depth.InitializationSource = "guest-depth-clear";
}
else if (draw.RenderState.Depth.WriteEnable)
{ {
depth.InitializationSource = "translated-depth-write"; depth.InitializationSource = "translated-depth-write";
} }
@@ -9853,11 +9909,14 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
} }
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader( TraceVulkanShader(
$"vk.offscreen_draw mrt={targets.Length} " + $"vk.offscreen_draw mrt={targets.Length} " +
$"size={firstTarget.Width}x{firstTarget.Height} " + $"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}"); $"textures={work.Draw.Textures.Count}");
} }
}
catch (Exception exception) catch (Exception exception)
{ {
if (TryMarkDeviceLost(exception)) if (TryMarkDeviceLost(exception))
@@ -0,0 +1,89 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Agc;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.VideoOut;
using Xunit;
namespace SharpEmu.Libs.Tests.VideoOut;
public sealed class VulkanDepthAttachmentTests
{
private static readonly GuestDepthTarget Target = new(
ReadAddress: 0x1000,
WriteAddress: 0x1000,
Width: 1920,
Height: 1080,
GuestFormat: 1,
SwizzleMode: 0,
ClearDepth: 1f,
ReadOnly: false);
[Fact]
public void GuestDepthTarget_AttachesForDepthWork()
{
var state = new GuestDepthState(
TestEnable: true,
WriteEnable: true,
CompareOp: 3);
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
}
[Theory]
[InlineData(true, false)]
[InlineData(false, true)]
public void GuestDepthTarget_AttachesForEitherDepthOperation(
bool testEnable,
bool writeEnable)
{
var state = new GuestDepthState(testEnable, writeEnable, CompareOp: 3);
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
}
[Fact]
public void GuestDepthTarget_RequiresTargetAndDepthWork()
{
var state = GuestDepthState.Default;
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
Assert.False(VulkanVideoPresenter.ShouldAttachGuestDepth(
target: null,
new GuestDepthState(true, false, CompareOp: 3)));
}
[Fact]
public void GuestDepthTarget_AttachesForDepthClear()
{
var state = new GuestDepthState(
TestEnable: false,
WriteEnable: false,
CompareOp: 7,
ClearEnable: true);
Assert.True(VulkanVideoPresenter.ShouldAttachGuestDepth(Target, state));
}
[Theory]
[InlineData(0x41u, true)]
[InlineData(0x40u, false)]
public void DepthState_DecodesRenderControlClearBit(
uint renderControl,
bool clearEnable)
{
var registers = new Dictionary<uint, uint>
{
[0x000] = renderControl,
[0x200] = 0x776,
};
var state = AgcExports.DecodeDepthState(registers);
Assert.True(state.TestEnable);
Assert.True(state.WriteEnable);
Assert.Equal(7u, state.CompareOp);
Assert.Equal(clearEnable, state.ClearEnable);
}
}