Files
questshock/android/app/src/main/cpp/qs_splash.c
T
2026-09-20 16:49:28 +02:00

111 lines
3.8 KiB
C

#include "qs_splash.h"
#include <SDL.h>
#include <android/log.h>
#include <stdbool.h>
#include <string.h>
#include "xr_session.h"
#define QS_SPLASH_HOLD_MS 2500
#define QS_SPLASH_FADE_MS 500
#define QS_SPLASH_TOTAL_MS (QS_SPLASH_HOLD_MS + 2 * QS_SPLASH_FADE_MS)
// The XR session only starts once frames are polled, so the display time is
// counted from the first submitted frame; this bounds the wait for it.
#define QS_SPLASH_SESSION_TIMEOUT_MS 5000
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "QuestShock", __VA_ARGS__)
extern void pump_events(void);
// OpenGL.cc (engine patch 11): draws a software frame into the XR game swapchain.
extern void android_composite_software_frame(SDL_Surface *ui);
static SDL_Surface *s_image = NULL;
void qs_splash_set_image(const uint32_t *argb, int width, int height) {
if (s_image != NULL) {
SDL_FreeSurface(s_image);
s_image = NULL;
}
if (argb == NULL || width <= 0 || height <= 0)
return;
s_image = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
if (s_image == NULL)
return;
for (int y = 0; y < height; y++)
memcpy((uint8_t *)s_image->pixels + (size_t)y * s_image->pitch, argb + (size_t)y * width,
(size_t)width * sizeof(uint32_t));
SDL_SetSurfaceBlendMode(s_image, SDL_BLENDMODE_BLEND);
}
// 0-255 brightness at `elapsed` ms after the first frame.
static int qs_splash_level(uint32_t elapsed) {
uint32_t edge = elapsed < QS_SPLASH_TOTAL_MS - elapsed ? elapsed : QS_SPLASH_TOTAL_MS - elapsed;
if (edge >= QS_SPLASH_FADE_MS)
return 255;
return (int)(255 * edge / QS_SPLASH_FADE_MS);
}
void qs_splash_show(void) {
if (s_image == NULL) {
LOGI("Startup splash: no image");
return;
}
int width, height;
xr_get_game_swapchain_size(&width, &height);
if (width <= 0 || height <= 0) {
LOGI("Startup splash: no XR game swapchain");
return;
}
// The image is stretched over the whole swapchain image, so it is
// letterboxed into a canvas of that size to keep its aspect ratio.
SDL_Surface *canvas =
SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGBA32);
if (canvas == NULL)
return;
const float scale = SDL_min((float)width / s_image->w, (float)height / s_image->h);
SDL_Rect dst;
dst.w = (int)(s_image->w * scale);
dst.h = (int)(s_image->h * scale);
dst.x = (width - dst.w) / 2;
dst.y = (height - dst.h) / 2;
const uint32_t waitStart = SDL_GetTicks();
uint32_t shownStart = 0;
uint32_t elapsed = 0;
int drawnLevel = -1;
bool done = false;
while (!done) {
pump_events();
xr_poll_events();
if (xr_frame_begin()) {
if (shownStart == 0)
shownStart = SDL_GetTicks();
elapsed = SDL_GetTicks() - shownStart;
if (elapsed >= QS_SPLASH_TOTAL_MS) {
elapsed = QS_SPLASH_TOTAL_MS;
done = true;
}
const int level = qs_splash_level(elapsed);
if (level != drawnLevel) {
drawnLevel = level;
SDL_FillRect(canvas, NULL, SDL_MapRGB(canvas->format, 0, 0, 0));
SDL_SetSurfaceColorMod(s_image, level, level, level);
SDL_BlitScaled(s_image, NULL, canvas, &dst);
}
android_composite_software_frame(canvas);
} else {
SDL_Delay(10);
if (shownStart == 0 && SDL_GetTicks() - waitStart > QS_SPLASH_SESSION_TIMEOUT_MS)
done = true;
}
xr_frame_end();
}
LOGI("Startup splash: %s after %u ms", shownStart != 0 ? "shown" : "XR session did not start",
(unsigned)(SDL_GetTicks() - waitStart));
SDL_FreeSurface(canvas);
}