Android menus: pictures decoded on a thread of their own

A menu screen's first frame decoded its PNG pictures on the game's thread,
about 15 ms each ("7 menu pictures in 108 ms"): in VR a frame shown late.
Now a picture not yet decoded is asked of a decoding thread and drawn
transparent for the frames that takes; the game's thread only uploads the
decoded texels. Forgotten art's results are dropped by generation.
Decoding them all ahead was not done: 165 pictures, about 350 MB decoded
with their mipmaps.

Checked: the APK builds; desktop and web keep decoding as before (the
thread is the app's only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Stefan MaierandClaude Opus 5.5 committed 2026-10-12 21:58:33 +02:00
1 parent d396eedb61
commit 482a5ecf3a
3 files changed
+221 -8

No files matched your search

+14 -3
View File
@@ -326,14 +326,25 @@ failed:
return NULL;
}
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height)
{
return png_decode(png, size, width, height);
}
unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels)
{
unsigned long width = 0, height = 0, largest;
unsigned long width = 0, height = 0;
unsigned char *pixels = png_decode(png, size, &width, &height);
return pixels ? hud_hires_pixels_texture(pixels, width, height, levels) : 0;
}
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
unsigned long *levels)
{
unsigned long largest;
GLuint texture;
if (!pixels)
return 0;
*levels = 1;
for (largest = width > height ? width : height; largest > 1; largest >>= 1)
(*levels)++;
+5
View File
@@ -65,6 +65,11 @@ unsigned int hud_hires_override_texture(long asset, unsigned long *levels);
/* a GL texture drawn from an 8-bit RGBA PNG (as the tools write them), with
all its mip levels, and their number; 0 if it could not be */
unsigned int hud_hires_png_texture(const void *png, unsigned long size, unsigned long *levels);
/* the same in two steps: the PNG's texels (RGBA, rows top first; any thread
may decode), and a texture of them (which frees them) */
unsigned char *hud_hires_png_pixels(const void *png, unsigned long size, unsigned long *width, unsigned long *height);
unsigned int hud_hires_pixels_texture(unsigned char *pixels, unsigned long width, unsigned long height,
unsigned long *levels);
/* whether its green is its coverage (d3d8_gl.c, nv2a_psh.c: coverage_alpha) */
int hud_hires_override_coverage(long asset);
/* the GL texture of the texture drawn for some of a bitmap's sprites whose
+202 -5
View File
@@ -1029,8 +1029,184 @@ static struct
unsigned int texture;
unsigned long levels;
int failed;
#ifdef HALO_ANDROID
/* being decoded on the pictures' thread (art_decode) */
int decoding;
#endif
} art[MAXIMUM_ART];
static long art_count;
#ifdef HALO_ANDROID
/* The menus' pictures decoded on a thread of their own: a screen's first
frame decoded its PNGs, 15 ms each, 100 ms and more for a screen (in VR a
frame shown late, the picture doubled). A picture not decoded yet is drawn
transparent for the frames it takes; the game's thread only uploads it. The
requests and results carry the art's generation: forgotten art's results
are dropped. */
#include <pthread.h>
#define ART_DECODE_QUEUE 64
static struct
{
int started;
pthread_mutex_t lock;
pthread_cond_t wake;
/* asked (the art's index, its PNG and generation), and decoded */
struct
{
long index;
unsigned long generation;
const unsigned char *png;
unsigned long size;
unsigned char *pixels;
unsigned long width, height;
int state; /* 0 free, 1 asked, 2 decoding, 3 decoded */
} slots[ART_DECODE_QUEUE];
unsigned long generation;
/* drawn while a picture is decoded */
unsigned int transparent;
} art_decode;
static void *art_decode_main(void *unused)
{
(void)unused;
pthread_mutex_lock(&art_decode.lock);
for (;;)
{
int slot;
for (slot = 0; slot < ART_DECODE_QUEUE && art_decode.slots[slot].state != 1; slot++)
{
}
if (slot == ART_DECODE_QUEUE)
{
pthread_cond_wait(&art_decode.wake, &art_decode.lock);
continue;
}
art_decode.slots[slot].state = 2;
pthread_mutex_unlock(&art_decode.lock);
{
unsigned long width = 0, height = 0;
unsigned char *pixels = hud_hires_png_pixels(art_decode.slots[slot].png, art_decode.slots[slot].size,
&width, &height);
pthread_mutex_lock(&art_decode.lock);
art_decode.slots[slot].pixels = pixels;
art_decode.slots[slot].width = width;
art_decode.slots[slot].height = height;
art_decode.slots[slot].state = 3;
}
}
return NULL;
}
/* the art's texture if its picture is decoded (uploaded now), else asked
for and 0 (*waiting: draw it transparent for now) */
static unsigned int art_decoded_texture(long index, int *waiting)
{
unsigned int texture = 0;
int slot, free_slot = -1;
*waiting = 0;
if (!art_decode.started)
{
pthread_t thread;
art_decode.started = -1;
pthread_mutex_init(&art_decode.lock, NULL);
pthread_cond_init(&art_decode.wake, NULL);
if (pthread_create(&thread, NULL, art_decode_main, NULL) == 0)
{
pthread_detach(thread);
art_decode.started = 1;
}
}
if (art_decode.started < 0)
return 0;
pthread_mutex_lock(&art_decode.lock);
for (slot = 0; slot < ART_DECODE_QUEUE; slot++)
{
if (art_decode.slots[slot].state == 3 && art_decode.slots[slot].generation != art_decode.generation)
{
/* (forgotten art's) */
free(art_decode.slots[slot].pixels);
art_decode.slots[slot].pixels = NULL;
art_decode.slots[slot].state = 0;
}
if (art_decode.slots[slot].state == 0 && free_slot < 0)
free_slot = slot;
if (art_decode.slots[slot].state && art_decode.slots[slot].index == index &&
art_decode.slots[slot].generation == art_decode.generation)
{
break;
}
}
if (slot < ART_DECODE_QUEUE)
{
if (art_decode.slots[slot].state == 3)
{
unsigned char *pixels = art_decode.slots[slot].pixels;
unsigned long width = art_decode.slots[slot].width, height = art_decode.slots[slot].height;
art_decode.slots[slot].pixels = NULL;
art_decode.slots[slot].state = 0;
pthread_mutex_unlock(&art_decode.lock);
art[index].decoding = 0;
if (!pixels)
return 0;
{
unsigned long long start = xgpu_shader_work_now_us();
texture = hud_hires_pixels_texture(pixels, width, height, &art[index].levels);
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, width * height, start);
}
return texture;
}
pthread_mutex_unlock(&art_decode.lock);
*waiting = 1;
return 0;
}
if (free_slot >= 0 && !art[index].decoding)
{
unsigned long size = 0;
const unsigned char *png;
pthread_mutex_unlock(&art_decode.lock);
png = file_data(art[index].png, &size);
if (!png)
return 0;
pthread_mutex_lock(&art_decode.lock);
art_decode.slots[free_slot].index = index;
art_decode.slots[free_slot].generation = art_decode.generation;
art_decode.slots[free_slot].png = png;
art_decode.slots[free_slot].size = size;
art_decode.slots[free_slot].state = 1;
art[index].decoding = 1;
pthread_cond_signal(&art_decode.wake);
}
pthread_mutex_unlock(&art_decode.lock);
/* (the queue full: asked again at its next draw) */
*waiting = 1;
return 0;
}
static unsigned int art_transparent_texture(void)
{
if (!art_decode.transparent)
{
static const unsigned char clear[4] = { 0, 0, 0, 0 };
glGenTextures(1, &art_decode.transparent);
glBindTexture(GL_TEXTURE_2D, art_decode.transparent);
xgpu_gl_state_invalidate();
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, clear);
xgpu_gl_state_invalidate();
}
return art_decode.transparent;
}
#endif
/* the lowest and highest texture the art stands for: every texture the
renderer binds asks (xbox_textures.c), every frame of a game too, and the
menus' lie together, made as the map loaded, mostly apart from the map's
@@ -1093,6 +1269,14 @@ void halo_menus_art_forget(void)
}
art_count = 0;
art_lowest = art_highest = 0;
#ifdef HALO_ANDROID
if (art_decode.started > 0)
{
pthread_mutex_lock(&art_decode.lock);
art_decode.generation++;
pthread_mutex_unlock(&art_decode.lock);
}
#endif
}
unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
@@ -1111,12 +1295,25 @@ unsigned int menu_art_texture(unsigned long data, unsigned long *levels)
return 0;
if (!art[index].texture)
{
unsigned long size = 0;
unsigned long long start = xgpu_shader_work_now_us();
const unsigned char *png = file_data(art[index].png, &size);
#ifdef HALO_ANDROID
int waiting = 0;
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, 0, start);
art[index].texture = art_decoded_texture(index, &waiting);
if (waiting)
{
*levels = 1;
return art_transparent_texture();
}
if (!art[index].texture && art_decode.started < 0)
#endif
{
unsigned long size = 0;
unsigned long long start = xgpu_shader_work_now_us();
const unsigned char *png = file_data(art[index].png, &size);
art[index].texture = png ? hud_hires_png_texture(png, size, &art[index].levels) : 0;
xgpu_texture_work_add(XGPU_TEXTURE_WORK_PICTURE, 0, start);
}
if (!art[index].texture)
{
platform_log("menus: could not draw %s", art[index].png);