Windows: --game-window, on Vulkan

A second Win32 window, added as a Game output like the SDL one. Keys
typed into it still go to the emulator; the mouse doesn't. It's for
testing multiple outputs (standing in for a TV over AirPlay) until real
multi-window support takes it over.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-22 20:41:35 +02:00
1 parent 82e6ac86f0
commit 5009724d91
5 files changed
+102 -2

No files matched your search

+93
View File
@@ -61,6 +61,8 @@
#include "Windows/Debugger/Debugger_MemoryDlg.h"
#include "Common/GPU/GraphicsContext.h"
#include "Common/GPU/thin3d.h"
#include "Common/SysError.h"
#include "Windows/main.h"
#include "Windows/DinputDevice.h"
@@ -1348,4 +1350,95 @@ namespace MainWindow {
PostMessage(hwndMain, WM_USER_RUN_CALLBACK, reinterpret_cast<WPARAM>(callback), reinterpret_cast<LPARAM>(userdata));
}
// --game-window: a second window showing just the game. For now it's only a way to test multiple outputs on
// the desktop, standing in for a TV over AirPlay. Later, real multi-window support (like ImGui's multi-viewports)
// will take over these pieces: the window class, the input forwarding and the output lifetime.
static HWND hwndGame;
static int gameOutputId;
static LRESULT CALLBACK GameWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_SIZE:
if (gameOutputId != 0 && wParam != SIZE_MINIMIZED) {
Native_ResizeOutput(gameOutputId, LOWORD(lParam), HIWORD(lParam));
}
return 0;
case WM_CLOSE:
// The window has to outlive the swapchain, so it stays until we quit.
Native_RemoveOutput(gameOutputId);
ShowWindow(hWnd, SW_HIDE);
return 0;
case WM_ACTIVATE:
if (wParam == WA_ACTIVE || wParam == WA_CLICKACTIVE) {
WindowsRawInput::GainFocus();
g_InputManager.GainFocus();
} else {
WindowsRawInput::LoseFocus();
g_InputManager.LoseFocus();
}
return 0;
case WM_INPUT:
{
// Keys still go to the emulator. The mouse would get main window coordinates, so it's left out.
RAWINPUTHEADER header;
UINT size = sizeof(header);
if (GetRawInputData((HRAWINPUT)lParam, RID_HEADER, &header, &size, sizeof(RAWINPUTHEADER)) != (UINT)-1 && header.dwType == RIM_TYPEKEYBOARD) {
return WindowsRawInput::Process(hWnd, wParam, lParam);
}
return DefWindowProc(hWnd, message, wParam, lParam);
}
case WM_CHAR:
return WindowsRawInput::ProcessChar(hWnd, wParam, lParam);
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
void CreateGameWindow() {
if ((GPUBackend)g_Config.iGPUBackend != GPUBackend::VULKAN) {
ERROR_LOG(Log::System, "--game-window needs Vulkan");
return;
}
static constexpr const wchar_t *szGameWindowClass = L"PPSSPPGameWnd";
WNDCLASSEX wcex{};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = (WNDPROC)GameWndProc;
wcex.hInstance = hInst;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
wcex.lpszClassName = szGameWindowClass;
wcex.hIcon = LoadIcon(hInst, (LPCTSTR)IDI_PPSSPP);
wcex.hIconSm = (HICON)LoadImage(hInst, (LPCTSTR)IDI_PPSSPP, IMAGE_ICON, 16, 16, LR_SHARED);
RegisterClassEx(&wcex);
RECT rc{ 0, 0, 960, 544 };
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, FALSE);
// No owner: an owned window is destroyed along with the main window, which can be before the swapchain is.
hwndGame = CreateWindowEx(0, szGameWindowClass, L"PPSSPP game", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInst, NULL);
if (!hwndGame) {
ERROR_LOG(Log::System, "Error creating the game window: %s", GetLastErrorMsg().c_str());
return;
}
RECT client;
GetClientRect(hwndGame, &client);
Draw::SwapchainDesc desc{};
desc.winsys = WINDOWSYSTEM_WIN32;
desc.data1 = hInst;
desc.data2 = hwndGame;
desc.width = client.right - client.left;
desc.height = client.bottom - client.top;
desc.tag = "GameWindow";
gameOutputId = Native_AddOutput(OutputRole::Game, desc);
ShowWindow(hwndGame, SW_SHOWNOACTIVATE);
}
} // namespace MainWindow