Shaders: a deferred load compiles its passes on the task thread

One task per load compiles the preset's passes ahead, one per call,
detached from the chain's thread: it reads, compiles and writes the
cache as that thread would, but asks and tells the blob table nothing,
prints no text and falls back to no other compiler. Each pass's stages
land in a slot; the chain's thread offers the ready ones to the table
before its step, so the step's own compile takes them by reference,
and the load's tick holds a step back while its pass is still with the
worker. Only with a threaded task queue, and only where rslc is the
compiler; what a chain never took is released unlisted by the last
holder. shader_chain_test covers the detached compile and the offer.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-24 00:30:03 +02:00
1 parent 8b6754f929
commit f2d338b9e7
7 files changed
+426 -11

No files matched your search

+110 -11
View File
@@ -1323,7 +1323,7 @@ static void slang_output_print_text(glslang_output *output,
static bool slang_compile_stage(const char *source,
enum glslang_compile_stage stage,
struct slang_blob **blob, const char **compiler,
const struct slang_text_target *text)
const struct slang_text_target *text, bool detached)
{
#if defined(HAVE_GLSLANG)
uint32_t *words = NULL;
@@ -1342,12 +1342,19 @@ static bool slang_compile_stage(const char *source,
*compiler = "rslc";
return true;
}
/* glslang is one process-wide instance: a detached compile leaves
* the stage to the chain's own thread rather than enter it beside
* that thread */
if (detached)
return false;
#if defined(HAVE_GLSLANG)
RARCH_WARN("[Slang] rslc rejected the stage; compiling it with glslang.\n");
#endif
}
#endif
#if defined(HAVE_GLSLANG)
if (detached)
return false;
*compiler = "glslang";
if (!glslang_compile_spirv(source, stage, &words, &nwords))
return false;
@@ -1401,9 +1408,15 @@ bool glslang_compile_shader_cached(const char *shader_path,
return glslang_compile_shader_text(shader_path, output, include_cache, NULL);
}
bool glslang_compile_shader_text(const char *shader_path,
/* The compile of one pass. Detached, it runs on a thread that is not
* the chain's: it reads and compiles as the chain's thread would and
* writes the cache, but neither asks the blob table nor lists in it,
* prints no text, falls back to no other compiler and tells the user
* nothing; @key_out (33 bytes) gets the pass's cache key, under which
* the chain's thread offers the stages to the table (slang_blob_offer). */
static bool slang_compile_pass(const char *shader_path,
glslang_output *output, void *include_cache,
const struct slang_text_target *text)
const struct slang_text_target *text, bool detached, char *key_out)
{
glslang_output_init(output);
@@ -1454,10 +1467,15 @@ bool glslang_compile_shader_text(const char *shader_path,
fallback_filename);
#endif
if (key_out)
strlcpy(key_out, cache_filename, 33);
/* A pass a live chain already compiled: its blobs, by reference */
listed = slang_output_take_listed(output, cache_filename);
if (!detached)
listed = slang_output_take_listed(output, cache_filename);
#if defined(HAVE_RSLC) && defined(HAVE_GLSLANG)
if (!listed && slang_output_take_listed(output, fallback_filename))
if (!detached && !listed
&& slang_output_take_listed(output, fallback_filename))
{
listed = true;
used = "glslang";
@@ -1502,7 +1520,8 @@ bool glslang_compile_shader_text(const char *shader_path,
slang_output_print_text(output, vertex_source, fragment_source,
text);
#endif
slang_output_list(output, cache_filename);
if (!detached)
slang_output_list(output, cache_filename);
RARCH_LOG("[Slang] Loaded shader from cache: \"%s\".\n", shader_path);
free(vertex_source);
free(fragment_source);
@@ -1515,7 +1534,7 @@ bool glslang_compile_shader_text(const char *shader_path,
goto error;
if (!slang_compile_stage(vertex_source,
GLSLANG_COMPILE_STAGE_VERTEX, &blob, &used, text))
GLSLANG_COMPILE_STAGE_VERTEX, &blob, &used, text, detached))
{
RARCH_ERR("[Slang] Failed to compile vertex shader stage.\n");
goto error;
@@ -1533,7 +1552,7 @@ bool glslang_compile_shader_text(const char *shader_path,
vertex_source = NULL;
if (!slang_compile_stage(fragment_source,
GLSLANG_COMPILE_STAGE_FRAGMENT, &blob, &used, text))
GLSLANG_COMPILE_STAGE_FRAGMENT, &blob, &used, text, detached))
{
RARCH_ERR("[Slang] Failed to compile fragment shader stage.\n");
goto error;
@@ -1558,12 +1577,14 @@ bool glslang_compile_shader_text(const char *shader_path,
#if defined(HAVE_RSLC) && defined(HAVE_GLSLANG)
spirv_cache_save(used != compiler ? fallback_filename : cache_filename,
output);
slang_output_list(output,
used != compiler ? fallback_filename : cache_filename);
if (!detached)
slang_output_list(output,
used != compiler ? fallback_filename : cache_filename);
#else
(void)used;
spirv_cache_save(cache_filename, output);
slang_output_list(output, cache_filename);
if (!detached)
slang_output_list(output, cache_filename);
#endif
free(vertex_source);
@@ -1580,6 +1601,7 @@ error:
}
#endif
if (!detached)
{
size_t _len;
char msg[NAME_MAX_LENGTH];
@@ -1594,6 +1616,83 @@ error:
return false;
}
bool glslang_compile_shader_text(const char *shader_path,
glslang_output *output, void *include_cache,
const struct slang_text_target *text)
{
return slang_compile_pass(shader_path, output, include_cache, text,
false, NULL);
}
bool slang_compile_pass_detached(const char *shader_path, bool hlsl_or_msl,
glslang_output *output, char *key)
{
#if defined(HAVE_RSLC)
#if defined(HAVE_GLSLANG)
struct slang_text_target text;
memset(&text, 0, sizeof(text));
text.hlsl = hlsl_or_msl;
/* under glslang the chain's thread compiles, as before */
if (!slang_prefer_rslc(hlsl_or_msl ? &text : NULL))
return false;
#else
(void)hlsl_or_msl;
#endif
return slang_compile_pass(shader_path, output, NULL, NULL, true, key);
#else
(void)shader_path;
(void)hlsl_or_msl;
(void)output;
(void)key;
return false;
#endif
}
/* Stages compiled detached, held here with one reference each so they
* stay listed until the chain that asked for them takes them; the
* oldest goes when the ring is full. */
#define SLANG_OFFERS 64
static struct
{
struct slang_blob *vs[SLANG_OFFERS];
struct slang_blob *fs[SLANG_OFFERS];
unsigned next;
} slang_offers;
void slang_blob_offer(const char *key, struct slang_blob *vs,
struct slang_blob *fs)
{
struct slang_blob *listed = slang_blob_table_find(key, false);
unsigned i;
if (listed || !vs || !fs)
{
/* already there, or half a pass: nothing to list */
slang_blob_unref(listed);
slang_blob_unref(vs);
slang_blob_unref(fs);
return;
}
slang_blob_table_add(vs, key, false);
slang_blob_table_add(fs, key, true);
i = slang_offers.next++ % SLANG_OFFERS;
slang_blob_unref(slang_offers.vs[i]);
slang_blob_unref(slang_offers.fs[i]);
slang_offers.vs[i] = vs;
slang_offers.fs[i] = fs;
}
void slang_blob_offers_release(void)
{
unsigned i;
for (i = 0; i < SLANG_OFFERS; i++)
{
slang_blob_unref(slang_offers.vs[i]);
slang_blob_unref(slang_offers.fs[i]);
slang_offers.vs[i] = NULL;
slang_offers.fs[i] = NULL;
}
}
bool slang_preprocess_parse_parameters_meta(const glslang_meta *meta,
struct video_shader *shader, unsigned pass)
{
+22
View File
@@ -414,6 +414,28 @@ bool glslang_compile_shader_text(const char *shader_path,
glslang_output *output, void *include_cache,
const struct slang_text_target *text);
/* The compile of one pass on a thread that is not the chain's: it
* reads, compiles and writes the cache as the chain's thread would,
* but asks and tells the blob table nothing, prints no text and falls
* back to no other compiler. False, with nothing done, when the
* chain's compiler would be glslang (@hlsl_or_msl: the driver takes
* HLSL or MSL text, which decides that under auto). @key (33 bytes) gets the pass's
* cache key. The chain's thread then offers the output's blobs under
* it (slang_blob_offer) and its own compile of the pass takes them. */
bool slang_compile_pass_detached(const char *shader_path, bool hlsl_or_msl,
glslang_output *output, char *key);
/* Lists @vs and @fs under @key for the passes to come, holding one
* reference to each until the chain has taken them or the offers ring
* (64 passes) has moved on; takes the references passed. Nothing is
* listed, and both are released, when the key is listed already. On
* the thread that builds chains. */
void slang_blob_offer(const char *key, struct slang_blob *vs,
struct slang_blob *fs);
/* Drops the references the offers ring holds. */
void slang_blob_offers_release(void);
/* Merge parameters harvested into @meta into @shader, enforcing the
* duplicate-must-match rule. @pass is the pass @meta was harvested
* from, and is recorded on each parameter this call adds, so that a
+241
View File
@@ -167,6 +167,11 @@ static void video_driver_read_vp_params(struct video_vp_param_snap *ps);
#include "../configuration.h"
#include "video_shader_parse.h"
#include <compat/strl.h>
#include <queues/task_queue.h>
#ifdef HAVE_SLANG
#include "drivers_shader/slang_process.h"
#include "drivers_shader/slang_blob.h"
#endif
#ifdef __MACH__
#include <TargetConditionals.h>
#endif
@@ -933,6 +938,214 @@ video_driver_state_t *video_state_get_ptr(void)
* load is in progress, compiles one pass per frame and
* handles completion or failure.
**/
#ifdef HAVE_SLANG
/* The passes of a deferred load compiled ahead on the task thread,
* while the chain's thread builds them one per frame. A worker reads
* and compiles a pass (slang_compile_pass_detached) and publishes its
* stages in that pass's slot; the chain's thread lists them in the blob
* table (slang_blob_offer) before its step, so the step's own compile
* of the pass takes them and does no work. The load's tick holds a
* step back while its pass is still with the worker. The task and the
* load each hold a reference; the slots a chain never took are released
* by the last, unlisted, so on either thread. */
enum shader_precompile_state
{
SHADER_PRECOMPILE_PENDING = 0,
SHADER_PRECOMPILE_READY,
SHADER_PRECOMPILE_FAILED
};
struct shader_precompile_pass
{
struct slang_blob *vs;
struct slang_blob *fs;
retro_atomic_int_t state;
char key[33];
bool taken;
};
struct shader_precompile
{
struct shader_precompile_pass *pass;
char **path;
unsigned n;
unsigned next; /* the worker's cursor */
retro_atomic_int_t refs;
retro_atomic_int_t done; /* the task is through */
retro_atomic_int_t stop; /* the load ended */
bool hlsl_or_msl;
};
static void shader_precompile_unref(struct shader_precompile *p)
{
unsigned i;
if (!p || retro_atomic_fetch_add_int(&p->refs, -1) != 1)
return;
for (i = 0; i < p->n; i++)
{
slang_blob_unref(p->pass[i].vs);
slang_blob_unref(p->pass[i].fs);
free(p->path[i]);
}
free(p->pass);
free(p->path);
free(p);
}
static void shader_precompile_handler(retro_task_t *task)
{
struct shader_precompile *p = (struct shader_precompile*)task->state;
struct shader_precompile_pass *slot;
glslang_output output;
unsigned i;
if ( retro_atomic_load_acquire_int(&p->stop)
|| (task_get_flags(task) & RETRO_TASK_FLG_CANCELLED)
|| p->next >= p->n)
{
task_set_flags(task, RETRO_TASK_FLG_FINISHED, true);
return;
}
/* one pass per call, so the queue's other work gets its turn */
i = p->next++;
slot = &p->pass[i];
if (slang_compile_pass_detached(p->path[i], p->hlsl_or_msl, &output,
slot->key))
{
slot->vs = slang_blob_ref(output.vertex_blob);
slot->fs = slang_blob_ref(output.fragment_blob);
glslang_output_free(&output);
retro_atomic_store_release_int(&slot->state, SHADER_PRECOMPILE_READY);
}
else
retro_atomic_store_release_int(&slot->state, SHADER_PRECOMPILE_FAILED);
}
static void shader_precompile_cleanup(retro_task_t *task)
{
struct shader_precompile *p = (struct shader_precompile*)task->state;
retro_atomic_store_release_int(&p->done, 1);
shader_precompile_unref(p);
}
/* Starts the task for @preset_path's passes; NULL when the work stays
* with the chain's thread (no task thread, or nothing to read). */
static struct shader_precompile *shader_precompile_start(
const char *preset_path)
{
struct video_shader *shader;
struct shader_precompile *p = NULL;
retro_task_t *task;
const char *ident;
unsigned i;
if (!task_queue_is_threaded())
return NULL;
if (!(shader = (struct video_shader*)calloc(1, sizeof(*shader))))
return NULL;
if ( !video_shader_load_preset_into_shader(preset_path, shader)
|| !shader->passes
|| !(p = (struct shader_precompile*)calloc(1, sizeof(*p))))
goto end;
p->pass = (struct shader_precompile_pass*)calloc(shader->passes,
sizeof(*p->pass));
p->path = (char**)calloc(shader->passes, sizeof(*p->path));
if (!p->pass || !p->path)
goto fail;
for (i = 0; i < shader->passes; i++)
{
if (!(p->path[i] = strdup(shader->pass[i].source.path)))
goto fail;
p->n++;
}
ident = video_driver_get_ident();
p->hlsl_or_msl = ident && ( string_starts_with(ident, "d3d1")
|| string_is_equal(ident, "metal"));
retro_atomic_int_init(&p->refs, 2);
retro_atomic_int_init(&p->done, 0);
retro_atomic_int_init(&p->stop, 0);
for (i = 0; i < p->n; i++)
retro_atomic_int_init(&p->pass[i].state, SHADER_PRECOMPILE_PENDING);
if (!(task = task_init()))
goto fail;
task->handler = shader_precompile_handler;
task->cleanup = shader_precompile_cleanup;
task->state = p;
task->flags |= RETRO_TASK_FLG_MUTE;
if (!task_queue_push(task))
{
/* the queue freed the task; nothing of it runs */
retro_atomic_int_init(&p->refs, 1);
goto fail;
}
goto end;
fail:
if (p)
{
retro_atomic_int_init(&p->refs, 1);
shader_precompile_unref(p);
p = NULL;
}
end:
free(shader);
return p;
}
/* The load's side letting go: the task finishes on its next call. */
static void shader_precompile_stop(shader_load_deferred_t *d)
{
struct shader_precompile *p = (struct shader_precompile*)d->precompile;
if (!p)
return;
retro_atomic_store_release_int(&p->stop, 1);
shader_precompile_unref(p);
d->precompile = NULL;
}
/* Whether the step for @d's next pass should wait for the worker. */
static bool shader_precompile_pending(const shader_load_deferred_t *d)
{
struct shader_precompile *p = (struct shader_precompile*)d->precompile;
if (!p || d->current_pass >= p->n)
return false;
if (retro_atomic_load_acquire_int(&p->pass[d->current_pass].state)
!= SHADER_PRECOMPILE_PENDING)
return false;
/* a task that is through left the pass to the chain's thread */
return !retro_atomic_load_acquire_int(&p->done);
}
#endif
void video_driver_shader_deferred_take(shader_load_deferred_t *d, bool end)
{
#ifdef HAVE_SLANG
struct shader_precompile *p = (struct shader_precompile*)d->precompile;
unsigned i;
if (p)
{
for (i = 0; i < p->n; i++)
{
struct shader_precompile_pass *slot = &p->pass[i];
if (slot->taken || retro_atomic_load_acquire_int(&slot->state)
!= SHADER_PRECOMPILE_READY)
continue;
slang_blob_offer(slot->key, slot->vs, slot->fs);
slot->vs = NULL;
slot->fs = NULL;
slot->taken = true;
}
}
if (end)
slang_blob_offers_release();
#else
(void)d;
(void)end;
#endif
}
bool video_shader_deferred_notify(retro_task_callback_t cb, void *user_data)
{
shader_load_deferred_t *d = &video_driver_st.shader_deferred;
@@ -965,12 +1178,26 @@ void video_driver_shader_deferred_cancel(const char *why)
}
vid->shader_load_step(video_st->data, d);
retro_atomic_store_relaxed_int(&d->cancel, 0);
if (!video_st->threaded)
video_driver_shader_deferred_take(d, true);
}
#ifdef HAVE_SLANG
shader_precompile_stop(d);
#endif
d->state = SHADER_LOAD_IDLE;
d->driver_data = NULL;
task_notify_fire(&d->done, d->preset_path, why);
}
void video_driver_shader_deferred_start(shader_load_deferred_t *d)
{
#ifdef HAVE_SLANG
d->precompile = shader_precompile_start(d->preset_path);
#else
(void)d;
#endif
}
void video_driver_shader_deferred_tick(void)
{
video_driver_state_t *video_st = &video_driver_st;
@@ -997,6 +1224,15 @@ void video_driver_shader_deferred_tick(void)
return;
}
#ifdef HAVE_SLANG
/* the worker has the next pass: the step would only compile it
* itself, so it waits a frame */
if (d->state == SHADER_LOAD_COMPILING && shader_precompile_pending(d))
return;
#endif
if (!video_st->threaded)
video_driver_shader_deferred_take(d, false);
if (d->state == SHADER_LOAD_COMPILING
&& video_st->current_video->shader_load_step(video_st->data, d))
{
@@ -1016,6 +1252,11 @@ void video_driver_shader_deferred_tick(void)
{
/* no more work: the step left d->state DONE or FAILED */
if (!video_st->threaded)
video_driver_shader_deferred_take(d, true);
#ifdef HAVE_SLANG
shader_precompile_stop(d);
#endif
if (d->state == SHADER_LOAD_DONE)
{
settings_t *settings = config_get_ptr();
+10
View File
@@ -1455,6 +1455,7 @@ typedef struct shader_load_deferred
unsigned total_passes;
void *driver_data; /* driver-specific work state */
task_notify_t done; /* told once the load ends */
void *precompile; /* the passes compiling ahead */
retro_atomic_int_t busy; /* a step runs on the video thread */
retro_atomic_int_t cancel; /* the next step ends the load */
} shader_load_deferred_t;
@@ -2302,6 +2303,15 @@ bool video_shader_deferred_notify(retro_task_callback_t cb, void *user_data);
* on screen stays, and a caller waiting on the load is told @why. */
void video_driver_shader_deferred_cancel(const char *why);
/* Once the driver has begun a deferred load: its passes start
* compiling ahead on the task thread, when there is one. */
void video_driver_shader_deferred_start(shader_load_deferred_t *d);
/* On the thread that builds chains, before a step: the passes the
* worker has finished are listed for the step to take; with @end, the
* load is over and what was listed for it is let go. */
void video_driver_shader_deferred_take(shader_load_deferred_t *d, bool end);
bool video_driver_set_rotation(unsigned rotation);
/* The size, packed with VIDEO_SCALE_PACK, the video driver would ask
+3
View File
@@ -4157,10 +4157,13 @@ bool video_shader_apply_shader(
d->current_pass = 0;
strlcpy(d->preset_path, preset_path, sizeof(d->preset_path));
if (!video_st->threaded)
video_driver_shader_deferred_take(d, true);
if (video_st->current_video->shader_load_begin(
video_st->data, d))
{
d->state = SHADER_LOAD_COMPILING;
video_driver_shader_deferred_start(d);
/* Store the runtime preset path early so hotkey
* cycling and auto-preset logic see it immediately */
+4
View File
@@ -1223,6 +1223,7 @@ static bool video_thread_handle_packet(
case CMD_SHADER_LOAD_BEGIN:
{
shader_load_deferred_t *d = (shader_load_deferred_t*)pkt.data.v;
video_driver_shader_deferred_take(d, true);
pkt.data.b = thr->driver_data && thr->driver
&& thr->driver->shader_load_begin
&& thr->driver->shader_load_begin(thr->driver_data, d);
@@ -1240,6 +1241,7 @@ static bool video_thread_handle_packet(
d->current_pass = d->total_passes;
d->state = SHADER_LOAD_FAILED;
}
video_driver_shader_deferred_take(d, false);
if (thr->driver_data && thr->driver && thr->driver->shader_load_step)
pkt.data.b = thr->driver->shader_load_step(thr->driver_data, d);
else
@@ -1247,6 +1249,8 @@ static bool video_thread_handle_packet(
d->state = SHADER_LOAD_FAILED;
pkt.data.b = false;
}
if (!pkt.data.b)
video_driver_shader_deferred_take(d, true);
/* publishes the step's writes to the main thread's tick */
retro_atomic_store_release_int(&d->busy, 0);
}
@@ -260,6 +260,42 @@ int main(void)
slang_blob_table_stats(&n, &reused);
CHECK(n == 0, "no chain, no blobs");
/* a pass compiled detached - on a thread that is not the chain's -
* lists nothing until the chain's thread offers it, and the offer
* is what the chain's own compile then takes */
printf("detached a, offer, load a\n");
{
glslang_output out;
struct slang_blob *vs, *fs;
char key[33];
CHECK(slang_compile_pass_detached("a.slang", false, &out, key), "detached compile");
slang_blob_table_stats(&n, &reused);
CHECK(n == 0, "a detached compile lists nothing");
CHECK(strlen(key) == 32, "the detached compile names the pass's key");
vs = slang_blob_ref(out.vertex_blob);
fs = slang_blob_ref(out.fragment_blob);
glslang_output_free(&out);
slang_blob_offer(key, vs, fs);
slang_blob_table_stats(&n, &reused);
CHECK(n == 2, "the offer lists both stages");
CHECK(chain_build(&live, abac, 1) == 0, "the chain's compile takes the offered stages");
CHECK(live.pass[0].vertex_blob == vs && live.pass[0].fragment_blob == fs,
"by reference");
/* a second offer of a listed pass is dropped, references and all */
CHECK(slang_compile_pass_detached("a.slang", false, &out, key), "detached compile again");
vs = slang_blob_ref(out.vertex_blob);
glslang_output_free(&out);
slang_blob_offer(key, vs, NULL);
slang_blob_table_stats(&n, &reused);
CHECK(n == 2, "an offer of a listed pass lists nothing");
chain_free(&live);
slang_blob_table_stats(&n, &reused);
CHECK(n == 2, "the offer holds the stages after the chain let go");
slang_blob_offers_release();
slang_blob_table_stats(&n, &reused);
CHECK(n == 0, "released offers leave the table");
}
printf("%s: %u failure(s)\n", failures ? "FAIL" : "ok", failures);
return failures ? 1 : 0;
}