Compare commits

..

5 Commits

Author SHA1 Message Date
Berk bd65609076 Update README order 2026-07-17 02:47:38 +03:00
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
12 changed files with 364 additions and 73 deletions
+10 -11
View File
@@ -41,6 +41,16 @@ This project is developed purely for research and educational purposes. There ar
SharpEmu focuses exclusively on the PlayStation 5.
Our goal is **not** to emulate PS4 games, as there is already an excellent emulator dedicated to that platform: **ShadPS4**.
## Games Tested
| Demons Souls Remake | Dreaming Sarah |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
## Status
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
@@ -88,17 +98,6 @@ chmod +x ./SharpEmu
A Vulkan-capable GPU and current graphics driver are required. The macOS
release includes the MoltenVK Vulkan implementation.
## Games Tested
| Demons Souls Remake | Dreaming Sarah |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
> [!IMPORTANT]
> This project does **not** support or condone piracy.
> All games used during development and testing are dumped from consoles that we personally own.
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",
"About.Github.Label": "GitHub",
"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.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!",
+2
View File
@@ -131,6 +131,8 @@
"About.Github.Label": "GitHub",
"About.Github.Desc": "Código fuente, issues y desarrollo del proyecto.",
"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.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!"
+28 -4
View File
@@ -333,11 +333,35 @@ SPDX-License-Identifier: GPL-2.0-or-later
Classes="sectionTitle"
Text="ABOUT" />
<!--Latest commit info-->
<Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
<TextBlock x:Name="UpdateLabel" Text="Updates" FontSize="13" />
<TextBlock x:Name="UpdateStatusText" Text="Current build: dev" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
<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="UpdateStatusText" Text="Current build: dev" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Button Grid.Column="1" x:Name="UpdateButton" Classes="ghost"
Content="Check for updates" VerticalAlignment="Center" />
+109
View File
@@ -20,6 +20,7 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
using System.Net.Http.Headers;
namespace SharpEmu.GUI;
@@ -78,6 +79,10 @@ public partial class MainWindow : Window
private long _navUpNextAt;
private long _navDownNextAt;
//Github http client for latest commit
private static readonly HttpClient GithubHttpClient = CreateGithubHttpClient();
private string? _latestCommitSha;
public MainWindow()
{
InitializeComponent();
@@ -195,6 +200,21 @@ public partial class MainWindow : Window
UseShellExecute = true
});
};
LatestCommitHashText.Click += (_, _) =>
{
if (string.IsNullOrWhiteSpace(_latestCommitSha))
{
return;
}
Process.Start(new ProcessStartInfo
{
FileName =
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
UseShellExecute = true
});
};
}
/// <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 ----
private void PollGamepad()
@@ -381,6 +486,8 @@ public partial class MainWindow : Window
ApplySettingsToControls();
LocateEmulator();
UpdateDiscordPresence();
_ = LoadLatestCommitAsync();
if (_settings.CheckForUpdatesOnStartup)
{
_ = CheckForUpdatesAsync();
@@ -516,6 +623,8 @@ public partial class MainWindow : Window
GithubButton.Content = loc.Get("About.GithubButton");
DiscordButton.Content = loc.Get("About.DiscordButton");
UpdateLabel.Text = loc.Get("Updater.Label");
LatestCommitLabel.Text = loc.Get("About.Github.LatestCommitLabel");
LatestCommitDescription.Text = loc.Get("About.Github.LatestCommitDescription");
RefreshUpdateText();
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\github.png" Link="Assets/github.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>
+14 -10
View File
@@ -121,6 +121,7 @@ public static partial class AgcExports
private const uint CbBlend0Control = 0x1E0;
private const uint PaScModeCntl0 = 0x292;
// GFX10 DB context registers (register byte address minus 0x28000, / 4).
private const uint DbRenderControl = 0x000;
private const uint DbDepthView = 0x002;
private const uint DbDepthSizeXy = 0x007;
private const uint DbDepthClear = 0x00B;
@@ -5291,7 +5292,7 @@ public static partial class AgcExports
var hasDepthOnlyCandidate = hasExportShader &&
!hasPixelShader &&
depthTarget is not null &&
(depthState.TestEnable || depthState.WriteEnable);
(depthState.TestEnable || depthState.WriteEnable || depthState.ClearEnable);
if (hasDepthOnlyCandidate &&
TryCreateTranslatedDepthOnlyGuestDraw(
ctx,
@@ -6652,27 +6653,30 @@ public static partial class AgcExports
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
// 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 static GuestDepthState DecodeDepthState(
internal static GuestDepthState DecodeDepthState(
IReadOnlyDictionary<uint, uint> registers)
{
if (!registers.TryGetValue(DbDepthControl, out var control))
{
return GuestDepthState.Default;
}
var hasDepthControl = registers.TryGetValue(DbDepthControl, out var control);
registers.TryGetValue(DbRenderControl, out var renderControl);
var testEnable = (control & 0x2u) != 0;
var writeEnable = (control & 0x4u) != 0;
var compareOp = (control >> 4) & 0x7u;
return new GuestDepthState(testEnable, writeEnable, compareOp);
var compareOp = hasDepthControl
? (control >> 4) & 0x7u
: GuestDepthState.Default.CompareOp;
var clearEnable = (renderControl & 0x1u) != 0;
return new GuestDepthState(testEnable, writeEnable, compareOp, clearEnable);
}
private static GuestDepthTarget? DecodeDepthTarget(
IReadOnlyDictionary<uint, uint> registers)
{
var depthState = DecodeDepthState(registers);
if (!depthState.TestEnable && !depthState.WriteEnable)
if (!depthState.TestEnable &&
!depthState.WriteEnable &&
!depthState.ClearEnable)
{
return null;
}
+3 -2
View File
@@ -91,9 +91,10 @@ internal readonly record struct GuestRasterState(
internal readonly record struct GuestDepthState(
bool TestEnable,
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
@@ -2213,6 +2213,12 @@ internal static unsafe class VulkanVideoPresenter
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(
byte[]? Pixels,
uint Width,
@@ -4399,11 +4405,14 @@ internal static unsafe class VulkanVideoPresenter
CollectCompletedGuestSubmissions(waitForOldest: false);
var waitedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - waitStart) *
1000.0 / System.Diagnostics.Stopwatch.Frequency;
TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}");
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.queue_visibility queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"target_timeline={targetTimeline} completed_timeline={_completedTimeline} " +
$"waited_ms={waitedMs:F3}");
}
}
private void ExecuteOrderedGuestAction(VulkanOrderedGuestAction work)
@@ -4411,10 +4420,13 @@ internal static unsafe class VulkanVideoPresenter
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
work.Action();
TraceVulkanShader(
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.ordered_action queue={_activeGuestQueue.Name} " +
$"submission={_activeGuestQueue.SubmissionId} " +
$"work_sequence={_activeGuestWorkSequence} name='{work.DebugName}'");
}
}
private void ExecuteOrderedGuestFlip(VulkanOrderedGuestFlip work)
@@ -7523,21 +7535,43 @@ internal static unsafe class VulkanVideoPresenter
var size = (ulong)Math.Max(guestBuffer.Length, sizeof(uint));
var endAddress = checked(guestBuffer.BaseAddress + size);
var allocation = _guestBufferAllocations
.Where(candidate =>
candidate.BaseAddress <= guestBuffer.BaseAddress &&
candidate.BaseAddress + candidate.Size >= endAddress)
.OrderByDescending(candidate =>
string.Equals(
GuestBufferAllocation? allocation = null;
var allocationPriority = -1;
// This runs for every bound global buffer. Preserve the previous
// stable queue preference without allocating LINQ sort state.
foreach (var candidate in _guestBufferAllocations)
{
if (candidate.BaseAddress > guestBuffer.BaseAddress ||
candidate.BaseAddress + candidate.Size < endAddress)
{
continue;
}
var candidatePriority = string.Equals(
candidate.QueueName,
_activeGuestQueue.Name,
StringComparison.Ordinal))
.ThenByDescending(candidate =>
string.Equals(
StringComparison.Ordinal)
? 2
: string.Equals(
candidate.QueueName,
SharedReadOnlyGuestBufferQueue,
StringComparison.Ordinal))
.First();
StringComparison.Ordinal)
? 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 descriptorOffset = guestOffset &
~(GuestStorageBufferOffsetAlignment - 1);
@@ -7586,16 +7620,15 @@ internal static unsafe class VulkanVideoPresenter
WaitForActiveGuestQueueSubmissionsForCpuVisibility();
WriteBackAllDirtyGuestBuffers(_activeGuestQueue.Name);
}
var live = new byte[guestBuffer.Length];
if (_guestMemory?.TryRead(guestBuffer.BaseAddress, live) == true)
var mapped = new Span<byte>(
(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>(
(void*)(allocation.Mapped + checked((nint)guestOffset)),
source.Length));
source.CopyTo(shadow);
mapped.CopyTo(shadow);
}
if (ShouldTraceVulkanResources() &&
@@ -7605,14 +7638,6 @@ internal static unsafe class VulkanVideoPresenter
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
$"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)
{
GuestDataPool.Return(guestBuffer.Data);
@@ -9440,6 +9465,7 @@ internal static unsafe class VulkanVideoPresenter
{
var extent = new Extent2D(firstTarget.Width, firstTarget.Height);
var draw = work.Draw;
var clearDepthForDraw = draw.RenderState.Depth.ClearEnable;
if (work.DepthTarget?.ReadOnly == true && draw.RenderState.Depth.WriteEnable)
{
draw = draw with
@@ -9452,9 +9478,10 @@ internal static unsafe class VulkanVideoPresenter
}
GuestDepthResource? depth = null;
DepthFramebufferResource? depthFramebuffer = null;
if (work.DepthTarget is { } depthTarget &&
(draw.RenderState.Depth.TestEnable ||
draw.RenderState.Depth.WriteEnable))
if (ShouldAttachGuestDepth(
work.DepthTarget,
draw.RenderState.Depth) &&
work.DepthTarget is { } depthTarget)
{
var effectiveDepthTarget = depthTarget;
if (depthTarget.Width < firstTarget.Width || depthTarget.Height < firstTarget.Height)
@@ -9501,21 +9528,45 @@ internal static unsafe class VulkanVideoPresenter
depth = GetOrCreateGuestDepth(effectiveDepthTarget);
PrepareFirstUseDepth(depth, draw.RenderState.Depth);
if (clearDepthForDraw)
{
depth.GuestClearDepth = effectiveDepthTarget.ClearDepth;
depth.ClearDepth = effectiveDepthTarget.ClearDepth;
}
if (targets.Length == 1)
{
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
? firstTarget.Initialized
? firstTarget.RenderPass
: firstTarget.InitialRenderPass
: firstTarget.Initialized
? depth!.Initialized
? depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.LoadRenderPass
: depthFramebuffer.DepthClearRenderPass
: depth!.Initialized
: depth!.Initialized && !clearDepthForDraw
? depthFramebuffer.ColorClearRenderPass
: depthFramebuffer.BothClearRenderPass;
var framebuffer = depthFramebuffer?.Framebuffer ?? firstTarget.Framebuffer;
@@ -9531,7 +9582,7 @@ internal static unsafe class VulkanVideoPresenter
targets.Select(target =>
target.Initialized || target.InitialUploadPending).ToArray(),
depth,
depth?.Initialized == true);
depth?.Initialized == true && !clearDepthForDraw);
transientRenderPass = renderPass;
transientFramebuffer = framebuffer;
}
@@ -9719,7 +9770,12 @@ internal static unsafe class VulkanVideoPresenter
if (depth is not null)
{
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";
}
@@ -9853,10 +9909,13 @@ internal static unsafe class VulkanVideoPresenter
}
}
}
TraceVulkanShader(
$"vk.offscreen_draw mrt={targets.Length} " +
$"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}");
if (_traceVulkanShaderEnabled)
{
TraceVulkanShader(
$"vk.offscreen_draw mrt={targets.Length} " +
$"size={firstTarget.Width}x{firstTarget.Height} " +
$"textures={work.Draw.Textures.Count}");
}
}
catch (Exception 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);
}
}