mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-27 13:11:04 +08:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 459ae7e3f7 | |||
| 8f405caebe | |||
| dabf723b3e | |||
| 2129a12684 |
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 |
@@ -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>
|
||||||
|
|||||||
@@ -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.
|
|  |  |
|
||||||

|
|
||||||
|
|
||||||
* **Poppy Playtime Chapter 1**
|
| Void Terrarium | Dead Cells |
|
||||||
* [Poppy Playtime Chapter 1 [PPSA20591]](https://github.com/sharpemu/sharpemu/issues/3)
|
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
|
||||||
|
|  |  |
|
||||||
* **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;
|
|
||||||

|
|
||||||
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 653 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@@ -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!",
|
||||||
|
|||||||
@@ -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!"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user