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:
Spooks
2026-07-20 05:37:20 -06:00
committed by GitHub
parent 8cd46243ab
commit db9b20481c
6 changed files with 159 additions and 26 deletions
+5
View File
@@ -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>
+7
View File
@@ -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)
+7
View File
@@ -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;
}
+23
View File
@@ -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">
+38 -2
View File
@@ -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 =