game: support splash screens (#4236)

Closes #1496 

This brings back the SCE splash screens. Also adds a runtime flag
`-nosplash` to skip it. Right now, it's on by default, but open to
changes on that (maybe always disable in debug to speed up start times,
etc.).
This commit is contained in:
Hat Kid
2026-04-20 03:19:08 +02:00
committed by GitHub
parent a63cb1d212
commit 1dff571820
20 changed files with 304 additions and 7 deletions
+2
View File
@@ -31,6 +31,8 @@ class GfxDisplay {
virtual std::shared_ptr<InputManager> get_input_manager() const = 0;
virtual void render() = 0;
virtual void init_splash() = 0;
virtual void draw_splash(int fb_w, int fb_h) = 0;
void set_imgui_visible(bool visible) {
m_imgui_visible = visible;
+1
View File
@@ -28,6 +28,7 @@ namespace Gfx {
std::function<void()> vsync_callback;
GfxGlobalSettings g_global_settings;
game_settings::DebugSettings g_debug_settings;
SplashScreen g_splash;
const GfxRendererModule* GetRenderer(GfxPipeline pipeline) {
switch (pipeline) {
+10
View File
@@ -6,8 +6,10 @@
*/
#include <array>
#include <atomic>
#include <functional>
#include <memory>
#include <vector>
#include "common/common_types.h"
#include "common/util/FileUtil.h"
@@ -130,4 +132,12 @@ void CollisionRendererSetMask(GfxGlobalSettings::CollisionRendererMode mode, s64
void CollisionRendererClearMask(GfxGlobalSettings::CollisionRendererMode mode, s64 mask_id);
void CollisionRendererSetMode(GfxGlobalSettings::CollisionRendererMode mode);
struct SplashScreen {
std::vector<u8> data;
int width = 0;
int height = 0;
std::atomic<bool> ready{false};
};
extern SplashScreen g_splash;
} // namespace Gfx
@@ -0,0 +1,7 @@
#version 410 core
in vec2 frag_uv;
out vec4 out_color;
uniform sampler2D splash_tex;
void main() {
out_color = texture(splash_tex, frag_uv);
}
@@ -0,0 +1,17 @@
#version 410 core
out vec2 frag_uv;
uniform vec2 u_res;
uniform vec2 u_tex;
void main() {
vec2 pos = vec2(float(gl_VertexID & 1) * 2.0 - 1.0, float(gl_VertexID >> 1) * 2.0 - 1.0);
frag_uv = vec2(pos.x * 0.5 + 0.5, 0.5 - pos.y * 0.5);
float tex_aspect = u_tex.x / u_tex.y;
float fb_aspect = u_res.x / u_res.y;
vec2 scale = (fb_aspect > tex_aspect)
? vec2(tex_aspect / fb_aspect, 1.0)
: vec2(1.0, fb_aspect / tex_aspect);
gl_Position = vec4(pos * scale, 0.0, 1.0);
}
+106
View File
@@ -344,7 +344,103 @@ GLDisplay::GLDisplay(SDL_Window* window, SDL_GLContext gl_context, bool is_main)
}));
}
void GLDisplay::init_splash() {
if (m_splash_program)
return;
if (!Gfx::g_splash.ready.load())
return;
auto shader_folder = "game/graphics/opengl_renderer/shaders";
auto vert_src =
file_util::read_text_file(file_util::get_file_path({shader_folder, "splash.vert"}));
auto frag_src =
file_util::read_text_file(file_util::get_file_path({shader_folder, "splash.frag"}));
constexpr int len = 1024;
GLint compile_ok;
char err[len];
auto compile_shader = [](GLenum type, const char* src, GLint& compile_ok, char* err) -> GLuint {
GLuint s = glCreateShader(type);
glShaderSource(s, 1, &src, nullptr);
glCompileShader(s);
glGetShaderiv(s, GL_COMPILE_STATUS, &compile_ok);
if (!compile_ok) {
glGetShaderInfoLog(s, len, nullptr, err);
lg::error("splash shader compile failed: {}\n", err);
glDeleteShader(s);
return 0;
}
return s;
};
GLuint vs = compile_shader(GL_VERTEX_SHADER, vert_src.c_str(), compile_ok, err);
GLuint fs = compile_shader(GL_FRAGMENT_SHADER, frag_src.c_str(), compile_ok, err);
if (!vs || !fs) {
glDeleteShader(vs);
glDeleteShader(fs);
return;
}
m_splash_program = glCreateProgram();
glAttachShader(m_splash_program, vs);
glAttachShader(m_splash_program, fs);
glLinkProgram(m_splash_program);
glDeleteShader(vs);
glDeleteShader(fs);
glGetProgramiv(m_splash_program, GL_LINK_STATUS, &compile_ok);
if (!compile_ok) {
glGetProgramInfoLog(m_splash_program, len, nullptr, err);
lg::error("Failed to link splash shader:\n{}", err);
glDeleteProgram(m_splash_program);
m_splash_program = 0;
return;
}
if (!Gfx::g_splash.data.empty()) {
glGenTextures(1, &m_splash_texture);
glBindTexture(GL_TEXTURE_2D, m_splash_texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, Gfx::g_splash.width, Gfx::g_splash.height, 0, GL_RGBA,
GL_UNSIGNED_BYTE, Gfx::g_splash.data.data());
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
Gfx::g_splash.data.clear();
Gfx::g_splash.data.shrink_to_fit();
}
glGenVertexArrays(1, &m_splash_vao);
}
void GLDisplay::draw_splash(int fb_w, int fb_h) {
if (!m_splash_program || !m_splash_texture)
return;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glClearColor(0.f, 0.f, 0.f, 1.f);
glClear(GL_COLOR_BUFFER_BIT);
glViewport(0, 0, fb_w, fb_h);
glDisable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
glUseProgram(m_splash_program);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_splash_texture);
glUniform1i(glGetUniformLocation(m_splash_program, "splash_tex"), 0);
glUniform2f(glGetUniformLocation(m_splash_program, "u_res"), fb_w, fb_h);
glUniform2f(glGetUniformLocation(m_splash_program, "u_tex"), Gfx::g_splash.width,
Gfx::g_splash.height);
glBindVertexArray(m_splash_vao);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glBindVertexArray(0);
}
GLDisplay::~GLDisplay() {
if (m_splash_texture)
glDeleteTextures(1, &m_splash_texture);
if (m_splash_program)
glDeleteProgram(m_splash_program);
if (m_splash_vao)
glDeleteVertexArrays(1, &m_splash_vao);
// Cleanup ImGUI
ImGuiIO& io = ImGui::GetIO();
io.IniFilename = nullptr;
@@ -551,6 +647,16 @@ void GLDisplay::render() {
// set the size of the visible/playable portion of the game in the window
get_display_manager()->set_game_size(Gfx::g_global_settings.lbox_w,
Gfx::g_global_settings.lbox_h);
// draw splash screen on startup if requested
if (SplashScreen && DiskBoot && !m_splash_program) {
init_splash();
}
if (SplashScreen && Gfx::g_splash.ready.load() &&
SplashTimer.getSeconds() < SPLASH_SCREEN_TIME) {
draw_splash(fbuf_w, fbuf_h);
}
render_game_frame(
game_res_w, game_res_h, fbuf_w, fbuf_h, Gfx::g_global_settings.lbox_w,
Gfx::g_global_settings.lbox_h, Gfx::g_global_settings.msaa_samples,
+7
View File
@@ -56,6 +56,13 @@ class GLDisplay : public GfxDisplay {
int width = 0;
int height = 0;
} m_pending_size;
// splash screen
GLuint m_splash_texture = 0;
GLuint m_splash_program = 0;
GLuint m_splash_vao = 0;
void init_splash();
void draw_splash(int fb_w, int fb_h);
};
extern const GfxRendererModule gRendererOpenGL;
+5
View File
@@ -11,6 +11,10 @@ u32 DiskBoot;
// Set to 1 to enable debug heap
u32 MasterDebug;
// added in pc port for splash screen
Timer SplashTimer;
u32 SplashScreen;
// Set to 1 to load debug code
u32 DebugSegment;
@@ -31,6 +35,7 @@ void kboot_init_globals_common() {
MasterDebug = 1;
DebugSegment = 1;
MasterUseKernel = 1;
SplashScreen = 1;
strcpy(DebugBootLevel, "#f"); // no specified level
strcpy(DebugBootMessage, "play"); // play mode, the default retail mode
memset(&masterConfig, 0, sizeof(MasterConfig));
+7
View File
@@ -1,11 +1,14 @@
#pragma once
#include "common/common_types.h"
#include "common/util/Timer.h"
#define GAME_TERRITORY_SCEA 0 // sony america
#define GAME_TERRITORY_SCEE 1 // sony europe
#define GAME_TERRITORY_SCEI 2 // sony inc. (japan)
#define GAME_TERRITORY_SCEK 3 // sony korea
#define SPLASH_SCREEN_TIME 3.f // how long to display the splash screen for
enum class RuntimeExitStatus {
RUNNING = 0,
RESTART_RUNTIME = 1,
@@ -41,6 +44,10 @@ extern char DebugBootMessage[64];
// Added in PC port, option to run listener functions without the kernel for debugging
extern u32 MasterUseKernel;
// Added in PC port for splash screen
extern Timer SplashTimer;
extern u32 SplashScreen;
struct MasterConfig {
u16 language; //! GOAL language 0
u16 aspect; //! SCE_ASPECT 2
+48 -2
View File
@@ -19,6 +19,7 @@
#include "game/kernel/common/kprint.h"
#include "game/kernel/common/kscheme.h"
#include "game/mips2c/mips2c_table.h"
#include "game/runtime.h"
#include "game/sce/libcdvd_ee.h"
#include "game/sce/libpad.h"
#include "game/sce/libscf.h"
@@ -70,9 +71,54 @@ void InitCD() {
/*!
* Initialize the GS and display the splash screen.
* Not yet implemented. TODO
*/
void InitVideo() {}
void InitVideo() {
if (!SplashScreen) {
lg::info("InitVideo: skipping splash!\n");
return;
}
std::map<int, std::string> lang_to_splash_map{
{SCE_JAPANESE_LANGUAGE, "JAP"}, {SCE_ENGLISH_LANGUAGE, "USA"},
{SCE_FRENCH_LANGUAGE, "FRE"}, {SCE_SPANISH_LANGUAGE, "SPA"},
{SCE_GERMAN_LANGUAGE, "GER"}, {SCE_ITALIAN_LANGUAGE, "ITA"},
{SCE_PORTUGUESE_LANGUAGE, "POR"}, {SCE_KOREAN_LANGUAGE, "KOR"},
};
auto lang = ee::sceScfGetLanguage();
auto filename = "SCREEN1." + lang_to_splash_map.at(lang);
auto path = file_util::get_jak_project_dir() / "out" / game_version_names[g_game_version] /
"iso" / filename;
if (lang != SCE_ENGLISH_LANGUAGE && !fs::exists(path)) {
lg::warn("InitVideo: file {} not found, falling back to english...\n", filename);
path = file_util::get_jak_project_dir() / "out" / game_version_names[g_game_version] / "iso" /
"SCREEN1.USA";
}
if (!fs::exists(path)) {
lg::warn("InitVideo: splash screen not found!\n");
return;
}
auto data = file_util::read_binary_file(path);
// width is always 512, height is sometimes different (e.g. demo screens), so we infer from file
// size
constexpr int kWidth = 512;
if (data.size() % (kWidth * 4) != 0) {
lg::error("InitVideo: splash size {} not divisible by stride {}", data.size(), kWidth * 4);
return;
}
int kHeight = data.size() / (kWidth * 4);
if ((int)data.size() != kWidth * kHeight * 4) {
lg::error("InitVideo: unexpected size {}, expected {} for splash screen", data.size(),
kWidth * kHeight * 4);
return;
}
Gfx::g_splash.data = std::move(data);
Gfx::g_splash.width = kWidth;
Gfx::g_splash.height = kHeight;
Gfx::g_splash.ready.store(true);
SplashTimer.start();
while (SplashTimer.getSeconds() < SPLASH_SCREEN_TIME) {
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
/*!
* Flush caches. Does all the memory, regardless of what you specify
+8
View File
@@ -114,6 +114,12 @@ void InitParms(int argc, const char* const* argv) {
masterConfig.disable_sound = true;
}
// added in pc port to skip the splash screen
if (arg == "-nosplash") {
Msg(6, "dkernel: skipping splash screen\n");
SplashScreen = false;
}
// GOAL Settings
// ----------------------------
@@ -137,6 +143,8 @@ void InitParms(int argc, const char* const* argv) {
Msg(6, "dkernel: debug mode\n");
MasterDebug = 1;
DebugSegment = 1;
// disable splash in debug
SplashScreen = 0;
}
// the "debug-mem" mode is used to set up GOAL in debug mode, but not to load debug-segments
+8
View File
@@ -138,6 +138,8 @@ void InitParms(int argc, const char* const* argv) {
Msg(6, "dkernel: debug mode\n");
MasterDebug = 1;
DebugSegment = 1;
// disable splash in debug
SplashScreen = 0;
}
// the "debug-mem" mode is used to set up GOAL in debug mode, but not to load debug-segments
@@ -184,6 +186,12 @@ void InitParms(int argc, const char* const* argv) {
Msg(6, "dkernel: no sound mode\n");
masterConfig.disable_sound = true;
}
// added in pc port to skip the splash screen
if (arg == "-nosplash") {
Msg(6, "dkernel: skipping splash screen\n");
SplashScreen = false;
}
}
}
+8
View File
@@ -128,6 +128,8 @@ void InitParms(int argc, const char* const* argv) {
Msg(6, "dkernel: debug mode\n");
MasterDebug = 1;
DebugSegment = 1;
// disable splash in debug
SplashScreen = 0;
}
// the "debug-mem" mode is used to set up GOAL in debug mode, but not to load debug-segments
@@ -187,6 +189,12 @@ void InitParms(int argc, const char* const* argv) {
Msg(6, "dkernel: no sound mode\n");
masterConfig.disable_sound = true;
}
// added in pc port to skip the splash screen
if (arg == "-nosplash") {
Msg(6, "dkernel: skipping splash screen\n");
SplashScreen = false;
}
}
}
+38
View File
@@ -1,11 +1,15 @@
#include "libscf.h"
#include <cstdlib>
#include <cstring>
#include <ctime>
#ifdef _WIN32
// clang-format off
#include <Windows.h>
#include <WinNls.h>
#elif __linux__
#include <clocale>
// clang-format on
#endif
@@ -42,6 +46,40 @@ int sceScfGetLanguage() {
}
} else if (curLangMain == LANG_DUTCH) {
return SCE_DUTCH_LANGUAGE;
} else if (curLangMain == LANG_KOREAN) {
return SCE_KOREAN_LANGUAGE;
}
#elif __linux__
const char* lang = std::getenv("LANG");
if (!lang)
return SCE_ENGLISH_LANGUAGE;
if (!std::strncmp(lang, "ja", 2)) {
return SCE_JAPANESE_LANGUAGE;
}
if (!std::strncmp(lang, "en", 2)) {
return SCE_ENGLISH_LANGUAGE;
}
if (!std::strncmp(lang, "fr", 2)) {
return SCE_FRENCH_LANGUAGE;
}
if (!std::strncmp(lang, "es", 2)) {
return SCE_SPANISH_LANGUAGE;
}
if (!std::strncmp(lang, "de", 2)) {
return SCE_GERMAN_LANGUAGE;
}
if (!std::strncmp(lang, "it", 2)) {
return SCE_ITALIAN_LANGUAGE;
}
if (!std::strncmp(lang, "pt", 2)) {
return SCE_PORTUGUESE_LANGUAGE;
}
if (!std::strncmp(lang, "nl", 2)) {
return SCE_DUTCH_LANGUAGE;
}
if (!std::strncmp(lang, "ko", 2)) {
return SCE_KOREAN_LANGUAGE;
}
#endif
return SCE_ENGLISH_LANGUAGE;
+2
View File
@@ -10,6 +10,8 @@
#define SCE_ITALIAN_LANGUAGE 5
#define SCE_DUTCH_LANGUAGE 6
#define SCE_PORTUGUESE_LANGUAGE 7
// added
#define SCE_KOREAN_LANGUAGE 8
#define SCE_ASPECT_43 0
#define SCE_ASPECT_FULL 1
+7 -5
View File
@@ -90,6 +90,7 @@
(define *all-mus* '())
(define *all-sbk* '())
(define *all-vag* '())
(define *all-screens* '())
(define *all-gc* '())
;;;;;;;;;;;;;;;;;;;;;;;
@@ -255,6 +256,9 @@
(defmacro copy-vag-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-vag* (cons (copy-iso-file "VAGWAD" "VAG/" (string-append "." ,x)) *all-vag*))) files)))
(defmacro copy-screen-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-screens* (cons (copy-iso-file "SCREEN1" "DRIVERS/" (string-append "." ,x)) *all-screens*))) files)))
(defmacro group (name &rest stuff)
`(defstep :in ""
:tool 'group
@@ -331,11 +335,6 @@
:tool 'copy
:out '("$OUT/iso/SAVEGAME.ICO"))
;; the loading screen file
(defstep :in "$ISO/DRIVERS/SCREEN1.USA"
:tool 'copy
:out '("$OUT/iso/SCREEN1.USA"))
;;;;;;;;;;;;;;;;;;;;;
;; Textures (Common)
;;;;;;;;;;;;;;;;;;;;;
@@ -2054,6 +2053,8 @@
(copy-vag-files "ENG" "FRE" "GER" "ITA" "SPA" "JAP")
(copy-screen-files "EUR" "FRE" "GER" "ITA" "SPA" "JAP" "USA")
;;;;;;;;;;;;;;;;;;;;;
;; ISO Group
;;;;;;;;;;;;;;;;;;;;;
@@ -2069,6 +2070,7 @@
,@(reverse *all-sbk*)
,@(reverse *all-mus*)
,@(reverse *all-vag*)
,@(reverse *all-screens*)
,@(reverse *all-cgos*))
)
+8
View File
@@ -57,6 +57,7 @@
(define *all-mus* '())
(define *all-sbk* '())
(define *all-vag* '())
(define *all-screens* '())
(define *all-gc* '())
(define *file-entry-map* (make-string-hash-table))
@@ -382,6 +383,12 @@
"DANGER11" "DIG" "FOREST" "FORTRESS" "MOUNTAIN" "PALCAB" "RACE"
"RUINS" "SEWER" "STRIP" "TOMB" "TWEAKVAL")
;;;;;;;;;;;;;;;;;;;;;
;; Splash Screens
;;;;;;;;;;;;;;;;;;;;;
(copy-screen-files "DEE" "DEJ" "DEM" "EUR" "FRE" "GER" "ITA" "JAP" "KOR" "SPA" "USA")
;;;;;;;;;;;;;;;;;;;;;
;; Text
;;;;;;;;;;;;;;;;;;;;;
@@ -425,6 +432,7 @@
,@(reverse *all-sbk*)
,@(reverse *all-mus*)
,@(reverse *all-vag*)
,@(reverse *all-screens*)
,@(reverse *all-cgos*))
)
+3
View File
@@ -182,6 +182,9 @@
(defmacro copy-vag-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-vag* (cons (copy-iso-file "VAGWAD" "VAG/" (string-append "." ,x)) *all-vag*))) files)))
(defmacro copy-screen-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-screens* (cons (copy-iso-file "SCREEN1" "DRIVERS/" (string-append "." ,x)) *all-screens*))) files)))
(defun reverse-list (list)
(let ((new-list '())
(curr-elt list))
+9
View File
@@ -57,6 +57,7 @@
(define *all-mus* '())
(define *all-sbk* '())
(define *all-vag* '())
(define *all-screens* '())
(define *all-gc* '())
(define *file-entry-map* (make-string-hash-table))
@@ -501,6 +502,13 @@
(defstep :in "$ISO/RES/TWEAKVAL.MUS"
:tool 'copy
:out '("$OUT/iso/TWEAKVAL.MUS"))
;;;;;;;;;;;;;;;;;;;;;
;; Splash Screens
;;;;;;;;;;;;;;;;;;;;;
(copy-screen-files "DEE" "DEJ" "DEM" "EUR" "FRE" "GER" "ITA" "JAP" "KOR" "POR" "SPA" "USA")
;;;;;;;;;;;;;;;;;;;;;
;; Text
;;;;;;;;;;;;;;;;;;;;;
@@ -543,6 +551,7 @@
,@(reverse *all-str*)
,@(reverse *all-sbk*)
,@(reverse *all-vag*)
,@(reverse *all-screens*)
,@(reverse *all-cgos*))
)
+3
View File
@@ -159,6 +159,9 @@
(defmacro copy-vag-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-vag* (cons (copy-iso-file "VAGWAD" "VAG/" (string-append "." ,x)) *all-vag*))) files)))
(defmacro copy-screen-files (&rest files)
`(begin ,@(apply (lambda (x) `(set! *all-screens* (cons (copy-iso-file "SCREEN1" "RES/" (string-append "." ,x)) *all-screens*))) files)))
(defun reverse-list (list)
(let ((new-list '())
(curr-elt list))