Compare commits
9 Commits
1016_part3
...
fix/render
| Author | SHA1 | Date |
|---|---|---|
|
|
fb7383d514 | |
|
|
5a51123069 | |
|
|
5cac7c7812 | |
|
|
5c9b409e18 | |
|
|
f8c85b6168 | |
|
|
d5de44b6fb | |
|
|
44e223c5c8 | |
|
|
428c03eb32 | |
|
|
aaed66bf83 |
|
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
# Rendering resolution
|
||||
|
||||
Options → Rendering exposes two independent sizes, globally and per game:
|
||||
|
||||
- **Resolution** sets the host window size or exclusive fullscreen mode.
|
||||
- **Game display mode** reports a 1080p display to the game by default. Select
|
||||
3840×2160 to request a 4K display. A registered presentation buffer cannot
|
||||
change this setting.
|
||||
|
||||
The CLI equivalents are `--resolution=1280x720` and
|
||||
`--guest-resolution=1920x1080` (or `3840x2160`). For example, a 1080p game display
|
||||
can be presented in a 720p window; changing the window alone does not reduce the
|
||||
work the game renders.
|
||||
|
||||
The game chooses its internal render targets. Some games adapt those targets to
|
||||
the reported display mode; others render at a fixed resolution and may still
|
||||
register 4K buffers with a 1080p display. Silent Hill: The Short Message does so
|
||||
in current testing. The size of that presentation buffer alone also does not
|
||||
establish the sizes of its intermediate scene targets.
|
||||
|
||||
Arbitrary internal resolution scaling is not implemented. The previous
|
||||
`RenderResolutionScale` setting and its `SHARPEMU_RENDER_SCALE` export had no
|
||||
renderer consumer; they have been removed. They did not change rendering work.
|
||||
A correct implementation must preserve guest memory layouts while scaling host
|
||||
attachments, viewports, scissors, image fetches, compute accesses, aliases and
|
||||
readbacks consistently. Reducing only the output image or window is not an
|
||||
internal-resolution implementation.
|
||||
|
|
@ -1011,7 +1011,7 @@ internal static partial class Program
|
|||
|
||||
private static void PrintUsage()
|
||||
{
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--guest-resolution=<1920x1080|3840x2160>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--debug-server[=host:port]] <path-to-eboot.bin>");
|
||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
|
||||
}
|
||||
|
|
@ -1076,6 +1076,7 @@ internal static partial class Program
|
|||
var systemLanguage = 1;
|
||||
HostWindowMode? windowModeOverride = null;
|
||||
HostScalingMode? scalingModeOverride = null;
|
||||
GuestDisplayResolution? guestResolutionOverride = null;
|
||||
int? windowWidthOverride = null;
|
||||
int? windowHeightOverride = null;
|
||||
int? displayIndexOverride = null;
|
||||
|
|
@ -1135,6 +1136,18 @@ internal static partial class Program
|
|||
windowHeightOverride = windowHeight;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--guest-resolution", out var guestResolutionText))
|
||||
{
|
||||
if (guestResolutionText is not ("1920x1080" or "3840x2160"))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
return false;
|
||||
}
|
||||
guestResolutionOverride = guestResolutionText == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd;
|
||||
continue;
|
||||
}
|
||||
if (TrySplitOption(argument, "--display", out var displayText))
|
||||
{
|
||||
if (!int.TryParse(displayText, out var displayIndex) || displayIndex < 0)
|
||||
|
|
@ -1392,6 +1405,7 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowModeOverride ?? configuredVideoOptions.WindowMode,
|
||||
ScalingMode = scalingModeOverride ?? configuredVideoOptions.ScalingMode,
|
||||
GuestResolution = guestResolutionOverride ?? configuredVideoOptions.GuestResolution,
|
||||
Width = windowWidthOverride ?? configuredVideoOptions.Width,
|
||||
Height = windowHeightOverride ?? configuredVideoOptions.Height,
|
||||
DisplayIndex = displayIndexOverride ?? configuredVideoOptions.DisplayIndex,
|
||||
|
|
@ -1432,6 +1446,8 @@ internal static partial class Program
|
|||
{
|
||||
WindowMode = windowMode,
|
||||
ScalingMode = scalingMode,
|
||||
GuestResolution = effective.GuestResolution == "3840x2160"
|
||||
? GuestDisplayResolution.UltraHd : GuestDisplayResolution.Hd,
|
||||
Width = hasResolution ? configuredWidth : defaults.Width,
|
||||
Height = hasResolution ? configuredHeight : defaults.Height,
|
||||
DisplayIndex = effective.DisplayIndex,
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@ public sealed class GuiSettings
|
|||
|
||||
public string Resolution { get; set; } = "1920x1080";
|
||||
|
||||
public string GuestResolution { get; set; } = "1920x1080";
|
||||
|
||||
internal static string NormalizeGuestResolution(string? value) =>
|
||||
string.Equals(value, "3840x2160", StringComparison.OrdinalIgnoreCase) ? "3840x2160" : "1920x1080";
|
||||
|
||||
public int DisplayIndex { get; set; }
|
||||
|
||||
public int RefreshRate { get; set; }
|
||||
|
|
@ -89,9 +94,6 @@ public sealed class GuiSettings
|
|||
|
||||
public string BinkPlaybackMode { get; set; } = "Guest";
|
||||
|
||||
/// <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
|
||||
|
|
@ -184,6 +186,7 @@ public sealed class GuiSettings
|
|||
"Ukrainian");
|
||||
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
|
||||
settings.Resolution = NormalizeResolution(settings.Resolution);
|
||||
settings.GuestResolution = NormalizeGuestResolution(settings.GuestResolution);
|
||||
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
||||
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
|
||||
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "حصري",
|
||||
"Options.Resolution.Label": "الدقة",
|
||||
"Options.Resolution.Desc": "حجم النافذة الأولي أو دقة ملء الشاشة الحصرية.",
|
||||
"Options.GuestResolution.Label": "وضع عرض اللعبة",
|
||||
"Options.GuestResolution.Desc": "دقة العرض المُبلّغ عنها للعبة. الافتراضي 1080p. تختار الألعاب دقة التصيير الداخلية؛ وقد تتجاهل الألعاب ذات الدقة الثابتة هذا الإعداد.",
|
||||
"Options.Display.Label": "الشاشة",
|
||||
"Options.Display.Desc": "الشاشة المستخدمة للتوسيط وملء الشاشة.",
|
||||
"Options.RefreshRate.Label": "معدل التحديث",
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklüziv",
|
||||
"Options.Resolution.Label": "Təsvir ölçüsü",
|
||||
"Options.Resolution.Desc": "Pəncərənin başlanğıc ölçüsü və ya eksklüziv tam ekranın təsvir ölçüsü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran rejimi",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilən ekran təsvir ölçüsü. Standart 1080p-dir. Daxili render ölçüsünü oyun seçir; sabit ölçülü oyunlar bu ayarı nəzərə almaya bilər.",
|
||||
"Options.Display.Label": "Ekran",
|
||||
"Options.Display.Desc": "Pəncərənin mərkəzə yerləşdirilməsi və tam ekran üçün istifadə olunan monitor.",
|
||||
"Options.RefreshRate.Label": "Yenilənmə tezliyi",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de tela cheia exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de exibição do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução informada ao jogo. O padrão é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.Display.Label": "Tela",
|
||||
"Options.Display.Desc": "Monitor usado para centralização e tela cheia.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exklusiv",
|
||||
"Options.Resolution.Label": "Auflösung",
|
||||
"Options.Resolution.Desc": "Anfängliche Fenstergröße oder exklusive Vollbildauflösung.",
|
||||
"Options.GuestResolution.Label": "Anzeigemodus des Spiels",
|
||||
"Options.GuestResolution.Desc": "Dem Spiel gemeldete Bildschirmauflösung. Standard ist 1080p. Spiele wählen ihre interne Renderauflösung selbst; Spiele mit fester Auflösung können diese Einstellung ignorieren.",
|
||||
"Options.Display.Label": "Anzeige",
|
||||
"Options.Display.Desc": "Monitor für Zentrierung und Vollbilddarstellung.",
|
||||
"Options.RefreshRate.Label": "Bildwiederholrate",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiv",
|
||||
"Options.Resolution.Label": "Opløsning",
|
||||
"Options.Resolution.Desc": "Oprindelig vinduesstørrelse eller opløsning i eksklusiv fuldskærm.",
|
||||
"Options.GuestResolution.Label": "Spillets skærmtilstand",
|
||||
"Options.GuestResolution.Desc": "Skærmopløsning oplyst til spillet. Standard er 1080p. Spillet vælger den interne renderopløsning; spil med fast opløsning kan ignorere denne indstilling.",
|
||||
"Options.Display.Label": "Skærm",
|
||||
"Options.Display.Desc": "Skærm, der bruges til centrering og fuldskærm.",
|
||||
"Options.RefreshRate.Label": "Opdateringshastighed",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
"Options.WindowMode.Exclusive": "Eksklusiivne",
|
||||
"Options.Resolution.Label": "Eraldusvõime",
|
||||
"Options.Resolution.Desc": "Algne akna suurus või eksklusiivse täisekraani eraldusvõime.",
|
||||
"Options.GuestResolution.Label": "Mängu kuvarežiim",
|
||||
"Options.GuestResolution.Desc": "Mängule teatatud ekraani eraldusvõime. Vaikimisi 1080p. Mäng valib sisemise renderduse eraldusvõime; fikseeritud eraldusvõimega mängud võivad seda sätet eirata.",
|
||||
"Options.Display.Label": "Kuvar",
|
||||
"Options.Display.Desc": "Monitor, mida kasutatakse tsentreerimiseks ja täisekraanrežiimiks.",
|
||||
"Options.RefreshRate.Label": "Värskendussagedus",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusive",
|
||||
"Options.Resolution.Label": "Resolution",
|
||||
"Options.Resolution.Desc": "Initial window size or exclusive fullscreen resolution.",
|
||||
"Options.GuestResolution.Label": "Game display mode",
|
||||
"Options.GuestResolution.Desc": "Display resolution reported to the game. Defaults to 1080p. Games choose their internal rendering resolution; fixed-resolution games may ignore this setting.",
|
||||
"Options.Display.Label": "Display",
|
||||
"Options.Display.Desc": "Monitor used for centering and fullscreen.",
|
||||
"Options.RefreshRate.Label": "Refresh rate",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusiva",
|
||||
"Options.Resolution.Label": "Resolución",
|
||||
"Options.Resolution.Desc": "Tamaño inicial de la ventana o resolución de pantalla completa exclusiva.",
|
||||
"Options.GuestResolution.Label": "Modo de pantalla del juego",
|
||||
"Options.GuestResolution.Desc": "Resolución de pantalla comunicada al juego. El valor predeterminado es 1080p. El juego elige su resolución interna de renderizado; los juegos con resolución fija pueden ignorar este ajuste.",
|
||||
"Options.Display.Label": "Pantalla",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar y mostrar a pantalla completa.",
|
||||
"Options.RefreshRate.Label": "Frecuencia de actualización",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusif",
|
||||
"Options.Resolution.Label": "Résolution",
|
||||
"Options.Resolution.Desc": "Taille initiale de la fenêtre ou résolution du plein écran exclusif.",
|
||||
"Options.GuestResolution.Label": "Mode d’affichage du jeu",
|
||||
"Options.GuestResolution.Desc": "Résolution d’écran indiquée au jeu. La valeur par défaut est 1080p. Le jeu choisit sa résolution de rendu interne ; les jeux à résolution fixe peuvent ignorer ce réglage.",
|
||||
"Options.Display.Label": "Écran",
|
||||
"Options.Display.Desc": "Moniteur utilisé pour le centrage et le plein écran.",
|
||||
"Options.RefreshRate.Label": "Fréquence de rafraîchissement",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
"Options.WindowMode.Exclusive": "Kizárólagos",
|
||||
"Options.Resolution.Label": "Felbontás",
|
||||
"Options.Resolution.Desc": "Kezdeti ablakméret vagy kizárólagos teljes képernyős felbontás.",
|
||||
"Options.GuestResolution.Label": "A játék megjelenítési módja",
|
||||
"Options.GuestResolution.Desc": "A játéknak jelzett képernyőfelbontás. Alapértelmezés: 1080p. A belső renderelési felbontást a játék választja; a rögzített felbontású játékok figyelmen kívül hagyhatják ezt a beállítást.",
|
||||
"Options.Display.Label": "Kijelző",
|
||||
"Options.Display.Desc": "A középre igazításhoz és teljes képernyőhöz használt monitor.",
|
||||
"Options.RefreshRate.Label": "Frissítési gyakoriság",
|
||||
|
|
|
|||
|
|
@ -224,6 +224,8 @@
|
|||
"Options.WindowMode.Exclusive": "Esclusiva",
|
||||
"Options.Resolution.Label": "Risoluzione",
|
||||
"Options.Resolution.Desc": "Dimensione iniziale della finestra o risoluzione dello schermo intero esclusivo.",
|
||||
"Options.GuestResolution.Label": "Modalità schermo del gioco",
|
||||
"Options.GuestResolution.Desc": "Risoluzione dello schermo comunicata al gioco. Il valore predefinito è 1080p. Il gioco sceglie la risoluzione interna di rendering; i giochi con risoluzione fissa possono ignorare questa impostazione.",
|
||||
"Options.Display.Label": "Schermo",
|
||||
"Options.Display.Desc": "Monitor utilizzato per il centraggio e lo schermo intero.",
|
||||
"Options.RefreshRate.Label": "Frequenza di aggiornamento",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "排他",
|
||||
"Options.Resolution.Label": "解像度",
|
||||
"Options.Resolution.Desc": "初期ウィンドウサイズまたは排他フルスクリーンの解像度。",
|
||||
"Options.GuestResolution.Label": "ゲームの表示モード",
|
||||
"Options.GuestResolution.Desc": "ゲームに通知する画面解像度。既定値は1080pです。内部描画解像度はゲームが決定するため、解像度が固定されたゲームではこの設定が反映されない場合があります。",
|
||||
"Options.Display.Label": "ディスプレイ",
|
||||
"Options.Display.Desc": "中央配置とフルスクリーンに使用するモニター。",
|
||||
"Options.RefreshRate.Label": "リフレッシュレート",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "독점",
|
||||
"Options.Resolution.Label": "해상도",
|
||||
"Options.Resolution.Desc": "초기 창 크기 또는 독점 전체 화면 해상도.",
|
||||
"Options.GuestResolution.Label": "게임 디스플레이 모드",
|
||||
"Options.GuestResolution.Desc": "게임에 전달하는 화면 해상도입니다. 기본값은 1080p입니다. 내부 렌더링 해상도는 게임이 결정하며, 해상도가 고정된 게임은 이 설정을 무시할 수 있습니다.",
|
||||
"Options.Display.Label": "디스플레이",
|
||||
"Options.Display.Desc": "가운데 배치와 전체 화면에 사용할 모니터.",
|
||||
"Options.RefreshRate.Label": "새로 고침 빈도",
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusief",
|
||||
"Options.Resolution.Label": "Resolutie",
|
||||
"Options.Resolution.Desc": "Initiële venstergrootte of resolutie voor exclusief volledig scherm.",
|
||||
"Options.GuestResolution.Label": "Weergavemodus van het spel",
|
||||
"Options.GuestResolution.Desc": "Schermresolutie die aan het spel wordt doorgegeven. Standaard 1080p. Het spel kiest de interne renderresolutie; spellen met een vaste resolutie kunnen deze instelling negeren.",
|
||||
"Options.Display.Label": "Beeldscherm",
|
||||
"Options.Display.Desc": "Monitor die wordt gebruikt voor centrering en volledig scherm.",
|
||||
"Options.RefreshRate.Label": "Verversingssnelheid",
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@
|
|||
"Options.WindowMode.Exclusive": "Wyłączne",
|
||||
"Options.Resolution.Label": "Rozdzielczość",
|
||||
"Options.Resolution.Desc": "Początkowy rozmiar okna lub rozdzielczość pełnego ekranu wyłącznego.",
|
||||
"Options.GuestResolution.Label": "Tryb wyświetlania gry",
|
||||
"Options.GuestResolution.Desc": "Rozdzielczość ekranu zgłaszana grze. Domyślnie 1080p. Gra wybiera wewnętrzną rozdzielczość renderowania; gry ze stałą rozdzielczością mogą zignorować to ustawienie.",
|
||||
"Options.Display.Label": "Wyświetlacz",
|
||||
"Options.Display.Desc": "Monitor używany do centrowania i trybu pełnoekranowego.",
|
||||
"Options.RefreshRate.Label": "Częstotliwość odświeżania",
|
||||
|
|
|
|||
|
|
@ -220,6 +220,8 @@
|
|||
"Options.WindowMode.Exclusive": "Exclusivo",
|
||||
"Options.Resolution.Label": "Resolução",
|
||||
"Options.Resolution.Desc": "Tamanho inicial da janela ou resolução de ecrã inteiro exclusivo.",
|
||||
"Options.GuestResolution.Label": "Modo de apresentação do jogo",
|
||||
"Options.GuestResolution.Desc": "Resolução do ecrã comunicada ao jogo. A predefinição é 1080p. O jogo escolhe a resolução interna de renderização; jogos com resolução fixa podem ignorar esta opção.",
|
||||
"Options.Display.Label": "Ecrã",
|
||||
"Options.Display.Desc": "Monitor utilizado para centrar e apresentar em ecrã inteiro.",
|
||||
"Options.RefreshRate.Label": "Taxa de atualização",
|
||||
|
|
|
|||
|
|
@ -81,6 +81,8 @@
|
|||
"Options.WindowMode.Exclusive": "Эксклюзивный",
|
||||
"Options.Resolution.Label": "Разрешение",
|
||||
"Options.Resolution.Desc": "Начальный размер окна или разрешение эксклюзивного полноэкранного режима.",
|
||||
"Options.GuestResolution.Label": "Режим экрана игры",
|
||||
"Options.GuestResolution.Desc": "Разрешение экрана, сообщаемое игре. По умолчанию 1080p. Игра выбирает внутреннее разрешение рендеринга; игры с фиксированным разрешением могут игнорировать эту настройку.",
|
||||
"Options.Display.Label": "Монитор",
|
||||
"Options.Display.Desc": "Монитор, используемый для центрирования окна и полноэкранного режима.",
|
||||
"Options.RefreshRate.Label": "Частота обновления",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@
|
|||
"Options.WindowMode.Desc": "Normal pencere, kenarlıksız masaüstü veya özel tam ekran.",
|
||||
"Options.Resolution.Label": "Çözünürlük",
|
||||
"Options.Resolution.Desc": "Başlangıç pencere boyutu veya özel tam ekran çözünürlüğü.",
|
||||
"Options.GuestResolution.Label": "Oyunun ekran modu",
|
||||
"Options.GuestResolution.Desc": "Oyuna bildirilen ekran çözünürlüğü. Varsayılan 1080p’dir. Dahili işleme çözünürlüğünü oyun seçer; sabit çözünürlüklü oyunlar bu ayarı yok sayabilir.",
|
||||
"Options.Display.Label": "Ekran",
|
||||
"Options.Display.Desc": "Ortalama ve tam ekran için kullanılan monitör.",
|
||||
"Options.RefreshRate.Label": "Yenileme hızı",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ public partial class MainWindow
|
|||
GameLogLevelBox.ItemsSource = _logLevelChoices;
|
||||
GameWindowModeBox.ItemsSource = _windowModeChoices;
|
||||
GameScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GameGuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
GameHdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
GameOverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
GameOverlayCornerBox.ItemsSource = _overlayCornerChoices;
|
||||
|
|
@ -115,6 +116,7 @@ public partial class MainWindow
|
|||
GameRefreshRateBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameGuestResolutionBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayEnabledToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
|
||||
GameOverlayModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
|
||||
|
|
@ -216,6 +218,7 @@ public partial class MainWindow
|
|||
_scalingModeChoices,
|
||||
effective.ScalingMode,
|
||||
"Fit");
|
||||
GameGuestResolutionBox.SelectedItem = effective.GuestResolution;
|
||||
GameVSyncToggle.IsChecked = effective.VSync;
|
||||
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
|
||||
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
|
||||
|
|
@ -277,6 +280,7 @@ public partial class MainWindow
|
|||
LogToFile = GameLogToFileToggle.IsChecked == true,
|
||||
WindowMode = SelectedComboText(GameWindowModeBox, "Windowed"),
|
||||
Resolution = SelectedComboText(GameResolutionBox, "1920x1080"),
|
||||
GuestResolution = SelectedComboText(GameGuestResolutionBox, "1920x1080"),
|
||||
DisplayIndex = GameDisplayBox.SelectedItem is HostDisplayOption display
|
||||
? display.Index
|
||||
: 0,
|
||||
|
|
|
|||
|
|
@ -856,6 +856,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
|
||||
<local:SettingRow x:Name="GameGuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GameGuestResolutionBox"
|
||||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GameDisplayRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.Display.Label],
|
||||
|
|
@ -1490,6 +1498,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||
Classes="optionValue"
|
||||
VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="GuestResolutionRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.GuestResolution.Label], Source={x:Static local:Localization.Instance}}"
|
||||
Description="{Binding [Options.GuestResolution.Desc], Source={x:Static local:Localization.Instance}}">
|
||||
<ComboBox x:Name="GuestResolutionBox" Classes="optionValue" VerticalAlignment="Center" />
|
||||
</local:SettingRow>
|
||||
<local:SettingRow x:Name="DisplayRow"
|
||||
Classes="optionRow"
|
||||
Label="{Binding [Options.Display.Label], Source={x:Static local:Localization.Instance}}"
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ public partial class MainWindow : Window
|
|||
WindowModeBox.SelectionChanged += (_, _) => _settings.WindowMode = SelectedComboText(WindowModeBox, "Windowed");
|
||||
DisplayBox.SelectionChanged += (_, _) => OnHostDisplayChanged();
|
||||
ResolutionBox.SelectionChanged += (_, _) => OnHostResolutionChanged();
|
||||
GuestResolutionBox.SelectionChanged += (_, _) => _settings.GuestResolution = SelectedComboText(GuestResolutionBox, "1920x1080");
|
||||
RefreshRateBox.SelectionChanged += (_, _) => OnHostRefreshRateChanged();
|
||||
ScalingModeBox.SelectionChanged += (_, _) => _settings.ScalingMode = SelectedComboText(ScalingModeBox, "Fit");
|
||||
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
|
||||
|
|
@ -1249,6 +1250,7 @@ public partial class MainWindow : Window
|
|||
LogLevelBox.ItemsSource = _logLevelChoices;
|
||||
WindowModeBox.ItemsSource = _windowModeChoices;
|
||||
ScalingModeBox.ItemsSource = _scalingModeChoices;
|
||||
GuestResolutionBox.ItemsSource = new[] { "1920x1080", "3840x2160" };
|
||||
HdrModeBox.ItemsSource = _hdrModeChoices;
|
||||
BinkPlaybackBox.ItemsSource = _binkPlaybackChoices;
|
||||
OverlayModeBox.ItemsSource = _overlayModeChoices;
|
||||
|
|
@ -1324,6 +1326,7 @@ public partial class MainWindow : Window
|
|||
LoadHostDisplayOptions();
|
||||
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
|
||||
VSyncToggle.IsChecked = _settings.VSync;
|
||||
GuestResolutionBox.SelectedItem = GuiSettings.NormalizeGuestResolution(_settings.GuestResolution);
|
||||
HdrModeBox.SelectedIndex = ChoiceIndex(_settings.HdrMode, "Auto", "On", "Off");
|
||||
OverlayEnabledToggle.IsChecked = _settings.OverlayEnabled;
|
||||
OverlayModeBox.SelectedIndex = ChoiceIndex(_settings.OverlayMode, "Full", "Minimal", "TitleBar");
|
||||
|
|
@ -2560,13 +2563,6 @@ public partial class MainWindow : Window
|
|||
});
|
||||
_appliedEnvironmentVariables.Add(BinkModeEnvironmentName);
|
||||
|
||||
Environment.SetEnvironmentVariable(
|
||||
"SHARPEMU_RENDER_SCALE",
|
||||
_settings.RenderResolutionScale.ToString(
|
||||
"0.###",
|
||||
System.Globalization.CultureInfo.InvariantCulture));
|
||||
_appliedEnvironmentVariables.Add("SHARPEMU_RENDER_SCALE");
|
||||
|
||||
foreach (var entry in customEnvironment)
|
||||
{
|
||||
if (!CustomEnvironmentVariables.TryParseEntry(entry, out var name, out var value))
|
||||
|
|
@ -2784,6 +2780,7 @@ public partial class MainWindow : Window
|
|||
|
||||
arguments.Add($"--window-mode={launch.Settings.WindowMode.ToLowerInvariant()}");
|
||||
arguments.Add($"--resolution={launch.Settings.Resolution}");
|
||||
arguments.Add($"--guest-resolution={launch.Settings.GuestResolution}");
|
||||
arguments.Add($"--display={launch.Settings.DisplayIndex}");
|
||||
arguments.Add($"--refresh-rate={launch.Settings.RefreshRate}");
|
||||
arguments.Add($"--scaling={launch.Settings.ScalingMode.ToLowerInvariant()}");
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ public sealed class PerGameSettings
|
|||
|
||||
public string? Resolution { get; set; }
|
||||
|
||||
public string? GuestResolution { get; set; }
|
||||
|
||||
public int? DisplayIndex { get; set; }
|
||||
|
||||
public int? RefreshRate { get; set; }
|
||||
|
|
@ -52,6 +54,7 @@ public sealed class PerGameSettings
|
|||
LogToFile is null &&
|
||||
WindowMode is null &&
|
||||
Resolution is null &&
|
||||
GuestResolution is null &&
|
||||
DisplayIndex is null &&
|
||||
RefreshRate is null &&
|
||||
ScalingMode is null &&
|
||||
|
|
@ -145,6 +148,11 @@ public sealed class PerGameSettings
|
|||
Resolution = null;
|
||||
}
|
||||
|
||||
if (string.Equals(GuestResolution, global.GuestResolution, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
GuestResolution = null;
|
||||
}
|
||||
|
||||
if (DisplayIndex == global.DisplayIndex)
|
||||
{
|
||||
DisplayIndex = null;
|
||||
|
|
@ -272,6 +280,7 @@ public sealed record EffectiveLaunchSettings(
|
|||
bool LogToFile,
|
||||
string WindowMode,
|
||||
string Resolution,
|
||||
string GuestResolution,
|
||||
int DisplayIndex,
|
||||
int RefreshRate,
|
||||
string ScalingMode,
|
||||
|
|
@ -289,6 +298,7 @@ public sealed record EffectiveLaunchSettings(
|
|||
perGame?.LogToFile ?? global.LogToFile,
|
||||
perGame?.WindowMode ?? global.WindowMode,
|
||||
perGame?.Resolution ?? global.Resolution,
|
||||
GuiSettings.NormalizeGuestResolution(perGame?.GuestResolution ?? global.GuestResolution),
|
||||
Math.Max(0, perGame?.DisplayIndex ?? global.DisplayIndex),
|
||||
Math.Clamp(perGame?.RefreshRate ?? global.RefreshRate, 0, 1000),
|
||||
perGame?.ScalingMode ?? global.ScalingMode,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
|
|
@ -133,13 +134,15 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var indexAddress = ctx[CpuRegister.Rdx];
|
||||
var modifier = (uint)ctx[CpuRegister.Rcx];
|
||||
var modifier = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndex", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 5, out var baseCommand) ||
|
||||
!TryWriteUInt32(ctx, baseCommand, Pm4(3, ItIndexBase, 0)) ||
|
||||
!TryWriteUInt32(ctx, baseCommand + 4, (uint)indexAddress) ||
|
||||
|
|
@ -239,11 +242,13 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||
var modifier = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||
if (commandBufferAddress == 0)
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
NoteDrawModifier("sceAgcDcbDrawIndexAuto", modifier);
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
||||
|
|
@ -457,4 +462,19 @@ public static partial class AgcExports
|
|||
#pragma warning restore SHEM006
|
||||
|
||||
private static long _indirectDrawEmitRejectCount;
|
||||
|
||||
private static readonly ConcurrentDictionary<(string, ulong), byte> _reportedDrawModifiers = new();
|
||||
|
||||
// The modifier names the shader stage that receives the draw and the user registers the
|
||||
// command processor fills for indirect draws. A direct draw emits the same packet for every
|
||||
// modifier; rejecting the unfamiliar ones dropped whole passes, such as Unreal's volume
|
||||
// rasterization through the geometry stage, without a trace.
|
||||
private static void NoteDrawModifier(string export, ulong modifier)
|
||||
{
|
||||
if (modifier != 0x4000_0000 && _reportedDrawModifiers.TryAdd((export, modifier), 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] agc.draw_modifier {export} modifier=0x{modifier:X} stage={(uint)modifier >> 29} flags=0x{(uint)modifier & 7u:X}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,14 +64,24 @@ public static partial class AgcExports
|
|||
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||
var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||
var eventAddress = ctx[CpuRegister.Rdx];
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0)
|
||||
if (commandBufferAddress == 0 || eventType > 0x3F || (eventAddress != 0 && (eventType & ~1u) != 0x38))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||
// Counter selection and counter dumps carry a payload in the address fields.
|
||||
var hasAddress = (eventType & ~1u) == 0x38;
|
||||
var packetDwords = hasAddress ? 4u : 2u;
|
||||
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, packetDwords, out var commandAddress) ||
|
||||
!TryWriteUInt32(ctx, commandAddress, Pm4(packetDwords, ItEventWrite, 0)) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 4, hasAddress ? eventType | 0x100u : eventType))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
||||
if (hasAddress &&
|
||||
(!TryWriteUInt32(ctx, commandAddress + 8, (uint)eventAddress & ~7u) ||
|
||||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)(eventAddress >> 32))))
|
||||
{
|
||||
return ReturnPointer(ctx, 0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -435,15 +435,21 @@ public sealed partial class GpuCommandInterpreter
|
|||
throw _host.Fatal($"The occlusion-counter dump is invalid: index=0x{eventIndex:X8} address=0x{eventAddress:X16}.");
|
||||
}
|
||||
|
||||
// Publish visible occlusion results for each depth block.
|
||||
// Until real sample queries are implemented, conservatively report visibility.
|
||||
// A one-sample delta can disappear when the guest scales or reduces the counts.
|
||||
const ulong visibleCounterStep = 0x2FFFFFF;
|
||||
const ulong readyBit = 1UL << 63;
|
||||
var result = readyBit | SyntheticOcclusionCounter;
|
||||
for (var depthBlock = 0u; depthBlock < 16u; depthBlock++)
|
||||
{
|
||||
WriteQword(eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong)), result);
|
||||
// Query reducers can read a GPU-cached copy of this range. Use the same
|
||||
// ordered transfer path as clears instead of changing only CPU backing.
|
||||
var destination = eventAddress + ((ulong)depthBlock * 2 * sizeof(ulong));
|
||||
_host.FillBuffer(destination, sizeof(uint), (uint)result, false);
|
||||
_host.FillBuffer(destination + sizeof(uint), sizeof(uint), (uint)(result >> 32), false);
|
||||
}
|
||||
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + 1) & (readyBit - 1);
|
||||
SyntheticOcclusionCounter = (SyntheticOcclusionCounter + visibleCounterStep) & (readyBit - 1);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -360,7 +360,24 @@ public static class VertexInputResolver
|
|||
for (var bufferIndex = 0; bufferIndex < buffers.Count; bufferIndex++)
|
||||
{
|
||||
var buffer = buffers[bufferIndex];
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0);
|
||||
uint minimumFetchBytes = 0;
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
var descriptor = resource.Descriptor;
|
||||
// OOB_SELECT=2 only tests for an unbound (zero-record) descriptor.
|
||||
// A constant stream still reads the whole formatted element, even
|
||||
// when NUM_RECORDS is smaller than that element's byte width.
|
||||
if (descriptor.Stride == 0 && descriptor.OutOfBounds == 2 && descriptor.RecordCount != 0 &&
|
||||
VertexAttributeFormats.TryResolve(in descriptor, 4, out var format, out _))
|
||||
{
|
||||
minimumFetchBytes = Math.Max(minimumFetchBytes, Images.ViewFormatRules.BlockBytes(format));
|
||||
}
|
||||
}
|
||||
result[bufferIndex] = new VertexInputBuffer(buffer.Address, buffer.Stride, buffer.RecordCount, buffer.FetchIndex != 0)
|
||||
{
|
||||
MinimumFetchBytes = minimumFetchBytes,
|
||||
};
|
||||
foreach (var resourceIndex in buffer.AttributeIndices)
|
||||
{
|
||||
var resource = resources[resourceIndex];
|
||||
|
|
|
|||
|
|
@ -29,6 +29,13 @@ public static class ColorTargetResolver
|
|||
public static ColorTargetResolution? Resolve(ContextRegisters context, uint slot, uint drawLayerOffset, bool ignoreTargetMask, out uint resolvedSlot)
|
||||
{
|
||||
resolvedSlot = slot == FirstBoundSlot ? FirstBound(context) : slot;
|
||||
// Disabling CB takes precedence over stale target addresses and masks.
|
||||
// Depth-only passes may leave the preceding color targets bound.
|
||||
if (context.ColorControl.Mode == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ImageRequestBuilders.ColorTarget(in context.ColorTargets[resolvedSlot], context.RenderTargetMaskForSlot(resolvedSlot), drawLayerOffset, ignoreTargetMask);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -606,7 +606,8 @@ public sealed partial class RenderExecutor
|
|||
{
|
||||
var context = banks.Context;
|
||||
var shaderInterface = context.ShaderInterface;
|
||||
var hasColorOutput = (context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
var hasColorOutput = context.ColorControl.Mode != 0 &&
|
||||
(context.RenderTargetMask & shaderInterface.ColorShaderMask) != 0;
|
||||
return banks.Shader.Pixel.Address != 0 && (hasColorOutput || PixelShaderHasDepthOrCoverageSideEffects(shaderInterface));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,9 @@ public readonly record struct DispatchThreadLimits(uint X, uint Y, uint Z);
|
|||
// The vertex buffer words of one fetch slot as the vertex program declares them.
|
||||
public readonly record struct VertexInputBuffer(ulong Address, uint Stride, uint RecordCount, bool PerInstance = false)
|
||||
{
|
||||
public ulong Size => Stride != 0 ? (ulong)Stride * RecordCount : RecordCount;
|
||||
public uint MinimumFetchBytes { get; init; }
|
||||
|
||||
public ulong Size => Math.Max(Stride != 0 ? (ulong)Stride * RecordCount : RecordCount, MinimumFetchBytes);
|
||||
}
|
||||
|
||||
// One attribute of the vertex tables: its buffer words, the registers it fills and its buffer slot.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ public enum HostScalingMode
|
|||
Integer,
|
||||
}
|
||||
|
||||
// The display mode exposed to the guest, independent of host window and guest buffer sizes.
|
||||
public enum GuestDisplayResolution
|
||||
{
|
||||
Hd = 1,
|
||||
UltraHd = 2,
|
||||
}
|
||||
|
||||
public enum HostHdrMode
|
||||
{
|
||||
Auto,
|
||||
|
|
@ -55,6 +62,8 @@ public sealed record HostVideoOptions
|
|||
|
||||
public int Height { get; init; } = 1080;
|
||||
|
||||
public GuestDisplayResolution GuestResolution { get; init; } = GuestDisplayResolution.Hd;
|
||||
|
||||
public int DisplayIndex { get; init; }
|
||||
|
||||
public int RefreshRate { get; init; }
|
||||
|
|
@ -82,6 +91,7 @@ public sealed record HostVideoOptions
|
|||
{
|
||||
Width = Math.Clamp(Width, 640, 16384),
|
||||
Height = Math.Clamp(Height, 360, 16384),
|
||||
GuestResolution = Enum.IsDefined(GuestResolution) ? GuestResolution : GuestDisplayResolution.Hd,
|
||||
DisplayIndex = Math.Max(0, DisplayIndex),
|
||||
RefreshRate = Math.Clamp(RefreshRate, 0, 1000),
|
||||
HdrMode = Enum.IsDefined(HdrMode) ? HdrMode : HostHdrMode.Auto,
|
||||
|
|
|
|||
|
|
@ -519,18 +519,19 @@ public static partial class VideoOutExports
|
|||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
|
||||
WriteOutputStatus(status, port.OutputWidth, port.OutputHeight, port.RefreshRate,
|
||||
WriteOutputStatus(status, HostVideoHost.CurrentOptions.GuestResolution, port.RefreshRate,
|
||||
HostVideoHost.IsHdrOutputSupported);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
internal static void WriteOutputStatus(Span<byte> status, uint width, uint height, uint refreshRate,
|
||||
internal static void WriteOutputStatus(Span<byte> status, GuestDisplayResolution resolution, uint refreshRate,
|
||||
bool hdrOutputSupported)
|
||||
{
|
||||
status.Clear();
|
||||
var resolutionClass = width >= 3840 || height >= 2160 ? 2 : 1;
|
||||
// Buffer registration describes a source image, not the connected display.
|
||||
var resolutionClass = (int)resolution;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], hdrOutputSupported ? 2 : 1);
|
||||
// The status uses a refresh-rate code, not the frequency used for pacing.
|
||||
|
|
|
|||
|
|
@ -3955,7 +3955,7 @@ public static partial class Gen5SpirvTranslator
|
|||
{
|
||||
targetLane = BitwiseAnd(targetLane, UInt(31));
|
||||
}
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
var shuffled = ShuffleHalfWaveLane(value, targetLane);
|
||||
if (control.FetchInactive)
|
||||
{
|
||||
return shuffled;
|
||||
|
|
@ -3968,7 +3968,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
ShuffleHalfWaveLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
@ -3991,7 +3991,7 @@ public static partial class Gen5SpirvTranslator
|
|||
// operations are limited to a single half-wave for some encodings, so we
|
||||
// must not clamp wave64 lanes to 31; use the full lane mask instead.
|
||||
safeTarget = BitwiseAnd(safeTarget, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleLane(value, safeTarget);
|
||||
var shuffled = ShuffleHalfWaveLane(value, safeTarget);
|
||||
|
||||
var sourceAvailable = inRange;
|
||||
if (!control.FetchInactive)
|
||||
|
|
@ -4002,7 +4002,7 @@ public static partial class Gen5SpirvTranslator
|
|||
Load(_boolType, _exec),
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var shuffledActive = ShuffleLane(activeWord, safeTarget);
|
||||
var shuffledActive = ShuffleHalfWaveLane(activeWord, safeTarget);
|
||||
sourceAvailable = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
|
|
@ -5124,11 +5124,9 @@ public static partial class Gen5SpirvTranslator
|
|||
}
|
||||
|
||||
var targetLane = IAdd(rowBase, selector);
|
||||
// Mask to guest wave size — on Radeon hardware DPP is limited to a
|
||||
// single half-wave for some encodings, but we must not clamp wave64
|
||||
// lanes to 31; use the full lane mask instead.
|
||||
// Select a guest lane first; ShuffleHalfWaveLane maps it to the host subgroup.
|
||||
targetLane = BitwiseAnd(targetLane, UInt(_waveLaneCount == 64 ? 63u : 31u));
|
||||
var shuffled = ShuffleLane(value, targetLane);
|
||||
var shuffled = ShuffleHalfWaveLane(value, targetLane);
|
||||
var fetchInactive = (control.OperandSelect & 1) != 0;
|
||||
if (fetchInactive)
|
||||
{
|
||||
|
|
@ -5142,7 +5140,7 @@ public static partial class Gen5SpirvTranslator
|
|||
UInt(1),
|
||||
UInt(0));
|
||||
var sourceActive = IsNotZero(
|
||||
ShuffleLane(activeWord, targetLane));
|
||||
ShuffleHalfWaveLane(activeWord, targetLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
|
|
|
|||
|
|
@ -8303,6 +8303,15 @@ public static partial class Gen5SpirvTranslator
|
|||
? value
|
||||
: _module.AddInstruction(SpirvOp.GroupNonUniformShuffle, _uintType, UInt(3), value, lane);
|
||||
|
||||
// DPP row/quad operations and PERMLANE16 never cross a 32-lane half-wave.
|
||||
// Guest lanes 32..63 belong to the second host subgroup on 32-lane devices;
|
||||
// using those guest indices directly in OpGroupNonUniformShuffle is undefined.
|
||||
// Preserve the current physical half on devices whose subgroup is wider than 32.
|
||||
private uint ShuffleHalfWaveLane(uint value, uint guestLane) =>
|
||||
_subgroupInvocationIdInput == 0 ? value : ShuffleLane(value,
|
||||
BitwiseOr(BitwiseAnd(Load(_uintType, _subgroupInvocationIdInput), UInt(0xFFFF_FFE0)),
|
||||
BitwiseAnd(guestLane, UInt(31))));
|
||||
|
||||
private uint CurrentLaneBit()
|
||||
{
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
// Direct draws carry a modifier naming the shader stage and the user registers of the pipeline.
|
||||
// Every modifier must still produce the draw: Unreal's volume passes use a geometry-stage modifier.
|
||||
public sealed class AgcDirectDrawModifierTests
|
||||
{
|
||||
private const ulong BaseAddress = 0x2_1000_0000;
|
||||
private const ulong CommandBufferAddress = BaseAddress + 0x80;
|
||||
private const ulong CommandAddress = BaseAddress + 0x200;
|
||||
|
||||
public static TheoryData<ulong> Modifiers => new() { 0x4000_0000UL, 0x6000_0000UL, 0xA000_0000UL, 0x4008_0205UL, 0x1_6000_0000UL };
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Modifiers))]
|
||||
public void DrawIndexAutoEmitsTheDrawForEveryModifier(ulong modifier)
|
||||
{
|
||||
var memory = CreateCommandBuffer();
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
context[CpuRegister.Rsi] = 6;
|
||||
context[CpuRegister.Rdx] = modifier;
|
||||
|
||||
AgcExports.DcbDrawIndexAuto(context);
|
||||
|
||||
Assert.Equal(CommandAddress, context[CpuRegister.Rax]);
|
||||
var runner = new StreamRunner();
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 7)));
|
||||
var draw = Assert.Single(runner.Host.AutoDraws);
|
||||
Assert.Equal(6u, draw.VertexCount);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Modifiers))]
|
||||
public void DrawIndexEmitsTheDrawForEveryModifier(ulong modifier)
|
||||
{
|
||||
var memory = CreateCommandBuffer();
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
context[CpuRegister.Rdi] = CommandBufferAddress;
|
||||
context[CpuRegister.Rsi] = 36;
|
||||
context[CpuRegister.Rdx] = StreamRunner.DataAddress;
|
||||
context[CpuRegister.Rcx] = modifier;
|
||||
|
||||
AgcExports.DcbDrawIndex(context);
|
||||
|
||||
Assert.NotEqual(0UL, context[CpuRegister.Rax]);
|
||||
var runner = new StreamRunner();
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(ReadDwords(memory, CommandAddress, 11)));
|
||||
var draw = Assert.Single(runner.Host.IndexedDraws);
|
||||
Assert.Equal(36u, draw.IndexCount);
|
||||
Assert.Equal(StreamRunner.DataAddress, draw.IndexAddress);
|
||||
}
|
||||
|
||||
private static FakeCpuMemory CreateCommandBuffer()
|
||||
{
|
||||
var memory = new FakeCpuMemory(BaseAddress, 0x1000);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x10, CommandAddress);
|
||||
WriteUInt64(memory, CommandBufferAddress + 0x18, CommandAddress + 0x100);
|
||||
return memory;
|
||||
}
|
||||
|
||||
private static uint[] ReadDwords(FakeCpuMemory memory, ulong address, int count)
|
||||
{
|
||||
var bytes = new byte[count * sizeof(uint)];
|
||||
Assert.True(memory.TryRead(address, bytes));
|
||||
var dwords = new uint[count];
|
||||
for (var index = 0; index < count; index++)
|
||||
{
|
||||
dwords[index] = BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint)));
|
||||
}
|
||||
|
||||
return dwords;
|
||||
}
|
||||
|
||||
private static void WriteUInt64(FakeCpuMemory memory, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
Assert.True(memory.TryWrite(address, buffer));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Agc;
|
||||
using SharpEmu.Libs.Gpu.GpuCommands;
|
||||
using SharpEmu.Libs.Tests.Gpu.GpuCommands;
|
||||
using Xunit;
|
||||
|
||||
namespace SharpEmu.Libs.Tests.Agc;
|
||||
|
||||
public sealed class AgcEventWriteTests
|
||||
{
|
||||
[Fact]
|
||||
public void DrawCounterDumpsPublishBothHalvesOfQueryResults()
|
||||
{
|
||||
var runner = new StreamRunner();
|
||||
var begin = Build(0x39, StreamRunner.LabelAddress);
|
||||
var end = Build(0x39, StreamRunner.LabelAddress + 8);
|
||||
|
||||
Assert.Equal(0x139u, begin[1]);
|
||||
Assert.Equal(SubmissionProgress.Complete, runner.Run(begin, end));
|
||||
for (var block = 0u; block < 16; block++)
|
||||
{
|
||||
var address = StreamRunner.LabelAddress + block * 16;
|
||||
var first = runner.Host.ReadQword(address);
|
||||
var last = runner.Host.ReadQword(address + 8);
|
||||
Assert.NotEqual(0UL, first & last & (1UL << 63));
|
||||
Assert.True(last > first);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x38u, 0x1234567800UL, 4)]
|
||||
[InlineData(0x39u, 0x1234567800UL, 4)]
|
||||
[InlineData(0x3Au, 0UL, 2)]
|
||||
[InlineData(0x10u, 0UL, 2)]
|
||||
public void DrawEventsPreservePayloadAndAdvanceCursor(uint eventType, ulong payload, int words)
|
||||
{
|
||||
var packet = Build(eventType, payload);
|
||||
Assert.Equal(words, packet.Length);
|
||||
Assert.Equal(eventType | (words == 4 ? 0x100u : 0), packet[1]);
|
||||
if (words == 4)
|
||||
{
|
||||
Assert.Equal((uint)payload, packet[2]);
|
||||
Assert.Equal((uint)(payload >> 32), packet[3]);
|
||||
}
|
||||
}
|
||||
|
||||
private static uint[] Build(uint eventType, ulong payload)
|
||||
{
|
||||
const ulong memoryAddress = 0x2_1000_0000;
|
||||
const ulong commandBuffer = memoryAddress + 0x80;
|
||||
const ulong packetAddress = memoryAddress + 0x200;
|
||||
var memory = new FakeCpuMemory(memoryAddress, 0x1000);
|
||||
var context = new CpuContext(memory, Generation.Gen5);
|
||||
Span<byte> bytes = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes, packetAddress);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(bytes[8..], packetAddress + 0x100);
|
||||
Assert.True(memory.TryWrite(commandBuffer + 0x10, bytes));
|
||||
context[CpuRegister.Rdi] = commandBuffer;
|
||||
context[CpuRegister.Rsi] = eventType;
|
||||
context[CpuRegister.Rdx] = payload;
|
||||
|
||||
AgcExports.DcbEventWrite(context);
|
||||
Assert.Equal(packetAddress, context[CpuRegister.Rax]);
|
||||
Assert.True(memory.TryRead(commandBuffer + 0x10, bytes[..8]));
|
||||
var length = checked((int)(BinaryPrimitives.ReadUInt64LittleEndian(bytes) - packetAddress));
|
||||
Assert.InRange(length, 8, 16);
|
||||
Assert.True(memory.TryRead(packetAddress, bytes[..length]));
|
||||
var packet = new uint[length / 4];
|
||||
for (var i = 0; i < packet.Length; i++)
|
||||
packet[i] = BinaryPrimitives.ReadUInt32LittleEndian(bytes[(i * 4)..]);
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,33 @@ namespace SharpEmu.Libs.Tests.GUI;
|
|||
|
||||
public sealed class PerGameSettingsTests
|
||||
{
|
||||
[Fact]
|
||||
public void GuestResolutionDefaultsToHdAndCanBeOverriddenPerGame()
|
||||
{
|
||||
var global = new GuiSettings { Resolution = "3840x2160" };
|
||||
Assert.Equal("1920x1080", EffectiveLaunchSettings.Resolve(global, null).GuestResolution);
|
||||
var game = new PerGameSettings { GuestResolution = "3840x2160" };
|
||||
game.RemoveInheritedValues(global);
|
||||
Assert.False(game.IsEmpty);
|
||||
var restored = PerGameSettings.NormalizeFromJson(System.Text.Json.JsonSerializer.Serialize(game));
|
||||
Assert.Equal("3840x2160", EffectiveLaunchSettings.Resolve(global, restored).GuestResolution);
|
||||
global.GuestResolution = "3840x2160";
|
||||
game.RemoveInheritedValues(global);
|
||||
Assert.True(game.IsEmpty);
|
||||
Assert.Equal("3840x2160", EffectiveLaunchSettings.Resolve(global, game).GuestResolution);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData("invalid")]
|
||||
[InlineData("7680x4320")]
|
||||
public void InvalidGuestResolutionFallsBackToHd(string? value)
|
||||
{
|
||||
Assert.Equal("1920x1080", EffectiveLaunchSettings.Resolve(
|
||||
new GuiSettings(), new PerGameSettings { GuestResolution = value }).GuestResolution);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NewGameInheritsWritableApp0AndGlobalCrashCapture()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -325,12 +325,16 @@ public sealed class GpuCommandInterpreterLabelTests
|
|||
Assert.Contains("event type is unknown", runner.RunExpectingFatal(StreamRunner.Packet(PacketOpcode.EventWrite, 0x3F)).Message);
|
||||
|
||||
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
|
||||
// CPU-only stores leave GPU query-reduction inputs stale. Require the
|
||||
// host transfer path for both words of every counter, including ready.
|
||||
Assert.Equal(32, runner.Host.Calls.Count);
|
||||
Assert.All(runner.Host.Calls, call => Assert.StartsWith("fill ", call));
|
||||
Assert.Equal(1UL << 63, runner.Host.ReadQword(Label));
|
||||
Assert.Equal(1UL << 63, runner.Host.ReadQword(Label + 15 * 16));
|
||||
Assert.Equal(0UL, runner.Host.ReadQword(Label + 8));
|
||||
|
||||
runner.Run(StreamRunner.Packet(PacketOpcode.EventWrite, 0x39 | (1u << 8), StreamRunner.Low(Label), StreamRunner.High(Label)));
|
||||
Assert.Equal((1UL << 63) | 1, runner.Host.ReadQword(Label));
|
||||
Assert.Equal((1UL << 63) | 0x2FFFFFFUL, runner.Host.ReadQword(Label));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -354,4 +354,36 @@ public sealed class VertexInputResolverTests : IDisposable
|
|||
Assert.Contains("records=4/5", fatal.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(74u, 3, 12ul)]
|
||||
[InlineData(77u, 4, 16ul)]
|
||||
[InlineData(77u, 3, 16ul)]
|
||||
[InlineData(64u, 2, 8ul)]
|
||||
public void MergeBuffers_ConstantUncheckedStreamIncludesFullFormattedFetch(uint format, int components, ulong expectedSize)
|
||||
{
|
||||
var resource = Attribute(StreamBase, 0, 1, 0);
|
||||
resource = resource with
|
||||
{
|
||||
Descriptor = resource.Descriptor with { Word3 = IdentitySelect | (format << 12) | (2u << 28) },
|
||||
RegisterCount = components,
|
||||
};
|
||||
var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress));
|
||||
Assert.Equal(1u, buffer.RecordCount);
|
||||
Assert.Equal(0u, buffer.Stride);
|
||||
Assert.Equal(expectedSize, buffer.Size);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0u, 1u)]
|
||||
[InlineData(1u, 1u)]
|
||||
[InlineData(3u, 1u)]
|
||||
[InlineData(2u, 0u)]
|
||||
public void MergeBuffers_BoundedOrUnboundConstantStreamKeepsDeclaredFootprint(uint outOfBounds, uint records)
|
||||
{
|
||||
var resource = Attribute(StreamBase, 0, records, 0);
|
||||
resource = resource with { Descriptor = resource.Descriptor with { Word3 = resource.Descriptor.Word3 | (outOfBounds << 28) } };
|
||||
var buffer = Assert.Single(VertexInputResolver.MergeBuffers([resource], ShaderAddress));
|
||||
Assert.Equal((ulong)records, buffer.Size);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -322,6 +322,25 @@ public sealed class RenderExecutorStateTests : IDisposable
|
|||
Assert.Contains("create_graphics_pipeline colors=0 depth=True topology=TriangleList restart=False", _pipelines.Calls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public void Attachments_DisabledColorBufferIgnoresStaleTargetsAndPreservesDepthEffects(bool exportsDepth)
|
||||
{
|
||||
var banks = Banks(withDepth: true);
|
||||
banks.Context.ColorControl = ColorControlRegisters.Decode(0);
|
||||
banks.Context.ColorTargets[0] = RegisterWords.Color(ColorBase, 32, 32);
|
||||
banks.Context.ShaderInterface.DepthShaderControl = new DepthShaderControlRegisters { DepthExportEnable = exportsDepth };
|
||||
|
||||
_executor.DrawIndexed(1, banks, Indexed(3));
|
||||
|
||||
var rendering = Assert.Single(_host.BegunRenderings);
|
||||
Assert.Equal((0u, 64u, 64u), (rendering.ColorAttachmentCount, rendering.Width, rendering.Height));
|
||||
Assert.Equal(Format.D32Sfloat, rendering.DepthFormat);
|
||||
Assert.Equal(exportsDepth, Assert.Single(_pipelines.PipelineRequests).PixelActive);
|
||||
Assert.True(Assert.Single(_host.DynamicStates).DepthWriteEnabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0u, 0u, 64u)]
|
||||
[InlineData(0xFu, 1u, 32u)]
|
||||
|
|
|
|||
|
|
@ -17,12 +17,16 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO
|
|||
: IClassFixture<HeadlessVulkanFixture>
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Dpp8")]
|
||||
[InlineData("Dpp16")]
|
||||
[InlineData("Permlane16")]
|
||||
[InlineData("Permlanex16")]
|
||||
[InlineData("Reduction")]
|
||||
public void Wave64Shuffle_PreservesUpperHalf(string operation)
|
||||
[InlineData("Dpp8", 32)]
|
||||
[InlineData("Dpp8", 64)]
|
||||
[InlineData("Dpp16", 32)]
|
||||
[InlineData("Dpp16", 64)]
|
||||
[InlineData("Permlane16", 32)]
|
||||
[InlineData("Permlane16", 64)]
|
||||
[InlineData("Permlanex16", 32)]
|
||||
[InlineData("Permlanex16", 64)]
|
||||
[InlineData("Reduction", 64)]
|
||||
public void WaveShuffle_PreservesEachHalf(string operation, uint waveSize)
|
||||
{
|
||||
var vulkan = fixture.Vulkan;
|
||||
if (!GatePrerequisites.Ready(vulkan, shaderInt64: true)) return;
|
||||
|
|
@ -79,7 +83,7 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO
|
|||
var (plan, resources, layout) = Prepare(Program([.. instructions]));
|
||||
var request = new ShaderCompileRequest(plan, resources, layout)
|
||||
{
|
||||
LocalSizeX = 8, LocalSizeY = 8, ThreadCountX = 8, ThreadCountY = 8, WaveSize = 64,
|
||||
LocalSizeX = 8, LocalSizeY = waveSize / 8, ThreadCountX = 8, ThreadCountY = waveSize / 8, WaveSize = waveSize,
|
||||
};
|
||||
Assert.True(Gen5SpirvTranslator.TryCompileProgram(request, out var shader, out var error), error);
|
||||
using var harness = new ImageTestHarness(vulkan);
|
||||
|
|
@ -90,7 +94,7 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO
|
|||
harness.Run(() => runner.Dispatch(registers,
|
||||
new Dictionary<DescriptorBindingKind, GpuBuffer[]> { [DescriptorBindingKind.Buffers] = [result] }, 1));
|
||||
var actual = runner.ReadBack(result, 0, 256);
|
||||
for (uint lane = 0; lane < 64; lane++)
|
||||
for (uint lane = 0; lane < waveSize; lane++)
|
||||
{
|
||||
var expected = operation switch
|
||||
{
|
||||
|
|
@ -104,6 +108,6 @@ public sealed class WaveShuffleDeviceTests(HeadlessVulkanFixture fixture, ITestO
|
|||
Assert.Equal(expected, BinaryPrimitives.ReadUInt32LittleEndian(actual.AsSpan((int)lane * 4)));
|
||||
}
|
||||
harness.AssertNoValidationMessages();
|
||||
output.WriteLine($"Verified {operation} on {vulkan.DeviceName}.");
|
||||
output.WriteLine($"Verified wave{waveSize} {operation} on {vulkan.DeviceName}.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public sealed class HostVideoOptionsTests
|
|||
DisplayIndex = -2,
|
||||
RefreshRate = 5000,
|
||||
HdrMode = (HostHdrMode)99,
|
||||
GuestResolution = (GuestDisplayResolution)99,
|
||||
}.Normalize();
|
||||
|
||||
Assert.Equal(640, options.Width);
|
||||
|
|
@ -25,5 +26,18 @@ public sealed class HostVideoOptionsTests
|
|||
Assert.Equal(0, options.DisplayIndex);
|
||||
Assert.Equal(1000, options.RefreshRate);
|
||||
Assert.Equal(HostHdrMode.Auto, options.HdrMode);
|
||||
Assert.Equal(GuestDisplayResolution.Hd, options.GuestResolution);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData(1920, 1080, GuestDisplayResolution.UltraHd)]
|
||||
[InlineData(3840, 2160, GuestDisplayResolution.Hd)]
|
||||
public void GuestModeIsIndependentOfWindowSize(int width, int height, GuestDisplayResolution mode)
|
||||
{
|
||||
var options = new HostVideoOptions { Width = width, Height = height, GuestResolution = mode }.Normalize();
|
||||
Assert.Equal(mode, options.GuestResolution);
|
||||
Assert.Equal(GuestDisplayResolution.Hd, HostVideoOptions.Default.GuestResolution);
|
||||
Span<byte> status = stackalloc byte[0x30];
|
||||
VideoOutExports.WriteOutputStatus(status, options.GuestResolution, 60, false);
|
||||
Assert.Equal((int)mode, System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(status));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ public sealed class VideoOutOutputSupportTests
|
|||
Assert.Equal(expectedHdr, supported);
|
||||
|
||||
var bytes = Enumerable.Repeat((byte)0xA5, 0x32).ToArray();
|
||||
VideoOutExports.WriteOutputStatus(bytes.AsSpan(1, 0x30), 3840, 2160, 120, supported);
|
||||
VideoOutExports.WriteOutputStatus(bytes.AsSpan(1, 0x30), GuestDisplayResolution.UltraHd, 120, supported);
|
||||
|
||||
Assert.Equal(0xA5, bytes[0]);
|
||||
Assert.Equal(0xA5, bytes[^1]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue