mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Add internal render resolution scale and fix DPI Issue (#468)
* Add internal render resolution scale and fix embedded surface DPI scaling Adds a GUI-configurable internal resolution scale (Graphics tab) that renders offscreen color/depth targets below native guest resolution and upscales on present, trading image quality for GPU headroom. Storage/UAV images and sampled asset textures are left untouched, and texture-alias/feedback-loop lookups compare against each target's logical (unscaled) size so scaled render targets are still found correctly when sampled back. Also fixes the embedded game surface not filling the window: the isolated emulator child process had no declared DPI awareness, so Windows silently downscaled every window-geometry query it made against the GUI-owned surface HWND by the display's DPI factor, leaving an unfilled black margin on scaled displays. * Remove flaky Gen5ScalarMemoryFallbackTests * Restore Gen5ScalarMemoryFallbackTests --------- Co-authored-by: Spooks4576 <Spooks4576@users.noreply.github.com>
This commit is contained in:
@@ -13,4 +13,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
|
||||
@@ -351,6 +351,13 @@ public sealed class GameSurfaceHost : NativeControlHost
|
||||
var width = Math.Max(1, (int)Math.Round(Bounds.Width * renderScale));
|
||||
var height = Math.Max(1, (int)Math.Round(Bounds.Height * renderScale));
|
||||
var sizeChanged = _surface.PixelWidth != width || _surface.PixelHeight != height;
|
||||
if (Environment.GetEnvironmentVariable("SHARPEMU_TRACE_SURFACE_SIZE") == "1")
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[GUI][TRACE] GameSurfaceHost.UpdateSurfaceSize bounds={Bounds.Width}x{Bounds.Height} " +
|
||||
$"scale={renderScale} computed={width}x{height} changed={sizeChanged} " +
|
||||
$"prevSurface={_surface.PixelWidth}x{_surface.PixelHeight}");
|
||||
}
|
||||
_surface.UpdatePixelSize(width, height);
|
||||
|
||||
if (!sizeChanged)
|
||||
|
||||
@@ -53,6 +53,9 @@ public sealed class GuiSettings
|
||||
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
|
||||
public List<string> EnvironmentToggles { get; set; } = new();
|
||||
|
||||
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
|
||||
public double RenderResolutionScale { get; set; } = 1.0;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
@@ -96,6 +99,10 @@ public sealed class GuiSettings
|
||||
settings.LogLevel ??= "Info";
|
||||
settings.Language ??= "en";
|
||||
settings.DiscordClientId ??= "1525606762248540221";
|
||||
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
|
||||
{
|
||||
settings.RenderResolutionScale = 1.0;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
@@ -400,6 +400,29 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="GraphicsTabItem" Header="Graphics" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="RenderingSectionTitle" Classes="sectionTitle" Text="RENDERING" />
|
||||
|
||||
<local:SettingRow x:Name="RenderResolutionRow" Label="Internal resolution"
|
||||
Description="Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.">
|
||||
<ComboBox x:Name="RenderResolutionBox" Width="160" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="RenderResolution100Item" Content="100% (native)" Tag="1.0" />
|
||||
<ComboBoxItem x:Name="RenderResolution75Item" Content="75%" Tag="0.75" />
|
||||
<ComboBoxItem x:Name="RenderResolution50Item" Content="50%" Tag="0.5" />
|
||||
<ComboBoxItem x:Name="RenderResolution25Item" Content="25%" Tag="0.25" />
|
||||
</ComboBox>
|
||||
</local:SettingRow>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
<TabItem x:Name="EnvTabItem" Header="Environment" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
@@ -192,6 +192,18 @@ public partial class MainWindow : Window
|
||||
// it is open already uses the new values.
|
||||
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
|
||||
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
RenderResolutionBox.SelectionChanged += (_, _) =>
|
||||
{
|
||||
if (RenderResolutionBox.SelectedItem is ComboBoxItem { Tag: string tag } &&
|
||||
double.TryParse(
|
||||
tag,
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var scale))
|
||||
{
|
||||
_settings.RenderResolutionScale = scale;
|
||||
}
|
||||
};
|
||||
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
|
||||
@@ -869,6 +881,13 @@ public partial class MainWindow : Window
|
||||
_ => 2,
|
||||
};
|
||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
|
||||
{
|
||||
>= 0.875 => 0,
|
||||
>= 0.625 => 1,
|
||||
>= 0.375 => 2,
|
||||
_ => 3,
|
||||
};
|
||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||
@@ -1788,6 +1807,12 @@ public partial class MainWindow : Window
|
||||
_appliedEnvironmentVariables.Add(name);
|
||||
}
|
||||
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_RENDER_SCALE",
|
||||
_settings.RenderResolutionScale.ToString(
|
||||
"0.###",
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
|
||||
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
|
||||
{
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
@@ -2030,8 +2055,6 @@ public partial class MainWindow : Window
|
||||
RestoreGameViewToFull();
|
||||
GameView.Background = Brushes.Black;
|
||||
GameView.IsHitTestVisible = true;
|
||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||
LibraryPage.IsVisible = false;
|
||||
OptionsPage.IsVisible = false;
|
||||
LibraryToolbar.IsVisible = false;
|
||||
@@ -2040,6 +2063,19 @@ public partial class MainWindow : Window
|
||||
LaunchBar.IsVisible = false;
|
||||
HideSessionLoading();
|
||||
UpdateSessionBarVisibility();
|
||||
|
||||
// Defer so the layout pass from the margin change above settles first.
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
if (!_isRunning || _isStopping)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_gameSurfaceHost?.RefreshSurfaceSize();
|
||||
_gameSurfaceHost?.SetPresentationVisible(true);
|
||||
_gameSurfaceHost?.SetCursorAutoHide(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3088,6 +3088,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public ulong WriteAddress;
|
||||
public uint Width;
|
||||
public uint Height;
|
||||
// Unscaled guest-requested size; Width/Height are the physical (scaled) backing size.
|
||||
public uint LogicalWidth;
|
||||
public uint LogicalHeight;
|
||||
public uint GuestFormat;
|
||||
public uint SwizzleMode;
|
||||
public Image Image;
|
||||
@@ -3117,6 +3120,9 @@ internal static unsafe class VulkanVideoPresenter
|
||||
public long FlipVersion;
|
||||
public uint Width;
|
||||
public uint Height;
|
||||
// Unscaled guest-requested size; Width/Height are the physical (scaled) backing size.
|
||||
public uint LogicalWidth;
|
||||
public uint LogicalHeight;
|
||||
public uint MipLevels;
|
||||
public uint GuestFormat;
|
||||
public Format Format;
|
||||
@@ -7116,8 +7122,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
// but it must not make a differently sized alias outrank the image
|
||||
// that the texture descriptor actually names.
|
||||
var score = 0;
|
||||
if (candidate.Width == texture.Width &&
|
||||
candidate.Height == texture.Height)
|
||||
if (candidate.LogicalWidth == texture.Width &&
|
||||
candidate.LogicalHeight == texture.Height)
|
||||
{
|
||||
score += 32;
|
||||
}
|
||||
@@ -7202,7 +7208,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
continue;
|
||||
}
|
||||
|
||||
if (texture.Width > depth.Width || texture.Height > depth.Height)
|
||||
if (texture.Width > depth.LogicalWidth || texture.Height > depth.LogicalHeight)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -7412,8 +7418,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
GuestDrawTexture texture,
|
||||
GuestImageResource guestImage)
|
||||
{
|
||||
if (guestImage.Width == texture.Width &&
|
||||
guestImage.Height == texture.Height)
|
||||
if (guestImage.LogicalWidth == texture.Width &&
|
||||
guestImage.LogicalHeight == texture.Height)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -7425,8 +7431,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return false;
|
||||
}
|
||||
|
||||
return texture.Width <= guestImage.Width &&
|
||||
texture.Height <= guestImage.Height;
|
||||
return texture.Width <= guestImage.LogicalWidth &&
|
||||
texture.Height <= guestImage.LogicalHeight;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
@@ -9161,11 +9167,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private static GuestRect ClampScissor(GuestRect? scissor, Extent2D extent)
|
||||
{
|
||||
if (scissor is not { } rect)
|
||||
if (scissor is not { } guestRect)
|
||||
{
|
||||
return new GuestRect(0, 0, extent.Width, extent.Height);
|
||||
}
|
||||
|
||||
var rect = _renderResolutionScale == 1.0
|
||||
? guestRect
|
||||
: new GuestRect(
|
||||
(int)Math.Round(guestRect.X * _renderResolutionScale),
|
||||
(int)Math.Round(guestRect.Y * _renderResolutionScale),
|
||||
Math.Max(1u, (uint)Math.Round(guestRect.Width * _renderResolutionScale)),
|
||||
Math.Max(1u, (uint)Math.Round(guestRect.Height * _renderResolutionScale)));
|
||||
|
||||
var left = Math.Clamp(rect.X, 0, checked((int)extent.Width));
|
||||
var top = Math.Clamp(rect.Y, 0, checked((int)extent.Height));
|
||||
var right = Math.Clamp(
|
||||
@@ -9193,11 +9207,22 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
private static Viewport ClampViewport(GuestViewport? viewport, Extent2D extent)
|
||||
{
|
||||
if (viewport is not { } rect)
|
||||
if (viewport is not { } guestRect)
|
||||
{
|
||||
return new Viewport(0, 0, extent.Width, extent.Height, 0, 1);
|
||||
}
|
||||
|
||||
var scale = (float)_renderResolutionScale;
|
||||
var rect = scale == 1f
|
||||
? guestRect
|
||||
: guestRect with
|
||||
{
|
||||
X = guestRect.X * scale,
|
||||
Y = guestRect.Y * scale,
|
||||
Width = guestRect.Width * scale,
|
||||
Height = guestRect.Height * scale,
|
||||
};
|
||||
|
||||
// Do NOT trim the rectangle to the render target: Vulkan allows
|
||||
// viewports that extend beyond the framebuffer (rendering is
|
||||
// confined by the scissor), and trimming changes the guest's
|
||||
@@ -10513,10 +10538,11 @@ internal static unsafe class VulkanVideoPresenter
|
||||
draw.RenderState.Depth) &&
|
||||
work.DepthTarget is { } depthTarget)
|
||||
{
|
||||
// Logical dims: GetOrCreateGuestDepth below scales itself.
|
||||
var resolution = GuestDepthExtentResolver.Resolve(
|
||||
depthTarget,
|
||||
firstTarget.Width,
|
||||
firstTarget.Height,
|
||||
firstTarget.LogicalWidth,
|
||||
firstTarget.LogicalHeight,
|
||||
draw.Textures);
|
||||
var effectiveDepthTarget = resolution.IsUsable &&
|
||||
(resolution.Width != depthTarget.Width ||
|
||||
@@ -11216,7 +11242,14 @@ internal static unsafe class VulkanVideoPresenter
|
||||
$"address 0x{target.Address:X16}.");
|
||||
}
|
||||
|
||||
var mipLevels = ClampMipLevels(target.Width, target.Height, target.MipLevels);
|
||||
// Storage/UAV images keep native guest dimensions (compute shaders index them directly).
|
||||
var physicalWidth = requiresStorage
|
||||
? target.Width
|
||||
: ScaleGuestDimension(target.Width);
|
||||
var physicalHeight = requiresStorage
|
||||
? target.Height
|
||||
: ScaleGuestDimension(target.Height);
|
||||
var mipLevels = ClampMipLevels(physicalWidth, physicalHeight, target.MipLevels);
|
||||
var guestFormat = GetGuestTextureFormat(target.Format, target.NumberType);
|
||||
var requestedKey = new GuestImageVariantKey(
|
||||
target.Address,
|
||||
@@ -11227,8 +11260,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
format);
|
||||
if (_guestImages.TryGetValue(target.Address, out var existing))
|
||||
{
|
||||
if (existing.Width == target.Width &&
|
||||
existing.Height == target.Height &&
|
||||
if (existing.LogicalWidth == target.Width &&
|
||||
existing.LogicalHeight == target.Height &&
|
||||
existing.MipLevels == mipLevels &&
|
||||
existing.GuestFormat == guestFormat &&
|
||||
existing.Format == format)
|
||||
@@ -11278,8 +11311,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
_guestImageVariants.Add(
|
||||
new GuestImageVariantKey(
|
||||
existing.Address,
|
||||
existing.Width,
|
||||
existing.Height,
|
||||
existing.LogicalWidth,
|
||||
existing.LogicalHeight,
|
||||
existing.MipLevels,
|
||||
existing.GuestFormat,
|
||||
existing.Format),
|
||||
@@ -11348,7 +11381,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
ImageCreateFlags.CreateExtendedUsageBit,
|
||||
ImageType = ImageType.Type2D,
|
||||
Format = format,
|
||||
Extent = new Extent3D(target.Width, target.Height, 1),
|
||||
Extent = new Extent3D(physicalWidth, physicalHeight, 1),
|
||||
MipLevels = mipLevels,
|
||||
ArrayLayers = 1,
|
||||
Samples = SampleCountFlags.Count1Bit,
|
||||
@@ -11421,15 +11454,17 @@ internal static unsafe class VulkanVideoPresenter
|
||||
CreateRenderPassAndFramebuffer(
|
||||
format,
|
||||
mipViews[0],
|
||||
target.Width,
|
||||
target.Height);
|
||||
physicalWidth,
|
||||
physicalHeight);
|
||||
}
|
||||
|
||||
var resource = new GuestImageResource
|
||||
{
|
||||
Address = target.Address,
|
||||
Width = target.Width,
|
||||
Height = target.Height,
|
||||
Width = physicalWidth,
|
||||
Height = physicalHeight,
|
||||
LogicalWidth = target.Width,
|
||||
LogicalHeight = target.Height,
|
||||
MipLevels = mipLevels,
|
||||
GuestFormat = guestFormat,
|
||||
Format = format,
|
||||
@@ -11685,15 +11720,19 @@ internal static unsafe class VulkanVideoPresenter
|
||||
return existing;
|
||||
}
|
||||
|
||||
var (image, memory, view) = CreateDepthAttachment(target.Width, target.Height);
|
||||
var physicalWidth = ScaleGuestDimension(target.Width);
|
||||
var physicalHeight = ScaleGuestDimension(target.Height);
|
||||
var (image, memory, view) = CreateDepthAttachment(physicalWidth, physicalHeight);
|
||||
var resource = new GuestDepthResource
|
||||
{
|
||||
Key = key,
|
||||
Address = target.Address,
|
||||
ReadAddress = target.ReadAddress,
|
||||
WriteAddress = target.WriteAddress,
|
||||
Width = target.Width,
|
||||
Height = target.Height,
|
||||
Width = physicalWidth,
|
||||
Height = physicalHeight,
|
||||
LogicalWidth = target.Width,
|
||||
LogicalHeight = target.Height,
|
||||
GuestFormat = target.GuestFormat,
|
||||
SwizzleMode = target.SwizzleMode,
|
||||
Image = image,
|
||||
@@ -14153,6 +14192,22 @@ internal static unsafe class VulkanVideoPresenter
|
||||
!string.IsNullOrWhiteSpace(
|
||||
Environment.GetEnvironmentVariable(
|
||||
"SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||
private static readonly double _renderResolutionScale =
|
||||
double.TryParse(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_RENDER_SCALE"),
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var renderResolutionScale) &&
|
||||
renderResolutionScale > 0 &&
|
||||
renderResolutionScale <= 2.0
|
||||
? renderResolutionScale
|
||||
: 1.0;
|
||||
|
||||
private static uint ScaleGuestDimension(uint value) =>
|
||||
_renderResolutionScale == 1.0
|
||||
? value
|
||||
: Math.Max(1u, (uint)Math.Round(value * _renderResolutionScale));
|
||||
|
||||
private static readonly bool _forceFullscreenPipeline =
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_FORCE_FULLSCREEN_PIPELINE") == "1";
|
||||
private static readonly bool _forceFullscreenVertex =
|
||||
|
||||
Reference in New Issue
Block a user