16 Commits

Author SHA1 Message Date
8d5a106ab3 docs: Update documentation 2026-05-26 16:02:43 +05:30
4e7c53d79f docs: Update README to remove video and add asset link
Removed video demo from README and added a link to GitHub assets.
2026-05-26 15:57:15 +05:30
bb2b60de5b fix: md rendering 2026-05-26 15:52:58 +05:30
1159868e15 docs: Added demo video 2026-05-26 15:51:20 +05:30
e9c939f033 chore: delete binaries 2026-05-26 13:16:19 +05:30
94184c73d0 feat: add a 250ms delay between game window and splash close 2026-05-26 13:15:22 +05:30
2fe20ad576 fix: Timing issues for video playback
- Remove manual 33ms loop wait
2026-05-26 13:02:05 +05:30
433ccfb0fe refactor: video player code to video.c 2026-05-26 12:19:25 +05:30
c96c361973 feat: Add dummy game utility and enhance testing capabilities
- also improve argument handling
2026-05-26 12:05:39 +05:30
1cb0097e68 fix #1: Use SDL_CreateTexture instead of IMG_LoadTexture 2026-05-25 20:10:40 +05:30
b9ed8ffb4e feat: Background as video support 2026-05-24 11:35:49 +05:30
be9093655d feat: Add render mode options for image display in gsplash 2026-05-24 11:00:21 +05:30
b5de077ac3 refactor: Simplify Makefile structure 2026-05-24 10:24:15 +05:30
d220793b2d fix: binary rename fr fr 2026-05-24 10:13:07 +05:30
d3c0a3e58e fix: PKGBUILD and makefile and rename binary 2026-05-24 10:09:47 +05:30
744e12a926 chore: Update .gitignore and remove Python version file 2026-05-24 09:54:11 +05:30
13 changed files with 926 additions and 91 deletions

64
.gitignore vendored
View File

@@ -1,10 +1,60 @@
# Python-generated files
__pycache__/
*.py[oc]
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
build/
dist/
wheels/
*.egg-info
# Virtual environments
.venv
pkg/
# debug information files
*.dwo

View File

@@ -1 +0,0 @@
3.12

View File

@@ -1,32 +1,53 @@
# Compiler settings
CC = gcc
CFLAGS = -O2 -Wall -Wextra $(shell pkg-config --cflags sdl2 SDL2_image)
LIBS = $(shell pkg-config --libs sdl2 SDL2_image)
# Makefile for gsplash (community-standard layout)
# Project Structure
TARGET = game-splash
SRC = src/gsplash.c
# Installation Paths (Defaults to user local bin)
PREFIX ?= /usr/local
bindir ?= $(PREFIX)/bin
# Default target run when typing just 'make'
all: $(TARGET)
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra
PKG_CONFIG ?= pkg-config
# Compilation rule
$(TARGET): $(SRC)
$(CC) $(CFLAGS) $(SRC) -o $(TARGET) $(LIBS)
SDL_CFLAGS := $(shell $(PKG_CONFIG) --cflags sdl2 SDL2_image)
SDL_LIBS := $(shell $(PKG_CONFIG) --libs sdl2 SDL2_image)
FFMPEG_CFLAGS := $(shell $(PKG_CONFIG) --cflags libavformat libavcodec libswscale libavutil)
FFMPEG_LIBS := $(shell $(PKG_CONFIG) --libs libavformat libavcodec libswscale libavutil)
ALL_CFLAGS := $(CFLAGS) $(SDL_CFLAGS) $(FFMPEG_CFLAGS)
ALL_LIBS := $(LIBS) $(SDL_LIBS) $(FFMPEG_LIBS) -lm
BUILD_DIR ?= build
TARGET = $(BUILD_DIR)/gsplash
DUMMY_TARGET = $(BUILD_DIR)/dummy_game
SRC = src/gsplash.c src/video.c
DUMMY_SRC = src/dummy_game.c
.PHONY: all clean install uninstall check
all: $(TARGET) $(DUMMY_TARGET)
$(BUILD_DIR):
mkdir -p $(BUILD_DIR)
$(TARGET): $(BUILD_DIR) $(SRC)
$(CC) $(ALL_CFLAGS) $(LDFLAGS) $(SRC) -o $@ $(ALL_LIBS)
$(DUMMY_TARGET): $(BUILD_DIR) $(DUMMY_SRC)
$(CC) $(CFLAGS) $(SDL_CFLAGS) $(LDFLAGS) $(DUMMY_SRC) -o $@ $(SDL_LIBS)
# Lightweight smoke test (headless via SDL_VIDEODRIVER=dummy)
check: $(TARGET) $(DUMMY_TARGET)
@echo "Running smoke test (headless)..."
SDL_VIDEODRIVER=dummy $(TARGET) nonexistent.png /bin/true || true
@echo "Running CLI test suite..."
./tests/test_cli.sh
@echo "All tests finished successfully"
# Install binary system-wide
install: $(TARGET)
install -Dm755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
install -d "$(DESTDIR)$(bindir)"
install -m 755 $(TARGET) "$(DESTDIR)$(bindir)/gsplash"
# Remove binary from system
uninstall:
rm -f $(DESTDIR)$(PREFIX)/bin/$(TARGET)
rm -f "$(DESTDIR)$(bindir)/gsplash"
# Clean build artifacts
clean:
rm -f $(TARGET)
.PHONY: all install uninstall clean
rm -f $(TARGET) $(DUMMY_TARGET)

View File

@@ -1,24 +1,22 @@
pkgname=gsplash-git
pkgver=0.1.0
pkgrel=1
pkgdesc="A lightweight SDL2 splash screen wrapper for game launchers."
pkgdesc="A lightweight native SDL2 splash screen wrapper for game launchers."
arch=('x86_64')
depends=('sdl2' 'sdl2_image')
license=('custom')
depends=('sdl2' 'sdl2_image' 'ffmpeg')
makedepends=('make' 'gcc' 'pkgconf')
source=('src/splash.c' 'Makefile')
sha256sums=('SKIP' 'SKIP')
prepare() {
mkdir -p "$srcdir/src"
cp "$srcdir/splash.c" "$srcdir/src/"
}
# Leave this empty so makepkg doesn't look for external downloads or local copies
source=()
sha256sums=()
build() {
cd "$srcdir"
cd "$startdir"
make
}
package() {
cd "$srcdir"
cd "$startdir"
make DESTDIR="$pkgdir" PREFIX="/usr" install
}
}

View File

@@ -2,10 +2,12 @@
A fullscreen splash-screen wrapper for launching a game or app. It displays an image (or a fallback black screen), starts your executable, and closes when the process exits or loses focus after launch.
https://github.com/user-attachments/assets/22da49b4-0f1f-4208-8d0b-9eeef14e35e5
## Features
1. Fullscreen, borderless splash screen with hidden cursor (SDL2)
2. Displays a supplied image via SDL2_image (fallback to black if load fails)
2. Displays a supplied image via SDL2_image or video via ffmpeg (fallback to black if load fails)
3. Launches the target executable and exits when it finishes
4. Hides on focus loss after the game starts or closes on **Esc**
@@ -16,6 +18,7 @@ A fullscreen splash-screen wrapper for launching a game or app. It displays an i
- pkg-config
- SDL2
- SDL2_image
- ffmpeg (libavformat, libavcodec, libswscale, libavutil)
## Install
@@ -28,18 +31,75 @@ makepkg -si
For other distributions, build and install manually:
```bash
# Build the binary into build/
make
# Install system-wide (defaults to /usr/local)
sudo make install
# Staged install (useful for packaging):
DESTDIR=/some/staging/path make install
```
## Usage
Build output installs to `/usr/local/bin` by default. Override with `PREFIX`.
```bash
game-splash <image_path> <game_executable> [game_arguments...]
gsplash <image_or_video_path> <game_executable> [game_arguments...]
```
Example:
```bash
game-splash assets/splash.jpg /path/to/game --fullscreen --profile=default
gsplash assets/splash.jpg /path/to/game --fullscreen --profile=default
# Video splash (supported formats depend on ffmpeg build)
gsplash assets/splash.mp4 /path/to/game --fullscreen --profile=default
```
Gsplash allows you to configure how the image or video is displayed with 3 modes:
- `center` (default): letterbox
- `crop`: fill screen by cropping
- `stretch`: Distort to fill screen
You can set these by using the `-m` or `--mode` flag:
```bash
build/gsplash [--mode=stretch|center|crop] <background> <executable> [args...]
build/gsplash -m stretch|center|crop <background> <executable> [args...]
```
## Testing
Gsplash includes several testing utilities to ensure proper functionality without requiring a heavy game binary.
### Automated Testing
Run the automated test suite (which includes a headless smoke test and CLI argument validation) using:
```bash
make check
```
### Interactive Visual Testing
To physically test the splash screen rendering modes (`stretch`, `center`, `crop`) with a real image, use the interactive test script. It launches `gsplash` with each mode and prompts you to confirm if it displayed correctly:
```bash
./tests/test_interactive.sh path/to/your/image.png
```
*(Tip: You can place your test images in the `tests/assets/` directory).*
### Dummy Game Utility
For manual testing, a `dummy_game` binary is built alongside `gsplash`. It mimics a real game by sleeping to simulate startup time, creating an SDL window to trigger `gsplash`'s focus loss detection and exiting cleanly.
```bash
./build/gsplash path/to/image.png ./build/dummy_game
# Test with a custom 10 second simulated game load time
./build/gsplash path/to/image.png ./build/dummy_game 10
```

BIN
docs/assets/demo.mp4 Normal file

Binary file not shown.

67
src/dummy_game.c Normal file
View File

@@ -0,0 +1,67 @@
#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
int delay_seconds = 5;
if (argc > 1) {
int parsed = atoi(argv[1]);
if (parsed > 0) {
delay_seconds = parsed;
}
}
printf("[dummy_game] Simulating engine startup for %d seconds...\n", delay_seconds);
for (int i = 0; i < delay_seconds; i++) {
sleep(1);
printf("[dummy_game] Loading... %d/%d\n", i + 1, delay_seconds);
}
printf("[dummy_game] Creating window to trigger focus-loss on splash screen...\n");
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
fprintf(stderr, "[dummy_game] SDL Init Failed: %s\n", SDL_GetError());
return 1;
}
SDL_Window *window = SDL_CreateWindow(
"Dummy Game Window",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
800, 600,
SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE
);
if (!window) {
fprintf(stderr, "[dummy_game] Window Creation Failed: %s\n", SDL_GetError());
SDL_Quit();
return 1;
}
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer) {
SDL_SetRenderDrawColor(renderer, 0, 120, 255, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
}
printf("[dummy_game] Window created. Waiting 5 seconds before exiting...\n");
SDL_Event event;
int running = 1;
Uint32 start_time = SDL_GetTicks();
while (running && (SDL_GetTicks() - start_time < 5000)) {
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
running = 0;
}
}
SDL_Delay(50);
}
printf("[dummy_game] Exiting normally.\n");
if (renderer) SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}

View File

@@ -7,8 +7,14 @@
#include <stdarg.h>
#include <errno.h>
#include <string.h>
#include <math.h>
#include <stdbool.h>
#include <ctype.h>
static void log_info(const char* fmt, ...) {
#include "video.h"
static void log_info(const char *fmt, ...)
{
va_list args;
fprintf(stderr, "[splash] ");
va_start(args, fmt);
@@ -18,7 +24,8 @@ static void log_info(const char* fmt, ...) {
fflush(stderr);
}
static void log_error(const char* fmt, ...) {
static void log_error(const char *fmt, ...)
{
va_list args;
fprintf(stderr, "[splash][error] ");
va_start(args, fmt);
@@ -28,23 +35,102 @@ static void log_error(const char* fmt, ...) {
fflush(stderr);
}
int main(int argc, char* argv[]) {
if (argc < 3) {
log_error("Usage: %s <image_path> <game_executable> [args...]", argv[0]);
typedef enum RenderMode
{
RENDER_STRETCH = 0,
RENDER_CENTER,
RENDER_CROP
} RenderMode;
static RenderMode parse_render_mode(const char *value)
{
if (strcmp(value, "center") == 0)
{
return RENDER_CENTER;
}
if (strcmp(value, "crop") == 0)
{
return RENDER_CROP;
}
return RENDER_STRETCH;
}
static void compute_dest_rect(int src_w, int src_h, int out_w, int out_h, RenderMode mode, SDL_Rect *dst)
{
dst->x = 0;
dst->y = 0;
dst->w = out_w;
dst->h = out_h;
if (src_w <= 0 || src_h <= 0 || out_w <= 0 || out_h <= 0)
{
return;
}
if (mode == RENDER_CENTER)
{
float scale = fminf((float)out_w / (float)src_w, (float)out_h / (float)src_h);
dst->w = (int)(src_w * scale);
dst->h = (int)(src_h * scale);
dst->x = (out_w - dst->w) / 2;
dst->y = (out_h - dst->h) / 2;
}
else if (mode == RENDER_CROP)
{
float scale = fmaxf((float)out_w / (float)src_w, (float)out_h / (float)src_h);
dst->w = (int)(src_w * scale);
dst->h = (int)(src_h * scale);
dst->x = (out_w - dst->w) / 2;
dst->y = (out_h - dst->h) / 2;
}
}
int main(int argc, char *argv[])
{
RenderMode render_mode = RENDER_CENTER;
int arg_index = 1;
if (argc >= 3 && strncmp(argv[1], "--mode=", 7) == 0)
{
render_mode = parse_render_mode(argv[1] + 7);
arg_index += 1;
}
else if (argc >= 2 && strcmp(argv[1], "-m") == 0)
{
if (argc < 4)
{
log_error("Usage: %s [--mode=stretch|center|crop] <image_path> <game_executable> [args...]", argv[0]);
log_error(" %s -m stretch|center|crop <image_path> <game_executable> [args...]", argv[0]);
return 1;
}
render_mode = parse_render_mode(argv[2]);
arg_index += 2;
}
if (argc - arg_index < 2)
{
log_error("Usage: %s [--mode=stretch|center|crop] <image_path> <game_executable> [args...]", argv[0]);
log_error(" %s -m stretch|center|crop <image_path> <game_executable> [args...]", argv[0]);
return 1;
}
log_info("Starting splash: image='%s', game='%s'", argv[1], argv[2]);
const char *image_path = argv[arg_index];
const char *game_path = argv[arg_index + 1];
log_info("Starting splash: image='%s', game='%s', mode=%d", image_path, game_path, render_mode);
// Initialize SDL2 Video subsystems
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
if (SDL_Init(SDL_INIT_VIDEO) < 0)
{
log_error("SDL Init Failed: %s", SDL_GetError());
return 1;
}
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
log_info("SDL initialized");
// Initialize JPEG and PNG decoders
if (!(IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG) & (IMG_INIT_JPG | IMG_INIT_PNG))) {
if (!(IMG_Init(IMG_INIT_JPG | IMG_INIT_PNG) & (IMG_INIT_JPG | IMG_INIT_PNG)))
{
log_error("SDL_image Init Failed: %s", IMG_GetError());
SDL_Quit();
return 1;
@@ -52,14 +138,14 @@ int main(int argc, char* argv[]) {
log_info("SDL_image initialized (JPG/PNG)");
// Create a native borderless fullscreen window
SDL_Window* window = SDL_CreateWindow(
SDL_Window *window = SDL_CreateWindow(
"Game Splash Screen",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
0, 0,
SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS
);
SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_BORDERLESS);
if (!window) {
if (!window)
{
log_error("Window Creation Failed: %s", SDL_GetError());
IMG_Quit();
SDL_Quit();
@@ -70,87 +156,250 @@ int main(int argc, char* argv[]) {
SDL_ShowCursor(SDL_DISABLE); // Hide the mouse pointer
// Create a hardware-accelerated renderer
SDL_Renderer* renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer) {
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
if (renderer)
{
log_info("Renderer created (accelerated)");
}
SDL_Texture* texture = IMG_LoadTexture(renderer, argv[1]);
SDL_Texture *texture = NULL;
VideoPlayer video_player;
bool video_active = false;
if (!texture) {
log_error("Failed to load splash image '%s': %s; showing black screen", argv[1], IMG_GetError());
if (has_video_extension(image_path))
{
if (init_video_player(&video_player, renderer, image_path))
{
video_active = true;
}
else
{
log_error("Failed to open video '%s'; showing black screen", image_path);
}
}
else
{
SDL_Surface *surface = IMG_Load(image_path);
if (surface)
{
SDL_Surface *rgba_surface = SDL_ConvertSurfaceFormat(surface, SDL_PIXELFORMAT_RGBA32, 0);
SDL_FreeSurface(surface);
if (rgba_surface)
{
texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING,
rgba_surface->w, rgba_surface->h);
if (texture)
{
SDL_UpdateTexture(texture, NULL, rgba_surface->pixels, rgba_surface->pitch);
}
else
{
log_error("Failed to create texture: %s", SDL_GetError());
}
SDL_FreeSurface(rgba_surface);
}
}
if (!texture && init_video_player(&video_player, renderer, image_path))
{
video_active = true;
}
}
if (!texture && !video_active)
{
log_error("Failed to load splash image '%s': %s; showing black screen", image_path, IMG_GetError());
// Fallback: Clear to solid black if image file is broken
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
} else {
log_info("Splash image loaded");
SDL_RenderClear(renderer);
// Automatically scales your image to perfectly match the monitor aspect ratio
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
else
{
SDL_Rect dst_rect = {0, 0, 0, 0};
int out_w = 0;
int out_h = 0;
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
if (video_active)
{
log_info("Video splash loaded");
compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect);
decode_next_frame(&video_player);
SDL_RenderClear(renderer);
SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect);
SDL_RenderPresent(renderer);
}
else if (texture)
{
int tex_w = 0;
int tex_h = 0;
log_info("Splash image loaded");
if (SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h) == 0)
{
compute_dest_rect(tex_w, tex_h, out_w, out_h, render_mode, &dst_rect);
}
SDL_RenderClear(renderer);
if (dst_rect.w > 0 && dst_rect.h > 0)
{
SDL_RenderCopy(renderer, texture, NULL, &dst_rect);
}
else
{
SDL_RenderCopy(renderer, texture, NULL, NULL);
}
SDL_RenderPresent(renderer);
}
}
// Fork the process to run the game
log_info("Launching game executable");
pid_t pid = fork();
if (pid == 0) {
if (pid == 0)
{
// Inside Child Process: Hand over execution directly to the game binary
log_info("Execing game: %s", argv[2]);
execvp(argv[2], &argv[2]);
log_error("Failed to launch target game executable '%s': %s", argv[2], strerror(errno));
log_info("Execing game: %s", game_path);
execvp(game_path, &argv[arg_index + 1]);
log_error("Failed to launch target game executable '%s': %s", game_path, strerror(errno));
_exit(1);
} else if (pid < 0) {
}
else if (pid < 0)
{
log_error("Failed to fork process: %s", strerror(errno));
} else {
}
else
{
log_info("Game process started (pid=%d)", pid);
// Inside Parent Process: Manage splash screen lifecycle
int running = 1;
SDL_Event event;
bool hide_scheduled = false;
Uint32 hide_time = 0;
while (running) {
while (running)
{
// Non-blocking check: Has the game quit?
int status;
pid_t result = waitpid(pid, &status, WNOHANG);
if (result > 0) {
if (WIFEXITED(status)) {
if (result > 0)
{
if (WIFEXITED(status))
{
log_info("Game exited (code=%d)", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
}
else if (WIFSIGNALED(status))
{
log_info("Game terminated by signal %d", WTERMSIG(status));
} else {
}
else
{
log_info("Game exited");
}
break; // Game closed, break the loop and close the script
} else if (result < 0) {
}
else if (result < 0)
{
log_error("waitpid failed: %s", strerror(errno));
break; // Process error safetynet
}
if (video_active && SDL_GetTicks() >= video_player.next_frame_tick)
{
if (!decode_next_frame(&video_player))
{
// If decoding fails or loops, try again on next tick
}
video_player.next_frame_tick += (Uint32)video_player.frame_delay_ms;
Uint32 now = SDL_GetTicks();
if ((Sint32)(now - video_player.next_frame_tick) > 100)
{
video_player.next_frame_tick = now;
}
SDL_RenderClear(renderer);
SDL_Rect dst_rect = {0, 0, 0, 0};
int out_w = 0;
int out_h = 0;
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
compute_dest_rect(video_player.width, video_player.height, out_w, out_h, render_mode, &dst_rect);
SDL_RenderCopy(renderer, video_player.texture, NULL, &dst_rect);
SDL_RenderPresent(renderer);
}
// Check desktop server window events
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
while (SDL_PollEvent(&event))
{
if (event.type == SDL_QUIT)
{
log_info("Splash dismissed via window close");
running = 0;
}
if (event.type == SDL_WINDOWEVENT) {
if (event.type == SDL_WINDOWEVENT)
{
// THE MOMENT THE GAME WINDOW STEALS FOCUS:
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
log_info("Splash window hidden (focus lost)");
SDL_HideWindow(window); // Instantly make splash transparent/invisible
if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST && !hide_scheduled)
{
log_info("Splash window hidden (focus lost), scheduling hide");
hide_scheduled = true;
hide_time = SDL_GetTicks() + 250; // 250ms delay to prevent desktop flash
}
// Re-render static image when compositor requests a redraw
if (event.window.event == SDL_WINDOWEVENT_EXPOSED && texture && !video_active)
{
int out_w = 0, out_h = 0;
SDL_GetRendererOutputSize(renderer, &out_w, &out_h);
int tex_w = 0, tex_h = 0;
SDL_QueryTexture(texture, NULL, NULL, &tex_w, &tex_h);
SDL_Rect dst_rect = {0, 0, 0, 0};
compute_dest_rect(tex_w, tex_h, out_w, out_h, render_mode, &dst_rect);
SDL_RenderClear(renderer);
if (dst_rect.w > 0 && dst_rect.h > 0)
SDL_RenderCopy(renderer, texture, NULL, &dst_rect);
else
SDL_RenderCopy(renderer, texture, NULL, NULL);
SDL_RenderPresent(renderer);
}
}
if (event.type == SDL_KEYDOWN) {
if (event.key.keysym.sym == SDLK_ESCAPE) {
if (event.type == SDL_KEYDOWN)
{
if (event.key.keysym.sym == SDLK_ESCAPE)
{
log_info("Splash dismissed via escape key");
running = 0; // Emergency escape key loop override
}
}
}
SDL_Delay(33); // ~30 FPS polling loop to ensure near-zero CPU usage
if (hide_scheduled && SDL_GetTicks() >= hide_time)
{
log_info("Hiding splash window (delayed)");
SDL_HideWindow(window);
hide_scheduled = false;
}
if (video_active)
{
Uint32 now = SDL_GetTicks();
if ((Sint32)(video_player.next_frame_tick - now) > 0)
{
Uint32 delay = video_player.next_frame_tick - now;
SDL_Delay(delay > 33 ? 33 : delay);
}
}
else
{
SDL_Delay(33); // ~30 FPS polling loop to ensure near-zero CPU usage
}
}
}
// Clean up memory space
if (texture) SDL_DestroyTexture(texture);
if (texture)
SDL_DestroyTexture(texture);
if (video_active)
cleanup_video_player(&video_player);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
IMG_Quit();

205
src/video.c Normal file
View File

@@ -0,0 +1,205 @@
#include "video.h"
#include <string.h>
#include <ctype.h>
bool has_video_extension(const char *path)
{
const char *dot = strrchr(path, '.');
if (!dot || dot == path)
{
return false;
}
char ext[8];
size_t i = 0;
dot++;
while (*dot && i < sizeof(ext) - 1)
{
ext[i++] = (char)tolower((unsigned char)*dot++);
}
ext[i] = '\0';
return strcmp(ext, "mp4") == 0 || strcmp(ext, "mkv") == 0 || strcmp(ext, "webm") == 0 ||
strcmp(ext, "avi") == 0 || strcmp(ext, "mov") == 0 || strcmp(ext, "mpg") == 0 ||
strcmp(ext, "mpeg") == 0;
}
bool init_video_player(VideoPlayer *player, SDL_Renderer *renderer, const char *path)
{
memset(player, 0, sizeof(*player));
if (avformat_open_input(&player->format_ctx, path, NULL, NULL) != 0)
{
return false;
}
if (avformat_find_stream_info(player->format_ctx, NULL) < 0)
{
return false;
}
player->stream_index = -1;
for (unsigned int i = 0; i < player->format_ctx->nb_streams; ++i)
{
if (player->format_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
player->stream_index = (int)i;
break;
}
}
if (player->stream_index < 0)
{
return false;
}
AVCodecParameters *codecpar = player->format_ctx->streams[player->stream_index]->codecpar;
const AVCodec *decoder = avcodec_find_decoder(codecpar->codec_id);
if (!decoder)
{
return false;
}
player->codec_ctx = avcodec_alloc_context3(decoder);
if (!player->codec_ctx)
{
return false;
}
if (avcodec_parameters_to_context(player->codec_ctx, codecpar) < 0)
{
return false;
}
if (avcodec_open2(player->codec_ctx, decoder, NULL) < 0)
{
return false;
}
player->width = player->codec_ctx->width;
player->height = player->codec_ctx->height;
player->frame = av_frame_alloc();
player->rgba_frame = av_frame_alloc();
if (!player->frame || !player->rgba_frame)
{
return false;
}
player->rgba_buffer_size = av_image_get_buffer_size(AV_PIX_FMT_RGBA, player->width, player->height, 1);
player->rgba_buffer = (uint8_t *)av_malloc(player->rgba_buffer_size);
if (!player->rgba_buffer)
{
return false;
}
av_image_fill_arrays(player->rgba_frame->data, player->rgba_frame->linesize, player->rgba_buffer,
AV_PIX_FMT_RGBA, player->width, player->height, 1);
player->sws_ctx = sws_getContext(player->width, player->height, player->codec_ctx->pix_fmt,
player->width, player->height, AV_PIX_FMT_RGBA, SWS_BILINEAR,
NULL, NULL, NULL);
if (!player->sws_ctx)
{
return false;
}
player->texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32,
SDL_TEXTUREACCESS_STREAMING, player->width, player->height);
if (!player->texture)
{
return false;
}
AVRational fr = player->format_ctx->streams[player->stream_index]->avg_frame_rate;
if (fr.num > 0 && fr.den > 0)
{
player->frame_delay_ms = (int)(1000.0 * fr.den / fr.num);
}
else
{
player->frame_delay_ms = 33;
}
player->next_frame_tick = SDL_GetTicks();
return true;
}
void cleanup_video_player(VideoPlayer *player)
{
if (player->texture)
{
SDL_DestroyTexture(player->texture);
}
if (player->sws_ctx)
{
sws_freeContext(player->sws_ctx);
}
if (player->rgba_buffer)
{
av_free(player->rgba_buffer);
}
if (player->rgba_frame)
{
av_frame_free(&player->rgba_frame);
}
if (player->frame)
{
av_frame_free(&player->frame);
}
if (player->codec_ctx)
{
avcodec_free_context(&player->codec_ctx);
}
if (player->format_ctx)
{
avformat_close_input(&player->format_ctx);
}
memset(player, 0, sizeof(*player));
}
bool decode_next_frame(VideoPlayer *player)
{
AVPacket packet;
av_init_packet(&packet);
while (av_read_frame(player->format_ctx, &packet) >= 0)
{
if (packet.stream_index != player->stream_index)
{
av_packet_unref(&packet);
continue;
}
if (avcodec_send_packet(player->codec_ctx, &packet) < 0)
{
av_packet_unref(&packet);
continue;
}
av_packet_unref(&packet);
while (true)
{
int ret = avcodec_receive_frame(player->codec_ctx, player->frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
{
break;
}
if (ret < 0)
{
return false;
}
sws_scale(player->sws_ctx, (const uint8_t *const *)player->frame->data,
player->frame->linesize, 0, player->height,
player->rgba_frame->data, player->rgba_frame->linesize);
SDL_UpdateTexture(player->texture, NULL, player->rgba_frame->data[0],
player->rgba_frame->linesize[0]);
return true;
}
}
av_seek_frame(player->format_ctx, player->stream_index, 0, AVSEEK_FLAG_BACKWARD);
avcodec_flush_buffers(player->codec_ctx);
return false;
}

34
src/video.h Normal file
View File

@@ -0,0 +1,34 @@
#ifndef VIDEO_H
#define VIDEO_H
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
typedef struct VideoPlayer
{
AVFormatContext *format_ctx;
AVCodecContext *codec_ctx;
struct SwsContext *sws_ctx;
AVFrame *frame;
AVFrame *rgba_frame;
uint8_t *rgba_buffer;
int rgba_buffer_size;
int stream_index;
int width;
int height;
int frame_delay_ms;
Uint32 next_frame_tick;
SDL_Texture *texture;
} VideoPlayer;
bool has_video_extension(const char *path);
bool init_video_player(VideoPlayer *player, SDL_Renderer *renderer, const char *path);
void cleanup_video_player(VideoPlayer *player);
bool decode_next_frame(VideoPlayer *player);
#endif // VIDEO_H

BIN
tests/assets/sample.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

83
tests/test_cli.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/bin/bash
# A test suite for testing CLI argument permutations of gsplash
GSPLASH="./build/gsplash"
GAME="/bin/true"
IMAGE="nonexistent.png"
if [ ! -f "$GSPLASH" ]; then
echo "Please run 'make' first to build gsplash."
exit 1
fi
export SDL_VIDEODRIVER=dummy
pass=0
fail=0
run_test() {
local expected_status=$1
shift
local expected_log="$1"
shift
local desc="$1"
shift
printf "Test: %-45s " "$desc"
# Run the command and capture output
local output
output=$($GSPLASH "$@" 2>&1)
local status=$?
local passed=true
local error_msg=""
if [ $status -ne $expected_status ]; then
passed=false
error_msg="Expected status $expected_status, got $status"
elif [ -n "$expected_log" ]; then
if ! echo "$output" | grep -q "$expected_log"; then
passed=false
error_msg="Expected log missing: $expected_log"
fi
fi
if $passed; then
echo "PASS"
pass=$((pass+1))
else
echo "FAIL ($error_msg)"
fail=$((fail+1))
fi
}
echo "=== Running CLI Argument Tests ==="
# Valid combinations (0=STRETCH, 1=CENTER, 2=CROP)
run_test 0 "mode=1" "Basic invocation" $IMAGE $GAME
run_test 0 "mode=0" "Long mode: stretch" --mode=stretch $IMAGE $GAME
run_test 0 "mode=1" "Long mode: center" --mode=center $IMAGE $GAME
run_test 0 "mode=2" "Long mode: crop" --mode=crop $IMAGE $GAME
run_test 0 "mode=0" "Long mode: fallback to stretch" --mode=invalid $IMAGE $GAME
run_test 0 "mode=0" "Short mode: stretch" -m stretch $IMAGE $GAME
run_test 0 "mode=1" "Short mode: center" -m center $IMAGE $GAME
run_test 0 "mode=2" "Short mode: crop" -m crop $IMAGE $GAME
run_test 0 "mode=0" "Short mode: fallback to stretch" -m whatever $IMAGE $GAME
run_test 0 "mode=1" "With game arguments" $IMAGE $GAME arg1 arg2
run_test 0 "mode=2" "Mode and game arguments" -m crop $IMAGE $GAME arg1 arg2
# Invalid combinations (Usage errors, so we just check for status 1, no specific log needed)
run_test 1 "" "No arguments"
run_test 1 "" "Only image" $IMAGE
run_test 1 "" "Missing arg for short mode (-m only)" -m $IMAGE
run_test 1 "" "Missing game for short mode" -m stretch $IMAGE
echo "================================================="
echo "Tests completed: $pass passed, $fail failed."
if [ $fail -gt 0 ]; then
exit 1
fi
exit 0

69
tests/test_interactive.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
GSPLASH="./build/gsplash"
DUMMY="./build/dummy_game"
if [ ! -f "$GSPLASH" ] || [ ! -f "$DUMMY" ]; then
echo "Please run 'make' first to build gsplash and dummy_game."
exit 1
fi
if [ -z "$1" ]; then
echo "Usage: $0 <path_to_test_image_or_video>"
exit 1
fi
IMAGE="$1"
if [ ! -f "$IMAGE" ]; then
echo "Error: File '$IMAGE' not found."
exit 1
fi
echo "=== Interactive gsplash Visual Test ==="
echo "This will physically display the splash screen using different modes."
echo "You will be asked to confirm if it looked correct after each test."
echo ""
pass=0
fail=0
run_interactive_test() {
local mode="$1"
echo "------------------------------------------------"
echo "Testing mode: $mode"
echo "Launching in 2 seconds (it will stay open for 3 seconds)..."
sleep 2
# Run gsplash with a 3-second dummy game
$GSPLASH --mode="$mode" "$IMAGE" "$DUMMY" 3
# Prompt the user interactively
while true; do
read -p "Did it display correctly for mode '$mode'? [y/N] " response
case "$response" in
[yY][eE][sS]|[yY])
echo "=> PASS recorded."
pass=$((pass+1))
break
;;
[nN][oO]|[nN]|"")
echo "=> FAIL recorded."
fail=$((fail+1))
break
;;
*)
echo "Please answer y or n."
;;
esac
done
}
run_interactive_test "stretch"
run_interactive_test "center"
run_interactive_test "crop"
echo "================================================"
echo "Interactive Testing Complete!"
echo "Passed: $pass"
echo "Failed: $fail"