Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion delta/formats/pkg_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,11 @@ struct PkgImpl {
if (be32(e) != wantId)
continue;
uint32_t off = be32(e + 16), sz = be32(e + 20);
if (sz == 0 || static_cast<uint64_t>(off) + sz > pkg.GetSize())
const uint32_t maxSize =
wantId == 0x1000 ? 1u << 20
: (wantId == 0x1200 ? 16u << 20 : UINT32_MAX);
if (sz == 0 || sz > maxSize ||
static_cast<uint64_t>(off) + sz > pkg.GetSize())
return false;
out.resize(sz);
R(off, sz, out.data());
Expand Down
15 changes: 15 additions & 0 deletions delta/gfx/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,19 @@ else()
${_imgui}/imgui_tables.cpp
${_imgui}/imgui_widgets.cpp)
target_include_directories(delta_gfx PRIVATE ${_imgui})

if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(_logo ${CMAKE_SOURCE_DIR}/delta/main/_res/logo.png)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_logo})
file(READ ${_logo} _logo_hex HEX)
string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\\1," _logo_bytes
"${_logo_hex}")
file(CONFIGURE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/prosperity_logo.h
CONTENT
"static constexpr unsigned char kProsperityLogoPng[] = {${_logo_bytes}};\n"
@ONLY)
target_include_directories(delta_gfx PRIVATE
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/vendor/equilibrium/external/tracy/profiler/src)
endif()
endif()
5 changes: 5 additions & 0 deletions delta/gfx/gfx.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* in the root of the source tree.
*/

#include <cstddef>
#include <cstdint>

// SDL3 window backed by a Vulkan swapchain. present() uploads a CPU framebuffer
Expand All @@ -24,6 +25,10 @@ enum class PixelFormat {
// already exists, otherwise it overrides the title the creator passes.
void setTitle(const char *title);

// Set PNG artwork for the game window. The desktop backend applies a small
// emulator badge before using it as the taskbar icon.
void setIcon(const uint8_t *png, size_t size);

// Create the window, Vulkan device and swapchain. Returns false on failure.
// Idempotent: returns true immediately if a window already exists.
bool init(const char *title, uint32_t width, uint32_t height);
Expand Down
94 changes: 94 additions & 0 deletions delta/gfx/gfx_vk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
// (gfx_headless.cpp) and the GPU renderer dumps frames instead of presenting.
#ifndef __ANDROID__

#include <algorithm>
#include <array>
#include <atomic>
#include <cstdint>
Expand All @@ -26,6 +27,14 @@
#include <utility>
#include <vector>

#if defined(__linux__)
#define STBI_ONLY_PNG
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

#include "prosperity_logo.h"
#endif

#include <SDL3/SDL.h>
#include <SDL3/SDL_vulkan.h>
#include <vulkan/vulkan.h>
Expand All @@ -52,6 +61,7 @@ namespace {
// videoout HLE race to bring the window up and each passes its own generic
// title, so whoever wins uses this instead when it is set.
std::string g_title;
std::vector<uint8_t> g_iconPng;

constexpr uint32_t kFrameSlotCount = 2;

Expand Down Expand Up @@ -106,6 +116,75 @@ struct State {
State g;
std::atomic_bool g_canPresent{true};
constexpr uint64_t kPresentWaitSliceNs = 50'000'000;
constexpr size_t kMaxIconSize = 16u << 20;
constexpr int kMaxIconDimension = 4096;

#if defined(__linux__)
void drawBadge(uint8_t *pixels, int width, int height, const uint8_t *logo,
int logoWidth, int logoHeight) {
const int size = std::max(1, std::min(width, height) * 3 / 8);
const int left = width - size;
for (int y = 0; y < size; ++y) {
for (int x = 0; x < size; ++x) {
const int px = left + x;
uint8_t *rgba = pixels + (static_cast<size_t>(y) * width + px) * 4;
const uint8_t *badge =
logo + (static_cast<size_t>(y * logoHeight / size) * logoWidth +
x * logoWidth / size) *
4;
const uint32_t alpha = badge[3];
const uint32_t dstAlpha = rgba[3];
const uint32_t outAlpha = alpha * 255 + dstAlpha * (255 - alpha);
if (outAlpha) {
for (int channel = 0; channel < 3; ++channel)
rgba[channel] = static_cast<uint8_t>(
(badge[channel] * alpha * 255 +
rgba[channel] * dstAlpha * (255 - alpha) + outAlpha / 2) /
outAlpha);
}
rgba[3] = static_cast<uint8_t>((outAlpha + 127) / 255);
}
}
}

void applyWindowIcon() {
if (!g.window || g_iconPng.empty() || g_iconPng.size() > kMaxIconSize)
return;
int width = 0;
int height = 0;
int channels = 0;
if (!stbi_info_from_memory(g_iconPng.data(),
static_cast<int>(g_iconPng.size()), &width,
&height, &channels) ||
width > kMaxIconDimension || height > kMaxIconDimension)
return;
stbi_uc *pixels = stbi_load_from_memory(
g_iconPng.data(), static_cast<int>(g_iconPng.size()), &width, &height,
&channels, STBI_rgb_alpha);
if (!pixels) {
stbi_image_free(pixels);
return;
}
int logoWidth = 0;
int logoHeight = 0;
stbi_uc *logo =
stbi_load_from_memory(kProsperityLogoPng, sizeof(kProsperityLogoPng),
&logoWidth, &logoHeight, nullptr, STBI_rgb_alpha);
if (!logo) {
stbi_image_free(pixels);
return;
}
drawBadge(pixels, width, height, logo, logoWidth, logoHeight);
SDL_Surface *surface = SDL_CreateSurfaceFrom(
width, height, SDL_PIXELFORMAT_RGBA32, pixels, width * STBI_rgb_alpha);
if (surface) {
SDL_SetWindowIcon(g.window, surface);
SDL_DestroySurface(surface);
}
stbi_image_free(logo);
stbi_image_free(pixels);
}
#endif

void stopPresenting(const char *operation, VkResult result) {
std::fprintf(stderr, "[gfx] %s failed: VkResult=%d\n", operation, result);
Expand Down Expand Up @@ -425,6 +504,9 @@ bool init(const char *title, uint32_t width, uint32_t height) {
std::fprintf(stderr, "[gfx] SDL_CreateWindow failed: %s\n", SDL_GetError());
return false;
}
#if defined(__linux__)
applyWindowIcon();
#endif

// Instance: SDL-required extensions + optional validation.
uint32_t nExt = 0;
Expand Down Expand Up @@ -761,6 +843,18 @@ void setTitle(const char *title) {
SDL_SetWindowTitle(g.window, g_title.c_str());
}

void setIcon(const uint8_t *png, size_t size) {
#if defined(__linux__)
if (size > kMaxIconSize)
return;
g_iconPng.assign(png, png + size);
applyWindowIcon();
#else
(void)png;
(void)size;
#endif
}

bool available() {
return g.window != nullptr && g.swapchain != VK_NULL_HANDLE;
}
Expand Down
Binary file modified delta/main/_res/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
128 changes: 116 additions & 12 deletions delta/main/dcore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ bool deltaCore::init() {
}

namespace {
constexpr uint64_t kMaxSfoSize = 1u << 20;
constexpr uint64_t kMaxIconSize = 16u << 20;

// Minimal param.sfo reader: return the string value of `key` (e.g. "TITLE_ID"),
// or "" if absent. The SFO is a small flat table; all offsets are bounds-checked.
std::string sfoGet(const uint8_t *d, size_t n, const char *key) {
Expand Down Expand Up @@ -90,6 +93,28 @@ std::string jsonGetString(const std::string &js, const char *key) {
return js.substr(open + 1, close - open - 1);
}

bool readHostFile(const std::string &path, uint64_t maxSize,
std::vector<uint8_t> &out) {
utl::File file(base::String(path.c_str()), utl::fileMode::read);
if (!file.IsOpen())
return false;
const uint64_t size = file.GetSize();
if (size == 0 || size > maxSize)
return false;
out.resize(static_cast<size_t>(size));
if (file.Read(out.data(), out.size()) != size) {
out.clear();
return false;
}
return true;
}

std::string parentPath(const base::String &path) {
const std::string value(path.c_str());
const size_t slash = value.find_last_of("/\\");
return slash == std::string::npos ? std::string(".") : value.substr(0, slash);
}

// param.json stores sdkVersion as "0xMMmmpppp00000000"; libkernel wants the top
// half (0x03000000 for a 3.00 title). Empty/unparsable -> 0.
uint32_t parseSdkVersion(const std::string &s) {
Expand Down Expand Up @@ -154,6 +179,19 @@ class PkgProvider : public krnl::vfs::VirtualProvider {
return {};
}

std::string title() {
std::vector<uint8_t> sfo;
if (fs_.readPkgEntry(0x1000, sfo) > 0)
return sfoGet(sfo.data(), sfo.size(), "TITLE");
return {};
}

std::vector<uint8_t> icon() {
std::vector<uint8_t> png;
fs_.readPkgEntry(0x1200, png);
return png;
}

// SOTTR workaround: cache every .manifest.bin's bytes keyed by its base name
// (e.g. "PRIORITY7_ENGLISH"), so the count-setter can fill the header buffer
// with correct data (the engine's async manifest reader races on our threads).
Expand Down Expand Up @@ -305,6 +343,18 @@ class Ufs2Provider : public krnl::vfs::VirtualProvider {
// instead of the PS4 param.sfo; pull the "titleId" string out of it.
std::string titleId() { return paramJsonField("titleId"); }

std::vector<uint8_t> icon() {
const auto *node = fs_.find("/sce_sys/icon0.png");
if (!node || node->size > kMaxIconSize)
return {};
std::vector<uint8_t> png(node->size);
const int64_t read = fs_.read(*node, png.data(), 0, node->size);
if (read <= 0)
return {};
png.resize(static_cast<size_t>(read));
return png;
}

// param.json spells the SDK version as a 64-bit hex string ("0x0300...")
// whose top half is the 0xMMmmpppp form libkernel compares against.
uint32_t sdkVersion() { return parseSdkVersion(paramJsonField("sdkVersion")); }
Expand Down Expand Up @@ -361,13 +411,24 @@ void deltaCore::boot(const base::String &xdir) {

const bool isPkg = endsWithIgnoreCase(xdir, ".pkg");
const bool isFfpkg = endsWithIgnoreCase(xdir, ".ffpkg");
// A raw app dump: the extracted /app0 tree itself, identified by its PS5
// sce_sys/param.json. Host-mounted rather than read through an image reader.
std::string appJson = std::string(path.c_str()) + "/sce_sys/param.json";
const bool isAppDir = !isPkg && !isFfpkg && utl::File(base::String(appJson.c_str()),
utl::fileMode::read).Exists();
// A raw app dump: the extracted /app0 tree itself, identified by its console
// metadata. Host-mounted rather than read through an image reader.
const std::string appRoot(path.c_str());
const std::string appSfo = appRoot + "/sce_sys/param.sfo";
const std::string appJson = appRoot + "/sce_sys/param.json";
const bool isPs4AppDir =
!isPkg && !isFfpkg &&
utl::File(base::String(appSfo.c_str()), utl::fileMode::read).Exists();
const bool isPs5AppDir =
!isPkg && !isFfpkg && !isPs4AppDir &&
utl::File(base::String(appJson.c_str()), utl::fileMode::read).Exists();
const bool isAppDir = isPs4AppDir || isPs5AppDir;
base::String mainModule = path;
uint32_t sdkVersion = 0;
std::string gameTitle;
#if defined(__linux__) && !defined(__ANDROID__)
std::vector<uint8_t> gameIcon;
#endif

if (isPkg) {
auto provider = std::make_shared<PkgProvider>(path);
Expand All @@ -380,6 +441,10 @@ void deltaCore::boot(const base::String &xdir) {
// Publish the title id so savedata can give this game its own host save
// root (else saves for different titles collide under one directory).
krnl::vfs::setTitleId(provider->titleId());
gameTitle = provider->title();
#if defined(__linux__) && !defined(__ANDROID__)
gameIcon = provider->icon();
#endif
mainModule = base::String("/app0/eboot.bin");
} else if (isFfpkg) {
// PS5 game backup (UFS2). Mount it at /app0 and prefer the decrypted/ tree
Expand All @@ -393,22 +458,53 @@ void deltaCore::boot(const base::String &xdir) {
bool decrypted = provider->hasDecrypted();
krnl::vfs::mountVirtual("/app0", provider);
krnl::vfs::setTitleId(provider->titleId());
#if defined(__linux__) && !defined(__ANDROID__)
gameIcon = provider->icon();
#endif
sdkVersion = provider->sdkVersion();
mainModule = base::String(decrypted ? "/app0/decrypted/eboot.bin"
: "/app0/eboot.bin");
LOG_INFO("mounted ffpkg at /app0 ({}), boot module {}",
krnl::vfs::titleId().c_str(), mainModule.c_str());
} else if (isAppDir) {
krnl::vfs::mount("/app0", path.c_str());
if (utl::File f(base::String(appJson.c_str()), utl::fileMode::read); f.IsOpen()) {
std::string js(static_cast<size_t>(f.GetSize()), '\0');
f.Read(js.data(), js.size());
if (isPs4AppDir) {
std::vector<uint8_t> sfo;
if (readHostFile(appSfo, kMaxSfoSize, sfo)) {
krnl::vfs::setTitleId(sfoGet(sfo.data(), sfo.size(), "TITLE_ID"));
gameTitle = sfoGet(sfo.data(), sfo.size(), "TITLE");
}
#if defined(__linux__) && !defined(__ANDROID__)
if (!readHostFile(appRoot + "/sce_sys/icon0.png", kMaxIconSize, gameIcon))
readHostFile(appRoot + "/icon0.png", kMaxIconSize, gameIcon);
#endif
} else {
std::vector<uint8_t> json;
readHostFile(appJson, kMaxSfoSize, json);
const std::string js(json.begin(), json.end());
krnl::vfs::setTitleId(jsonGetString(js, "titleId"));
sdkVersion = parseSdkVersion(jsonGetString(js, "sdkVersion"));
#if defined(__linux__) && !defined(__ANDROID__)
if (!readHostFile(appRoot + "/sce_sys/icon0.png", kMaxIconSize, gameIcon))
readHostFile(appRoot + "/icon0.png", kMaxIconSize, gameIcon);
#endif
}
mainModule = base::String("/app0/eboot.bin");
LOG_INFO("mounted app dir at /app0 ({}), boot module {}",
krnl::vfs::titleId().c_str(), mainModule.c_str());
} else {
const std::string root = parentPath(path);
std::vector<uint8_t> sfo;
if (!readHostFile(root + "/sce_sys/param.sfo", kMaxSfoSize, sfo))
readHostFile(root + "/param.sfo", kMaxSfoSize, sfo);
if (!sfo.empty()) {
krnl::vfs::setTitleId(sfoGet(sfo.data(), sfo.size(), "TITLE_ID"));
gameTitle = sfoGet(sfo.data(), sfo.size(), "TITLE");
}
#if defined(__linux__) && !defined(__ANDROID__)
if (!readHostFile(root + "/sce_sys/icon0.png", kMaxIconSize, gameIcon))
readHostFile(root + "/icon0.png", kMaxIconSize, gameIcon);
#endif
}

// /download0 is the title's writable data volume (patches, add-on content,
Expand All @@ -426,14 +522,22 @@ void deltaCore::boot(const base::String &xdir) {

// These all boot from an /app0 mount rather than a bare host path.
const bool mounted = isPkg || isFfpkg || isAppDir;
const bool isPs5 = isFfpkg || isAppDir;
// Name the window after the booted title, since the renderer and the videoout
const bool isPs5 = isFfpkg || isPs5AppDir;
// Name the window after the booted game, since the renderer and the videoout
// HLE both bring it up with a generic title depending on who gets there first.
{
const std::string &tid = krnl::vfs::titleId();
gfx::setTitle(("prosperity - " + (tid.empty() ? std::string("unknown") : tid) +
(isPs5 ? " (PS5)" : " (PS4)")).c_str());
std::string title =
gameTitle.empty() ? (tid.empty() ? std::string("unknown") : tid)
: gameTitle;
title += " - prosperity";
title += isPs5 ? " (PS5)" : " (PS4)";
gfx::setTitle(title.c_str());
}
#if defined(__linux__) && !defined(__ANDROID__)
if (!gameIcon.empty())
gfx::setIcon(gameIcon.data(), gameIcon.size());
#endif
std::thread ctx([mainModule = std::move(mainModule), mounted, isPs5, sdkVersion]() {
auto p = base::MakeUnique<krnl::proc>();
if (isPs5)
Expand Down
Loading