From f63d3befdc20a566585a252b694c5426f692ddc4 Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 10 Oct 2026 18:58:43 +0200 Subject: [PATCH] Add startup splash screen --- src/common/emulatorConfig.h | 1 + src/emulator.cpp | 4 + src/graphics/presentation/ddsImage.cpp | 137 ++++++++++++++++++++ src/graphics/presentation/ddsImage.h | 32 +++++ src/graphics/presentation/systemOverlay.cpp | 120 +++++++++++++++-- src/graphics/presentation/systemOverlay.h | 3 + src/libs/libSystemService.cpp | 3 +- src/main.cpp | 6 + 8 files changed, 295 insertions(+), 11 deletions(-) create mode 100644 src/graphics/presentation/ddsImage.cpp create mode 100644 src/graphics/presentation/ddsImage.h diff --git a/src/common/emulatorConfig.h b/src/common/emulatorConfig.h index c2bbddddd..7deb31959 100644 --- a/src/common/emulatorConfig.h +++ b/src/common/emulatorConfig.h @@ -54,6 +54,7 @@ struct ConfigOptions { int32_t gpu_index = -1; bool fullscreen_enabled = false; bool hide_cursor_enabled = false; + bool splash_enabled = true; bool vr_enabled = false; bool amd_cpu_enabled = false; uint32_t vblank_frequency = 60; diff --git a/src/emulator.cpp b/src/emulator.cpp index da6ccef12..f522742ba 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -11,6 +11,7 @@ #include "common/subsystems.h" #include "common/systemInfo.h" #include "common/threads.h" +#include "graphics/presentation/systemOverlay.h" #include "graphics/presentation/window.h" #include "kernel/fileSystem.h" #include "kernel/memory.h" @@ -138,6 +139,9 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path& Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size); } } + if (cfg.splash_enabled) { + Libs::Graphics::InitializeSplashScreen(param_json.parent_path()); + } // Initialization order is explicit; destruction is automatic and reversed. subsystems.Initialize(); diff --git a/src/graphics/presentation/ddsImage.cpp b/src/graphics/presentation/ddsImage.cpp new file mode 100644 index 000000000..e20f274c7 --- /dev/null +++ b/src/graphics/presentation/ddsImage.cpp @@ -0,0 +1,137 @@ +#include "graphics/presentation/ddsImage.h" + +#include "common/assert.h" +#include "common/file.h" +#include "common/logging/log.h" +#include "imgui_impl_vulkan.h" + +#include + +namespace Libs::Graphics { + +DdsImage::~DdsImage() { + if (m_descriptor != VK_NULL_HANDLE) { + ImGui_ImplVulkan_RemoveTexture(m_descriptor); + } + m_graphics.device.destroyImageView(m_view, nullptr); + if (m_image.image != nullptr) { + m_graphics.DeleteImage(m_image); + } + if (m_staging != VK_NULL_HANDLE) { + vmaDestroyBuffer(m_graphics.allocator, m_staging, m_staging_allocation); + } +} + +bool DdsImage::Load(const std::filesystem::path& path) { + EXIT_IF(m_image.image != nullptr || m_staging != VK_NULL_HANDLE); + Common::File file(path, Common::File::Mode::Read); + if (file.IsInvalid()) { + return false; + } + + std::array header {}; + uint32_t read = 0; + file.Read(header.data(), sizeof(header), &read); + const auto width = header[4]; + const auto height = header[3]; + if (read != sizeof(header) || header[0] != 0x20534444 || header[1] != 124 || header[19] != 32 || + header[20] != 4 || header[21] != 0x30315844 || header[32] != 98 || header[33] != 3 || + header[34] != 0 || header[35] != 1 || header[6] != 0 || header[7] != 1 || header[28] != 0 || + width == 0 || height == 0 || width > 3840 || height > 2160) { + return false; + } + const uint32_t size = ((width + 3) / 4) * ((height + 3) / 4) * 16; + if (file.Size() != sizeof(header) + size) { + return false; + } + + constexpr auto format = vk::Format::eBc7UnormBlock; + constexpr auto features = vk::FormatFeatureFlagBits::eSampledImage | + vk::FormatFeatureFlagBits::eSampledImageFilterLinear | + vk::FormatFeatureFlagBits::eTransferDst; + if ((m_graphics.GetFormatProperties(format).optimalTilingFeatures & features) != features) { + LOGF("[Splash] host does not support BC7 textures\n"); + return false; + } + + vk::BufferCreateInfo buffer {}; + buffer.size = size; + buffer.usage = vk::BufferUsageFlagBits::eTransferSrc; + VmaAllocationCreateInfo allocation {}; + allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST; + allocation.flags = + VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT; + VmaAllocationInfo mapped {}; + if (vmaCreateBuffer(m_graphics.allocator, static_cast(buffer), + &allocation, &m_staging, &m_staging_allocation, &mapped) != VK_SUCCESS) { + LOGF("[Splash] could not allocate DDS upload buffer\n"); + return false; + } + file.Read(mapped.pMappedData, size, &read); + if (read != size) { + return false; + } + RequireVulkanSuccess(static_cast(vmaFlushAllocation(m_graphics.allocator, + m_staging_allocation, 0, size)), + "flush splash image upload"); + + vk::ImageCreateInfo image {}; + image.imageType = vk::ImageType::e2D; + image.extent = {width, height, 1}; + image.mipLevels = 1; + image.arrayLayers = 1; + image.format = format; + image.tiling = vk::ImageTiling::eOptimal; + image.usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled; + image.samples = vk::SampleCountFlagBits::e1; + if (!m_graphics.CreateImage(image, m_image)) { + LOGF("[Splash] could not allocate DDS image\n"); + return false; + } + vk::ImageViewCreateInfo view {}; + view.image = m_image.image; + view.viewType = vk::ImageViewType::e2D; + view.format = format; + view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; + RequireVulkanSuccess(m_graphics.device.createImageView(&view, nullptr, &m_view), + "create splash image view"); + m_descriptor = ImGui_ImplVulkan_AddTexture(static_cast(m_view), + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + return true; +} + +void DdsImage::Upload(vk::CommandBuffer command) { + if (m_image.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal) { + return; + } + vk::ImageMemoryBarrier barrier {}; + barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.oldLayout = vk::ImageLayout::eUndefined; + barrier.newLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = m_image.image; + barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; + command.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe, + vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0, nullptr, 1, + &barrier); + vk::BufferImageCopy copy {}; + copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1}; + copy.imageExtent = m_image.extent; + command.copyBufferToImage(m_staging, m_image.image, vk::ImageLayout::eTransferDstOptimal, 1, + ©); + barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead; + barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal; + barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal; + command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + vk::PipelineStageFlagBits::eFragmentShader, {}, 0, nullptr, 0, nullptr, + 1, &barrier); + m_image.state.layout = vk::ImageLayout::eShaderReadOnlyOptimal; +} + +ImTextureRef DdsImage::Texture() const { + return ImTextureRef {static_cast(reinterpret_cast(m_descriptor))}; +} + +} // namespace Libs::Graphics diff --git a/src/graphics/presentation/ddsImage.h b/src/graphics/presentation/ddsImage.h new file mode 100644 index 000000000..6cb6c90ce --- /dev/null +++ b/src/graphics/presentation/ddsImage.h @@ -0,0 +1,32 @@ +#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_ +#define EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_ + +#include "graphics/host_gpu/graphicContext.h" +#include "imgui.h" + +#include + +namespace Libs::Graphics { + +class DdsImage final { +public: + explicit DdsImage(GraphicContext& graphics): m_graphics(graphics) {} + ~DdsImage(); + KYTY_CLASS_NO_COPY(DdsImage); + + bool Load(const std::filesystem::path& path); + void Upload(vk::CommandBuffer command); + [[nodiscard]] ImTextureRef Texture() const; + +private: + GraphicContext& m_graphics; + VulkanImage m_image; + vk::ImageView m_view = nullptr; + VkDescriptorSet m_descriptor = VK_NULL_HANDLE; + VkBuffer m_staging = VK_NULL_HANDLE; + VmaAllocation m_staging_allocation = nullptr; +}; + +} // namespace Libs::Graphics + +#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_ diff --git a/src/graphics/presentation/systemOverlay.cpp b/src/graphics/presentation/systemOverlay.cpp index ce6f0a002..1d80af488 100644 --- a/src/graphics/presentation/systemOverlay.cpp +++ b/src/graphics/presentation/systemOverlay.cpp @@ -3,9 +3,12 @@ #include #include "common/assert.h" +#include "common/emulatorConfig.h" +#include "common/file.h" #include "common/logging/log.h" #include "common/stringUtils.h" #include "graphics/host_gpu/graphicContext.h" +#include "graphics/presentation/ddsImage.h" #include "imgui.h" #include "imgui_internal.h" #include "imgui_impl_vulkan.h" @@ -13,6 +16,7 @@ #include "libs/dialog.h" #include "libs/ime.h" #include "libs/imeDialog.h" +#include "loader/systemContent.h" #include "stb_image.h" #include @@ -23,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +42,8 @@ namespace Libs::Graphics { namespace { +using Clock = std::chrono::steady_clock; + namespace CoreIme = Libs::Ime; namespace DialogIme = Libs::Dialog::ImeDialog; namespace SystemDialog = Libs::Dialog::SystemDialog; @@ -194,6 +201,10 @@ struct TrophyNotification { std::mutex g_trophy_notification_mutex; std::deque g_trophy_notifications; std::atomic g_trophy_notification_active {false}; +std::array g_splash_paths; +std::atomic g_splash_active {false}; +std::atomic g_splash_hidden_at {0}; +bool g_splash_fast = false; std::vector WrapTrophyTitle(std::string_view text, float font_size, float width) { std::vector lines; @@ -464,6 +475,29 @@ void CheckVulkanResult(VkResult result) { } // namespace +void InitializeSplashScreen(const std::filesystem::path& sce_sys) { + const auto language = Config::GetConsoleLanguage(); + for (size_t i = 0; i < g_splash_paths.size(); ++i) { + auto& path = g_splash_paths[i]; + path = sce_sys / fmt::format("pic{}_{:02}.dds", i + 1, language); + if (!Common::File::IsFileExisting(path)) { + path = sce_sys / fmt::format("pic{}.dds", i + 1); + } + } + int32_t attribute = 0; + Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute); + g_splash_fast = (attribute & 0x1000) != 0; + g_splash_hidden_at.store(0, std::memory_order_release); + g_splash_active.store(Common::File::IsFileExisting(g_splash_paths[0]) && + Common::File::IsFileExisting(g_splash_paths[1]), + std::memory_order_release); +} + +void HideSplashScreen() { + Clock::duration::rep expected = 0; + g_splash_hidden_at.compare_exchange_strong(expected, Clock::now().time_since_epoch().count()); +} + void InitializeSystemOverlayInput(SDL_Window* window) { EXIT_IF(window == nullptr); const Uint32 type = SDL_RegisterEvents(1); @@ -481,6 +515,7 @@ void InitializeSystemOverlayInput(SDL_Window* window) { } void ShutdownSystemOverlayInput() { + g_splash_active.store(false, std::memory_order_release); CoreIme::SetVisibilityCallback(nullptr); DialogIme::SetVisibilityCallback(nullptr); SystemDialog::SetVisibilityCallback(nullptr); @@ -515,8 +550,10 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept { const auto dialog = DialogIme::GetVisualState(); const auto system = SystemDialog::GetVisualState(); const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire); - return {core.active || dialog.active || system.active || trophy_active, - core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)}; + const bool splash = g_splash_active.load(std::memory_order_acquire); + return {core.active || dialog.active || system.active || trophy_active || splash, + core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) + + (splash ? 2u : 0u)}; } bool ProcessSystemOverlayInput(const SDL_Event& event) { @@ -656,8 +693,6 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade, } struct SystemOverlay::Impl { - using Clock = std::chrono::steady_clock; - explicit Impl(GraphicContext& context): graphics(context) {} ~Impl() { @@ -770,6 +805,65 @@ struct SystemOverlay::Impl { return texture; } + void FinishSplash() { + g_splash_active.store(false, std::memory_order_release); + if (splash_dds[0] || splash_dds[1]) { + Common::LockGuard lock(graphics.queue_mutex); + RequireVulkanSuccess(graphics.queue.waitIdle(), "retire splash images"); + splash_dds = {}; + } + } + + void DrawSplash(vk::Extent2D frame_extent) { + if (!g_splash_active.load(std::memory_order_acquire)) { + return; + } + for (size_t i = 0; i < g_splash_paths.size(); ++i) { + if (splash_dds[i]) { + continue; + } + const auto& path = g_splash_paths[i]; + auto image = std::make_unique(graphics); + if (!image->Load(path)) { + LOGF("[Splash] could not load %s\n", path.string().c_str()); + FinishSplash(); + return; + } + splash_dds[i] = std::move(image); + } + const auto now = Clock::now(); + if (splash_born == Clock::time_point {}) { + splash_born = now; + } + const auto hidden = g_splash_hidden_at.load(std::memory_order_acquire); + const float fade = + hidden == 0 + ? 0.0f + : std::chrono::duration(now.time_since_epoch() - Clock::duration {hidden}) + .count() / + 0.3f; + const float alpha = 1.0f - std::clamp(fade, 0.0f, 1.0f); + if (alpha == 0.0f) { + FinishSplash(); + return; + } + const float age = std::chrono::duration(now - splash_born).count(); + const float logo = g_splash_fast ? 1.0f : std::clamp((age - 1.4f) / 2.2f, 0.0f, 1.0f); + const ImVec2 screen {static_cast(frame_extent.width), + static_cast(frame_extent.height)}; + const float fit = std::min(screen.x / 3840.0f, screen.y / 2160.0f); + const ImVec2 size {3840.0f * fit, 2160.0f * fit}; + const ImVec2 begin {(screen.x - size.x) * 0.5f, (screen.y - size.y) * 0.5f}; + const ImVec2 end {begin.x + size.x, begin.y + size.y}; + auto* draw = ImGui::GetBackgroundDrawList(); + draw->AddRectFilled({0, 0}, screen, IM_COL32(0, 0, 0, static_cast(255.0f * alpha))); + for (size_t i = 0; i < g_splash_paths.size(); ++i) { + draw->AddImage( + splash_dds[i]->Texture(), begin, end, {0, 0}, {1, 1}, + IM_COL32(255, 255, 255, static_cast(255.0f * alpha * (i == 0 ? 1.0f : logo)))); + } + } + void EnsureVulkan(vk::Format format, uint32_t image_count) { EnsureContext(); if (vulkan_initialized) { @@ -1215,7 +1309,8 @@ struct SystemOverlay::Impl { OverlaySnapshot snapshot; const bool has_overlay = GetOverlaySnapshot(&snapshot); const auto now = Clock::now(); - if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) { + if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) && + !g_splash_active.load(std::memory_order_acquire)) { return false; } const auto prepared_session = snapshot.session; @@ -1245,12 +1340,9 @@ struct SystemOverlay::Impl { last_frame = now; ImGui_ImplVulkan_NewFrame(); ImGui::NewFrame(); + DrawSplash(frame_extent); GetOverlaySnapshot(&snapshot); - if (snapshot.session != prepared_session) { - ImGui::EndFrame(); - return false; - } - if (has_overlay) { + if (has_overlay && snapshot.session == prepared_session) { if (snapshot.session.kind == OverlayKind::Dialog) { DrawDialog(snapshot.dialog, frame_extent); } else { @@ -1264,6 +1356,11 @@ struct SystemOverlay::Impl { } void Record(vk::CommandBuffer command, vk::ImageView target) { + for (const auto& image: splash_dds) { + if (image) { + image->Upload(command); + } + } vk::RenderingAttachmentInfo color {}; color.sType = vk::StructureType::eRenderingAttachmentInfo; color.imageView = target; @@ -1290,6 +1387,7 @@ struct SystemOverlay::Impl { return; } ImGui::SetCurrentContext(imgui_context); + splash_dds = {}; ImGui_ImplVulkan_Shutdown(); vulkan_initialized = false; } @@ -1309,6 +1407,8 @@ struct SystemOverlay::Impl { std::optional trophy_notification; Clock::time_point trophy_notification_born; std::vector> retired_trophy_images; + std::array, 2> splash_dds; + Clock::time_point splash_born; SDL_AudioStream* trophy_sound_stream = nullptr; uint8_t* trophy_sound_data = nullptr; uint32_t trophy_sound_size = 0; diff --git a/src/graphics/presentation/systemOverlay.h b/src/graphics/presentation/systemOverlay.h index 1ecdbc3d8..7a3c1e94e 100644 --- a/src/graphics/presentation/systemOverlay.h +++ b/src/graphics/presentation/systemOverlay.h @@ -5,6 +5,7 @@ #include "graphics/host_gpu/vulkanCommon.h" #include +#include #include #include #include @@ -21,6 +22,8 @@ struct SystemOverlayVisualState { uint64_t revision; }; +void InitializeSplashScreen(const std::filesystem::path& sce_sys); +void HideSplashScreen(); void InitializeSystemOverlayInput(SDL_Window* window); void ShutdownSystemOverlayInput(); bool ProcessSystemOverlayInput(const SDL_Event& event); diff --git a/src/libs/libSystemService.cpp b/src/libs/libSystemService.cpp index 690b813be..0cffc9bc2 100644 --- a/src/libs/libSystemService.cpp +++ b/src/libs/libSystemService.cpp @@ -4,6 +4,7 @@ #include "common/emulatorConfig.h" #include "common/logging/log.h" #include "common/stringUtils.h" +#include "graphics/presentation/systemOverlay.h" #include "libs/dialog.h" #include "libs/errno.h" #include "libs/libs.h" @@ -94,7 +95,7 @@ struct SystemServiceHdrToneMapLuminance { static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() { PRINT_NAME(); - + Graphics::HideSplashScreen(); return OK; } diff --git a/src/main.cpp b/src/main.cpp index 045055fe6..1ac7ffb7a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,6 +63,7 @@ static void PrintUsage() { ::printf(" --fullscreen Run in borderless desktop fullscreen.\n"); ::printf( " --hide-cursor Hide the cursor after 2 s idle. Default: off.\n"); + ::printf(" --no-splash Disable the startup splash screen.\n"); ::printf(" --vr Enable the virtual VR headset.\n"); ::printf(" --amd-cpu Apply AMD CPU instruction patches.\n"); ::printf(" --vblank-frequency Virtual vblank frequency. Default: 60.\n"); @@ -219,6 +220,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he continue; } + if (arg == "--no-splash") { + options.config.splash_enabled = false; + continue; + } + if (arg == "--vr") { options.config.vr_enabled = true; continue;