#include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "Decoder/Png.hpp" #include "SDL_vulkan.h" #include #include #include #include #ifdef _WIN32 #include "SDL_syswm.h" #include #include #endif namespace { void require(bool condition, const char* reason) { if (!condition) throw std::runtime_error(std::string("DisplayWindow: ") + reason); } void applyAppIcon(SDL_Window* window) { if (!HasAppIcon_nid_postfix()) return; const auto data = GetAppIconData_nid_postfix(); auto image = Decoder::Png::Decode(std::span(data.bytes, static_cast(data.size))); require(image.has_value(), "failed to decode icon0.png"); SDL_Surface* icon = SDL_CreateRGBSurfaceWithFormatFrom(image->pixels.data(), static_cast(image->width), static_cast(image->height), 32, static_cast(image->width * 4), SDL_PIXELFORMAT_RGBA32); require(icon != nullptr, SDL_GetError()); SDL_SetWindowIcon(window, icon); SDL_FreeSurface(icon); } #ifdef _WIN32 constexpr UINT_PTR DisplayWindowSubclassId = 0x41505335u; #endif } DisplayWindow::~DisplayWindow() { Destroy(); } void DisplayWindow::Ensure(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { require(sourceWidth != 0 && sourceHeight != 0, "source extent must be non-zero"); if (window == nullptr) create(sourceWidth, sourceHeight); updateAspectRatio(sourceWidth, sourceHeight); } void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { SDL_Rect usable{}; require(SDL_GetDisplayUsableBounds(0, &usable) == 0, SDL_GetError()); require(DisplayWindowInitialSizePercent > 0 && DisplayWindowInitialSizePercent <= 100, "initial window size percent must be between 1 and 100"); require(usable.w > 0 && usable.h > 0, "usable display extent must be positive"); const auto boundsWidth = static_cast(static_cast(usable.w) * DisplayWindowInitialSizePercent / 100); const auto boundsHeight = static_cast(static_cast(usable.h) * DisplayWindowInitialSizePercent / 100); const auto initialSize = AgcDriver::ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, boundsWidth, boundsHeight, true); require(initialSize.width >= DisplayWindowMinimumWidth && initialSize.height >= DisplayWindowMinimumHeight, "initial window extent is smaller than the minimum"); const auto title = GetAppTitle_nid_postfix(); AgcDriverLockVulkanLoader_nid_postfix(); window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); applyAppIcon(window); installSubclass(); } void DisplayWindow::updateAspectRatio(std::uint32_t sourceWidth, std::uint32_t sourceHeight) { aspectWidth = sourceWidth; aspectHeight = sourceHeight; } void DisplayWindow::Destroy() noexcept { if (window == nullptr) return; removeSubclass(); SDL_DestroyWindow(window); window = nullptr; } SDL_Window* DisplayWindow::Handle() const { return window; } void DisplayWindow::ToggleFullscreen() { require(window != nullptr, "window must exist before toggling fullscreen"); const auto flags = (SDL_GetWindowFlags(window) & SDL_WINDOW_FULLSCREEN) != 0 ? 0u : static_cast(SDL_WINDOW_FULLSCREEN_DESKTOP); require(SDL_SetWindowFullscreen(window, flags) == 0, SDL_GetError()); } void DisplayWindow::DrawableSize(std::uint32_t& width, std::uint32_t& height) const { if (window == nullptr) { width = 0; height = 0; return; } int drawableWidth = 0; int drawableHeight = 0; SDL_Vulkan_GetDrawableSize(window, &drawableWidth, &drawableHeight); width = drawableWidth > 0 ? static_cast(drawableWidth) : 0; height = drawableHeight > 0 ? static_cast(drawableHeight) : 0; } void DisplayWindow::UpdateTitle() { require(window != nullptr, "window must exist before updating title"); static const AppTitle title = GetAppTitle_nid_postfix(); static std::uint64_t fpsStart = sceKernelGetProcessTimeCounter(); static std::uint64_t frameNum = 0; static std::uint64_t fpsFrames = 0; static double currentFps = 0.0; const auto now = sceKernelGetProcessTimeCounter(); const auto frequency = sceKernelGetProcessTimeCounterFrequency(); frameNum++; fpsFrames++; if (now - fpsStart >= frequency * 2) { currentFps = static_cast(fpsFrames) * static_cast(frequency) / static_cast(now - fpsStart); fpsStart = now; fpsFrames = 0; } char text[160]; std::snprintf(text, sizeof(text), "%s | FPS: %.2f (%llu)", title.value, currentFps, static_cast(frameNum)); SDL_SetWindowTitle(window, text); } void DisplayWindow::installSubclass() { #ifdef _WIN32 SDL_SysWMinfo info; SDL_VERSION(&info.version); require(SDL_GetWindowWMInfo(window, &info) == SDL_TRUE, SDL_GetError()); require(info.subsystem == SDL_SYSWM_WINDOWS, "unsupported window subsystem"); const auto attached = SetWindowSubclass(info.info.win.window, reinterpret_cast(&DisplayWindow::windowProc), DisplayWindowSubclassId, reinterpret_cast(this)); require(attached != FALSE, "SetWindowSubclass failed"); #endif } void DisplayWindow::removeSubclass() noexcept { #ifdef _WIN32 if (window == nullptr) return; SDL_SysWMinfo info; SDL_VERSION(&info.version); if (SDL_GetWindowWMInfo(window, &info) != SDL_TRUE) return; if (info.subsystem != SDL_SYSWM_WINDOWS) return; RemoveWindowSubclass(info.info.win.window, reinterpret_cast(&DisplayWindow::windowProc), DisplayWindowSubclassId); #endif } void DisplayWindow::applyAspectRatio(void* hwnd, std::uintptr_t edge, void* rect) const { #ifdef _WIN32 require(aspectWidth != 0 && aspectHeight != 0, "source extent must be non-zero during resize"); RECT windowBounds{}; RECT clientBounds{}; require(GetWindowRect(static_cast(hwnd), &windowBounds) != FALSE, "GetWindowRect failed"); require(GetClientRect(static_cast(hwnd), &clientBounds) != FALSE, "GetClientRect failed"); const auto frameWidth = (windowBounds.right - windowBounds.left) - (clientBounds.right - clientBounds.left); const auto frameHeight = (windowBounds.bottom - windowBounds.top) - (clientBounds.bottom - clientBounds.top); require(frameWidth >= 0 && frameHeight >= 0, "invalid window frame extent"); auto* bounds = static_cast(rect); const auto clientWidth = bounds->right - bounds->left - frameWidth; const auto clientHeight = bounds->bottom - bounds->top - frameHeight; require(clientWidth > 0 && clientHeight > 0, "resized client extent must be positive"); if (edge == WMSZ_TOP || edge == WMSZ_BOTTOM) { const auto width = AgcDriver::ComputeWidthForHeight_nid_postfix(aspectWidth, aspectHeight, static_cast(clientHeight)); bounds->right = bounds->left + static_cast(width) + frameWidth; return; } const auto height = AgcDriver::ComputeHeightForWidth_nid_postfix(aspectWidth, aspectHeight, static_cast(clientWidth)); if (edge == WMSZ_TOPLEFT || edge == WMSZ_TOPRIGHT) bounds->top = bounds->bottom - static_cast(height) - frameHeight; else bounds->bottom = bounds->top + static_cast(height) + frameHeight; #else static_cast(hwnd); static_cast(edge); static_cast(rect); #endif } std::intptr_t DisplayWindow::windowProc(void* hwnd, unsigned int message, std::uintptr_t wParam, std::intptr_t lParam, std::uintptr_t subclassId, std::uintptr_t referenceData) { #ifdef _WIN32 static_cast(subclassId); if (message == WM_SIZING) { reinterpret_cast(referenceData)->applyAspectRatio(hwnd, wParam, reinterpret_cast(lParam)); return TRUE; } return DefSubclassProc(static_cast(hwnd), message, static_cast(wParam), static_cast(lParam)); #else static_cast(hwnd); static_cast(message); static_cast(wParam); static_cast(lParam); static_cast(subclassId); static_cast(referenceData); return 0; #endif }