/******************************************************************************
*
* Copyright (C) 2026  Brian Berlin
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
*
******************************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>

#include "logger.h"
#include "shader_gpu.h"
#include "util.h"

#define LOG_SRC "SHADER_GPU"

static const t_shader_bundle_stage * shader_gpu_find_stage (
        const t_shader_bundle_pass *pass, const char *stage_name ) {

    if ( !pass || !stage_name )
        return NULL;

    for ( int i = 0; i < pass->num_stages; ++i ) {
        if ( pass->stages[i].stage && !strcmp(pass->stages[i].stage,
                    stage_name) )
            return &pass->stages[i];
    }

    return NULL;

} /* end shader_gpu_find_stage */

static int shader_gpu_param_stage_matches (
        const t_shader_bundle_param *param, const char *stage_name ) {

    if ( !param || !stage_name || !param->stage )
        return 0;

    return !strcmp(param->stage, stage_name) || !strcmp(param->stage, "both");

} /* end shader_gpu_param_stage_matches */

static int shader_gpu_stage_count_path ( const t_shader_bundle_stage *stage,
        SDL_GPUShaderFormat format ) {

    const char *path = shader_gpu_stage_blob_path ( stage, format );

    return path && strlen(path);

} /* end shader_gpu_stage_count_path */

static int shader_gpu_bundle_supports_format ( const t_shader_bundle *bundle,
        SDL_GPUShaderFormat format ) {

    if ( !bundle )
        return 0;

    for ( int i = 0; i < bundle->num_passes; ++i ) {
        const t_shader_bundle_pass *pass = &bundle->passes[i];
        for ( int j = 0; j < pass->num_stages; ++j ) {
            if ( !shader_gpu_stage_count_path(&pass->stages[j], format) )
                return 0;
        }
    }

    return 1;

} /* end shader_gpu_bundle_supports_format */

static const char * shader_gpu_entrypoint ( const t_shader_bundle_stage *stage,
        SDL_GPUShaderFormat format ) {

    if ( format == SDL_GPU_SHADERFORMAT_MSL )
        return "main0";

    if ( stage && stage->entrypoint && strlen(stage->entrypoint) )
        return stage->entrypoint;

    return "main";

} /* end shader_gpu_entrypoint */

static SDL_GPUShaderStage shader_gpu_sdl_stage ( const char *stage_name ) {

    if ( stage_name && !strcmp(stage_name, "fragment") )
        return SDL_GPU_SHADERSTAGE_FRAGMENT;

    return SDL_GPU_SHADERSTAGE_VERTEX;

} /* end shader_gpu_sdl_stage */

static void shader_gpu_log_error ( char *error_buffer, size_t error_size,
        const char *message, const char *detail ) {

    char log_message[768];

    if ( !message )
        message = "SDL GPU shader pipeline error";
    if ( !detail )
        detail = "";

    if ( error_buffer && error_size )
        snprintf ( error_buffer, error_size, "%s%s%s",
                message, strlen(detail) ? ": " : "", detail );

    snprintf ( log_message, sizeof(log_message), "%s%s%s",
            message, strlen(detail) ? ": " : "", detail );
    logger_log_message ( LOG_ERROR, log_message, LOG_SRC );

} /* end shader_gpu_log_error */

static void shader_gpu_set_error ( t_shader_gpu_pipeline *pipeline,
        const char *message, const char *detail ) {

    shader_gpu_log_error ( pipeline ? pipeline->error : NULL,
            pipeline ? sizeof(pipeline->error) : 0, message, detail );

} /* end shader_gpu_set_error */

static void shader_gpu_set_resource_error ( t_shader_gpu_resources *resources,
        const char *message, const char *detail ) {

    shader_gpu_log_error ( resources ? resources->error : NULL,
            resources ? sizeof(resources->error) : 0, message, detail );

} /* end shader_gpu_set_resource_error */

static int shader_gpu_full_path ( const t_shader_bundle *bundle,
        const t_shader_bundle_stage *stage, SDL_GPUShaderFormat format,
        char *path, size_t path_size ) {

    const char *relative_path;

    if ( !bundle || !bundle->dir || !stage || !path || !path_size )
        return -1;

    relative_path = shader_gpu_stage_blob_path ( stage, format );
    if ( !relative_path || !strlen(relative_path) )
        return -1;

    if ( snprintf(path, path_size, "%s/%s", bundle->dir, relative_path) >=
            (int) path_size )
        return -1;

    return 0;

} /* end shader_gpu_full_path */

static SDL_GPUShader * shader_gpu_create_shader ( SDL_GPUDevice *device,
        const t_shader_bundle *bundle, const t_shader_bundle_stage *stage,
        SDL_GPUShaderFormat format, t_shader_gpu_pipeline *pipeline ) {

    char path[KAT_PATH_MAX];
    char error[1024];
    size_t code_size = 0;
    void *code;
    SDL_GPUShader *shader;
    SDL_GPUShaderCreateInfo create_info;

    if ( shader_gpu_full_path(bundle, stage, format, path, sizeof(path)) ) {
        shader_gpu_set_error ( pipeline, "Missing shader blob path",
                stage && stage->stage ? stage->stage : "unknown stage" );
        return NULL;
    }

    code = SDL_LoadFile ( path, &code_size );
    if ( !code ) {
        snprintf ( error, sizeof(error), "%s (%s)", path, SDL_GetError() );
        shader_gpu_set_error ( pipeline, "Failed to load shader blob", error );
        return NULL;
    }

    SDL_zero(create_info);
    create_info.code = code;
    create_info.code_size = code_size;
    create_info.entrypoint = shader_gpu_entrypoint ( stage, format );
    create_info.format = format;
    create_info.stage = shader_gpu_sdl_stage ( stage->stage );
    create_info.num_samplers = (Uint32) stage->samplers;
    create_info.num_storage_textures = (Uint32) stage->storage_textures;
    create_info.num_storage_buffers = (Uint32) stage->storage_buffers;

    create_info.num_uniform_buffers = (Uint32) stage->uniform_buffers;

    shader = SDL_CreateGPUShader ( device, &create_info );
    SDL_free ( code );

    if ( !shader ) {
        snprintf ( error, sizeof(error), "%s (%s)", path, SDL_GetError() );
        shader_gpu_set_error ( pipeline, "Failed to create SDL GPU shader",
                error );
        return NULL;
    }

    return shader;

} /* end shader_gpu_create_shader */

static SDL_GPUGraphicsPipeline * shader_gpu_create_pass_pipeline (
        SDL_GPUDevice *device, const t_shader_bundle *bundle,
        const t_shader_bundle_pass *pass, SDL_GPUShaderFormat format,
        SDL_GPUTextureFormat color_format, t_shader_gpu_pipeline *pipeline,
        t_shader_gpu_pass *runtime_pass ) {

    const t_shader_bundle_stage *vertex_stage;
    const t_shader_bundle_stage *fragment_stage;
    SDL_GPUShader *vertex_shader = NULL;
    SDL_GPUShader *fragment_shader = NULL;
    SDL_GPUGraphicsPipeline *graphics_pipeline = NULL;
    SDL_GPUVertexBufferDescription vertex_buffer;
    SDL_GPUVertexAttribute vertex_attributes[2];
    SDL_GPUColorTargetDescription color_target;
    SDL_GPUGraphicsPipelineCreateInfo create_info;

    vertex_stage = shader_gpu_find_stage ( pass, "vertex" );
    fragment_stage = shader_gpu_find_stage ( pass, "fragment" );
    if ( !vertex_stage || !fragment_stage ) {
        shader_gpu_set_error ( pipeline,
                "Shader bundle pass is missing vertex or fragment stage",
                bundle && bundle->id ? bundle->id : "" );
        return NULL;
    }

    vertex_shader = shader_gpu_create_shader ( device, bundle, vertex_stage,
            format, pipeline );
    if ( !vertex_shader )
        goto done;

    fragment_shader = shader_gpu_create_shader ( device, bundle, fragment_stage,
            format, pipeline );
    if ( !fragment_shader )
        goto done;

    if ( runtime_pass ) {
        runtime_pass->vertex_samplers = vertex_stage->samplers;
        runtime_pass->fragment_samplers = fragment_stage->samplers;
        runtime_pass->vertex_common_uniform_slot =
            vertex_stage->common_uniform_slot;
        runtime_pass->fragment_common_uniform_slot =
            fragment_stage->common_uniform_slot;
        runtime_pass->vertex_common_uniform_size =
            (Uint32) vertex_stage->common_uniform_size;
        runtime_pass->fragment_common_uniform_size =
            (Uint32) fragment_stage->common_uniform_size;
        runtime_pass->vertex_param_uniform_slot =
            vertex_stage->param_uniform_slot;
        runtime_pass->fragment_param_uniform_slot =
            fragment_stage->param_uniform_slot;
        runtime_pass->vertex_param_uniform_size =
            (Uint32) vertex_stage->param_uniform_size;
        runtime_pass->fragment_param_uniform_size =
            (Uint32) fragment_stage->param_uniform_size;
    }

    SDL_zero(vertex_buffer);
    vertex_buffer.slot = 0;
    vertex_buffer.pitch = sizeof(t_shader_gpu_vertex);
    vertex_buffer.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;

    SDL_zeroa(vertex_attributes);
    vertex_attributes[0].location = 0;
    vertex_attributes[0].buffer_slot = 0;
    vertex_attributes[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
    vertex_attributes[0].offset = offsetof(t_shader_gpu_vertex, position);
    vertex_attributes[1].location = 1;
    vertex_attributes[1].buffer_slot = 0;
    vertex_attributes[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
    vertex_attributes[1].offset = offsetof(t_shader_gpu_vertex, tex_coord);

    SDL_zero(color_target);
    color_target.format = color_format;

    SDL_zero(create_info);
    create_info.vertex_shader = vertex_shader;
    create_info.fragment_shader = fragment_shader;
    create_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
    create_info.vertex_input_state.vertex_buffer_descriptions = &vertex_buffer;
    create_info.vertex_input_state.num_vertex_buffers = 1;
    create_info.vertex_input_state.vertex_attributes = vertex_attributes;
    create_info.vertex_input_state.num_vertex_attributes = 2;
    create_info.rasterizer_state.fill_mode = SDL_GPU_FILLMODE_FILL;
    create_info.rasterizer_state.cull_mode = SDL_GPU_CULLMODE_NONE;
    create_info.rasterizer_state.front_face =
        SDL_GPU_FRONTFACE_COUNTER_CLOCKWISE;
    create_info.multisample_state.sample_count = SDL_GPU_SAMPLECOUNT_1;
    create_info.target_info.color_target_descriptions = &color_target;
    create_info.target_info.num_color_targets = 1;

    graphics_pipeline = SDL_CreateGPUGraphicsPipeline ( device, &create_info );
    if ( !graphics_pipeline )
        shader_gpu_set_error ( pipeline,
                "Failed to create SDL GPU graphics pipeline", SDL_GetError() );

done:
    if ( vertex_shader )
        SDL_ReleaseGPUShader ( device, vertex_shader );
    if ( fragment_shader )
        SDL_ReleaseGPUShader ( device, fragment_shader );

    return graphics_pipeline;

} /* end shader_gpu_create_pass_pipeline */

void shader_gpu_pipeline_init ( t_shader_gpu_pipeline *pipeline ) {

    if ( pipeline )
        memset ( pipeline, 0, sizeof(t_shader_gpu_pipeline) );

} /* end shader_gpu_pipeline_init */

void shader_gpu_pipeline_free ( t_shader_gpu_pipeline *pipeline ) {

    if ( !pipeline )
        return;

    if ( pipeline->device && pipeline->passes ) {
        for ( int i = 0; i < pipeline->num_passes; ++i ) {
            if ( pipeline->passes[i].pipeline )
                SDL_ReleaseGPUGraphicsPipeline ( pipeline->device,
                        pipeline->passes[i].pipeline );
        }
    }

    free ( pipeline->passes );
    shader_gpu_pipeline_init ( pipeline );

} /* end shader_gpu_pipeline_free */

int shader_gpu_pipeline_create ( SDL_GPUDevice *device,
        const t_shader_bundle *bundle, SDL_GPUTextureFormat color_format,
        const SDL_GPUTextureFormat *pass_color_formats,
        t_shader_gpu_pipeline *pipeline ) {

    SDL_GPUShaderFormat available_formats;

    if ( !pipeline )
        return -1;

    shader_gpu_pipeline_init ( pipeline );

    if ( !device || !bundle ) {
        shader_gpu_set_error ( pipeline,
                "Cannot create SDL GPU shader pipeline without device/bundle",
                "" );
        return -1;
    }

    available_formats = SDL_GetGPUShaderFormats ( device );
    pipeline->shader_format = shader_gpu_select_format ( available_formats,
            bundle );
    if ( pipeline->shader_format == SDL_GPU_SHADERFORMAT_INVALID ) {
        shader_gpu_set_error ( pipeline,
                "No compatible precompiled shader blob for GPU backend",
                bundle->id ? bundle->id : "" );
        return -1;
    }

    if ( color_format == SDL_GPU_TEXTUREFORMAT_INVALID )
        color_format = shader_gpu_default_color_format();

    pipeline->device = device;
    pipeline->color_format = color_format;
    pipeline->num_passes = bundle->num_passes;
    pipeline->passes = calloc ( (size_t) pipeline->num_passes,
            sizeof(t_shader_gpu_pass) );
    if ( !pipeline->passes ) {
        shader_gpu_set_error ( pipeline, "Out of memory creating pipeline",
                bundle->id ? bundle->id : "" );
        return -1;
    }

    for ( int i = 0; i < bundle->num_passes; ++i ) {
        SDL_GPUTextureFormat pass_format = pass_color_formats ?
                pass_color_formats[i] : color_format;

        pipeline->passes[i].bundle_pass_index = bundle->passes[i].index;
        pipeline->passes[i].pipeline = shader_gpu_create_pass_pipeline ( device,
                bundle, &bundle->passes[i], pipeline->shader_format,
                pass_format, pipeline, &pipeline->passes[i] );
        if ( !pipeline->passes[i].pipeline ) {
            shader_gpu_pipeline_free ( pipeline );
            return -1;
        }
    }

    return 0;

} /* end shader_gpu_pipeline_create */

static int shader_gpu_upload_static_quad ( t_shader_gpu_resources *resources ) {

    static const t_shader_gpu_vertex vertices[4] = {
        { { -1.0f,  1.0f, 0.0f, 1.0f }, { 0.0f, 0.0f } },
        { {  1.0f,  1.0f, 0.0f, 1.0f }, { 1.0f, 0.0f } },
        { {  1.0f, -1.0f, 0.0f, 1.0f }, { 1.0f, 1.0f } },
        { { -1.0f, -1.0f, 0.0f, 1.0f }, { 0.0f, 1.0f } }
    };
    static const Uint16 indices[6] = { 0, 1, 2, 0, 2, 3 };
    const Uint32 vertex_bytes = (Uint32) sizeof(vertices);
    const Uint32 index_bytes = (Uint32) sizeof(indices);
    SDL_GPUTransferBuffer *transfer_buffer = NULL;
    SDL_GPUCommandBuffer *command_buffer = NULL;
    SDL_GPUCopyPass *copy_pass = NULL;
    Uint8 *transfer_data;
    int result = -1;

    transfer_buffer = SDL_CreateGPUTransferBuffer ( resources->device,
            &(SDL_GPUTransferBufferCreateInfo) {
                .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
                .size = vertex_bytes + index_bytes
            } );
    if ( !transfer_buffer ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to create static quad transfer buffer",
                SDL_GetError() );
        goto done;
    }

    transfer_data = SDL_MapGPUTransferBuffer ( resources->device,
            transfer_buffer, false );
    if ( !transfer_data ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to map static quad transfer buffer", SDL_GetError() );
        goto done;
    }

    memcpy ( transfer_data, vertices, vertex_bytes );
    memcpy ( transfer_data + vertex_bytes, indices, index_bytes );
    SDL_UnmapGPUTransferBuffer ( resources->device, transfer_buffer );

    command_buffer = SDL_AcquireGPUCommandBuffer ( resources->device );
    if ( !command_buffer ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to acquire command buffer for static quad upload",
                SDL_GetError() );
        goto done;
    }

    copy_pass = SDL_BeginGPUCopyPass ( command_buffer );
    if ( !copy_pass ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to begin copy pass for static quad upload",
                SDL_GetError() );
        SDL_CancelGPUCommandBuffer ( command_buffer );
        command_buffer = NULL;
        goto done;
    }

    SDL_UploadToGPUBuffer ( copy_pass,
            &(SDL_GPUTransferBufferLocation) {
                .transfer_buffer = transfer_buffer,
                .offset = 0
            },
            &(SDL_GPUBufferRegion) {
                .buffer = resources->vertex_buffer,
                .offset = 0,
                .size = vertex_bytes
            },
            false );
    SDL_UploadToGPUBuffer ( copy_pass,
            &(SDL_GPUTransferBufferLocation) {
                .transfer_buffer = transfer_buffer,
                .offset = vertex_bytes
            },
            &(SDL_GPUBufferRegion) {
                .buffer = resources->index_buffer,
                .offset = 0,
                .size = index_bytes
            },
            false );
    SDL_EndGPUCopyPass ( copy_pass );

    if ( !SDL_SubmitGPUCommandBuffer(command_buffer) ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to submit static quad upload", SDL_GetError() );
        command_buffer = NULL;
        goto done;
    }
    command_buffer = NULL;
    result = 0;

done:
    if ( command_buffer )
        SDL_CancelGPUCommandBuffer ( command_buffer );
    if ( transfer_buffer )
        SDL_ReleaseGPUTransferBuffer ( resources->device, transfer_buffer );

    return result;

} /* end shader_gpu_upload_static_quad */

static SDL_GPUTexture * shader_gpu_create_source_texture (
        t_shader_gpu_resources *resources, int width, int height ) {

    SDL_GPUTexture *texture;

    texture = SDL_CreateGPUTexture ( resources->device,
            &(SDL_GPUTextureCreateInfo) {
                .type = SDL_GPU_TEXTURETYPE_2D,
                /*
                 * draw_pixel_start points at SDL ARGB8888 surfaces. On our
                 * supported little-endian targets those bytes are laid out as
                 * BGRA in memory, so the sampled GPU texture must match that
                 * byte order unless we add a CPU-side conversion step.
                 */
                .format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
                .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
                .width = (Uint32) width,
                .height = (Uint32) height,
                .layer_count_or_depth = 1,
                .num_levels = 1,
                .sample_count = SDL_GPU_SAMPLECOUNT_1
            } );
    if ( !texture )
        shader_gpu_set_resource_error ( resources,
                "Failed to create source texture", SDL_GetError() );

    return texture;

} /* end shader_gpu_create_source_texture */

void shader_gpu_resources_init ( t_shader_gpu_resources *resources ) {

    if ( resources )
        memset ( resources, 0, sizeof(t_shader_gpu_resources) );

} /* end shader_gpu_resources_init */

void shader_gpu_resources_free ( t_shader_gpu_resources *resources ) {

    SDL_GPUDevice *device;

    if ( !resources )
        return;

    device = resources->device;
    if ( device ) {
        if ( resources->source_transfer_buffer )
            SDL_ReleaseGPUTransferBuffer ( device,
                    resources->source_transfer_buffer );
        if ( resources->source_sampler )
            SDL_ReleaseGPUSampler ( device, resources->source_sampler );
        if ( resources->source_texture )
            SDL_ReleaseGPUTexture ( device, resources->source_texture );
        if ( resources->index_buffer )
            SDL_ReleaseGPUBuffer ( device, resources->index_buffer );
        if ( resources->vertex_buffer )
            SDL_ReleaseGPUBuffer ( device, resources->vertex_buffer );
    }

    shader_gpu_resources_init ( resources );

} /* end shader_gpu_resources_free */

static int shader_gpu_ensure_source_transfer_buffer (
        t_shader_gpu_resources *resources, Uint32 texture_bytes ) {

    SDL_GPUTransferBuffer *transfer_buffer;

    if ( !resources || !resources->device || texture_bytes == 0 ) {
        shader_gpu_set_resource_error ( resources,
                "Invalid source staging buffer request", "" );
        return -1;
    }

    if ( resources->source_transfer_buffer &&
            resources->source_transfer_capacity >= texture_bytes )
        return 0;

    if ( resources->source_transfer_buffer ) {
        SDL_ReleaseGPUTransferBuffer ( resources->device,
                resources->source_transfer_buffer );
        resources->source_transfer_buffer = NULL;
        resources->source_transfer_capacity = 0;
    }

    transfer_buffer = SDL_CreateGPUTransferBuffer ( resources->device,
            &(SDL_GPUTransferBufferCreateInfo) {
                .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
                .size = texture_bytes
            } );
    if ( !transfer_buffer ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to create source texture transfer buffer",
                SDL_GetError() );
        return -1;
    }

    resources->source_transfer_buffer = transfer_buffer;
    resources->source_transfer_capacity = texture_bytes;
    return 0;

} /* end shader_gpu_ensure_source_transfer_buffer */

int shader_gpu_resources_create ( SDL_GPUDevice *device, int source_width,
        int source_height, int linear_filter,
        t_shader_gpu_resources *resources ) {

    if ( !resources )
        return -1;

    shader_gpu_resources_init ( resources );
    if ( !device || source_width <= 0 || source_height <= 0 ) {
        shader_gpu_set_resource_error ( resources,
                "Cannot create SDL GPU shader resources without device/size",
                "" );
        return -1;
    }

    resources->device = device;
    resources->vertex_buffer = SDL_CreateGPUBuffer ( device,
            &(SDL_GPUBufferCreateInfo) {
                .usage = SDL_GPU_BUFFERUSAGE_VERTEX,
                .size = (Uint32) ( sizeof(t_shader_gpu_vertex) * 4 )
            } );
    if ( !resources->vertex_buffer ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to create shader vertex buffer", SDL_GetError() );
        goto error;
    }

    resources->index_buffer = SDL_CreateGPUBuffer ( device,
            &(SDL_GPUBufferCreateInfo) {
                .usage = SDL_GPU_BUFFERUSAGE_INDEX,
                .size = (Uint32) ( sizeof(Uint16) * 6 )
            } );
    if ( !resources->index_buffer ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to create shader index buffer", SDL_GetError() );
        goto error;
    }

    resources->source_texture = shader_gpu_create_source_texture ( resources,
            source_width, source_height );
    if ( !resources->source_texture )
        goto error;

    resources->source_sampler = SDL_CreateGPUSampler ( device,
            &(SDL_GPUSamplerCreateInfo) {
                .min_filter = linear_filter ? SDL_GPU_FILTER_LINEAR :
                    SDL_GPU_FILTER_NEAREST,
                .mag_filter = linear_filter ? SDL_GPU_FILTER_LINEAR :
                    SDL_GPU_FILTER_NEAREST,
                .mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST,
                .address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
                .address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE,
                .address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE
            } );
    if ( !resources->source_sampler ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to create shader source sampler", SDL_GetError() );
        goto error;
    }

    resources->source_width = source_width;
    resources->source_height = source_height;

    if ( shader_gpu_upload_static_quad(resources) )
        goto error;

    return 0;

error:
    shader_gpu_resources_free ( resources );
    return -1;

} /* end shader_gpu_resources_create */

int shader_gpu_resources_upload_source ( SDL_GPUCommandBuffer *command_buffer,
        t_shader_gpu_resources *resources, const void *pixels, int width,
        int height, int pitch ) {

    SDL_GPUCopyPass *copy_pass = NULL;
    Uint8 *transfer_data;
    const Uint8 *src;
    Uint32 row_bytes;
    Uint32 texture_bytes;
    int result = -1;

    if ( !command_buffer || !resources || !resources->device || !pixels ||
            width <= 0 ||
            height <= 0 || pitch < width * 4 ) {
        shader_gpu_set_resource_error ( resources,
                "Invalid source texture upload request", "" );
        return -1;
    }

    if ( width != resources->source_width ||
            height != resources->source_height ) {
        if ( resources->source_texture )
            SDL_ReleaseGPUTexture ( resources->device,
                    resources->source_texture );
        resources->source_texture = shader_gpu_create_source_texture ( resources,
                width, height );
        if ( !resources->source_texture )
            return -1;
        resources->source_width = width;
        resources->source_height = height;
    }

    row_bytes = (Uint32) width * 4;
    texture_bytes = row_bytes * (Uint32) height;
    if ( shader_gpu_ensure_source_transfer_buffer(resources, texture_bytes) )
        goto done;

    /* Cycle the staging buffer: it is rewritten every frame and a prior frame's
       upload may still reference it, so cycling avoids stalling on that pending
       copy.  The destination texture is cycled separately below. */
    transfer_data = SDL_MapGPUTransferBuffer ( resources->device,
            resources->source_transfer_buffer, true );
    if ( !transfer_data ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to map source texture transfer buffer",
                SDL_GetError() );
        goto done;
    }

    src = pixels;
    for ( int y = 0; y < height; ++y )
        memcpy ( transfer_data + (size_t) y * row_bytes,
                src + (size_t) y * pitch, row_bytes );
    SDL_UnmapGPUTransferBuffer ( resources->device,
            resources->source_transfer_buffer );

    copy_pass = SDL_BeginGPUCopyPass ( command_buffer );
    if ( !copy_pass ) {
        shader_gpu_set_resource_error ( resources,
                "Failed to begin copy pass for source texture upload",
                SDL_GetError() );
        goto done;
    }

    SDL_UploadToGPUTexture ( copy_pass,
            &(SDL_GPUTextureTransferInfo) {
                .transfer_buffer = resources->source_transfer_buffer,
                .offset = 0,
                .pixels_per_row = (Uint32) width,
                .rows_per_layer = (Uint32) height
            },
            &(SDL_GPUTextureRegion) {
                .texture = resources->source_texture,
                .w = (Uint32) width,
                .h = (Uint32) height,
                .d = 1
            },
            true );
    SDL_EndGPUCopyPass ( copy_pass );
    result = 0;

done:
    return result;

} /* end shader_gpu_resources_upload_source */

int shader_gpu_resources_draw_first_pass ( SDL_GPUCommandBuffer *command_buffer,
        SDL_GPURenderPass *render_pass, const t_shader_gpu_pipeline *pipeline,
        const t_shader_gpu_resources *resources,
        const t_shader_gpu_uniform_blocks *uniform_blocks ) {

    SDL_GPUBufferBinding vertex_binding;
    SDL_GPUBufferBinding index_binding;
    SDL_GPUTextureSamplerBinding sampler_binding;
    const t_shader_gpu_pass *pass;

    if ( !command_buffer || !render_pass || !pipeline || !resources ||
            !pipeline->passes || pipeline->num_passes <= 0 ||
            !resources->vertex_buffer || !resources->index_buffer ) {
        return -1;
    }

    pass = &pipeline->passes[0];
    if ( !pass->pipeline )
        return -1;

    SDL_BindGPUGraphicsPipeline ( render_pass, pass->pipeline );

    SDL_zero(vertex_binding);
    vertex_binding.buffer = resources->vertex_buffer;
    SDL_BindGPUVertexBuffers ( render_pass, 0, &vertex_binding, 1 );

    SDL_zero(index_binding);
    index_binding.buffer = resources->index_buffer;
    SDL_BindGPUIndexBuffer ( render_pass, &index_binding,
            SDL_GPU_INDEXELEMENTSIZE_16BIT );

    if ( pass->fragment_samplers > 0 ) {
        SDL_zero(sampler_binding);
        sampler_binding.texture = resources->source_texture;
        sampler_binding.sampler = resources->source_sampler;
        SDL_BindGPUFragmentSamplers ( render_pass, 0, &sampler_binding, 1 );
    }

    if ( uniform_blocks && pass->vertex_common_uniform_slot >= 0 &&
            uniform_blocks->vertex_common.size )
        SDL_PushGPUVertexUniformData ( command_buffer,
                (Uint32) pass->vertex_common_uniform_slot,
                uniform_blocks->vertex_common.data,
                uniform_blocks->vertex_common.size );
    if ( uniform_blocks && pass->fragment_common_uniform_slot >= 0 &&
            uniform_blocks->fragment_common.size )
        SDL_PushGPUFragmentUniformData ( command_buffer,
                (Uint32) pass->fragment_common_uniform_slot,
                uniform_blocks->fragment_common.data,
                uniform_blocks->fragment_common.size );
    if ( uniform_blocks && uniform_blocks->vertex_param.size &&
            pass->vertex_param_uniform_slot >= 0 )
        SDL_PushGPUVertexUniformData ( command_buffer,
                (Uint32) pass->vertex_param_uniform_slot,
                uniform_blocks->vertex_param.data,
                uniform_blocks->vertex_param.size );
    if ( uniform_blocks && uniform_blocks->fragment_param.size &&
            pass->fragment_param_uniform_slot >= 0 )
        SDL_PushGPUFragmentUniformData ( command_buffer,
                (Uint32) pass->fragment_param_uniform_slot,
                uniform_blocks->fragment_param.data,
                uniform_blocks->fragment_param.size );

    SDL_DrawGPUIndexedPrimitives ( render_pass, 6, 1, 0, 0, 0 );

    return 0;

} /* end shader_gpu_resources_draw_first_pass */

void shader_gpu_common_uniforms_fill ( t_shader_gpu_common_uniforms *uniforms,
        int source_width, int source_height, int original_width,
        int original_height, int output_width, int output_height,
        Uint32 frame_count, Sint32 frame_direction ) {

    if ( !uniforms )
        return;

    memset ( uniforms, 0, sizeof(*uniforms) );
    uniforms->mvp[0] = 1.0f;
    uniforms->mvp[5] = 1.0f;
    uniforms->mvp[10] = 1.0f;
    uniforms->mvp[15] = 1.0f;

    uniforms->output_size[0] = (float) output_width;
    uniforms->output_size[1] = (float) output_height;
    uniforms->output_size[2] = output_width > 0 ? 1.0f / output_width : 0.0f;
    uniforms->output_size[3] = output_height > 0 ? 1.0f / output_height : 0.0f;

    uniforms->original_size[0] = (float) original_width;
    uniforms->original_size[1] = (float) original_height;
    uniforms->original_size[2] = original_width > 0 ? 1.0f / original_width :
        0.0f;
    uniforms->original_size[3] = original_height > 0 ?
        1.0f / original_height : 0.0f;

    uniforms->source_size[0] = (float) source_width;
    uniforms->source_size[1] = (float) source_height;
    uniforms->source_size[2] = source_width > 0 ? 1.0f / source_width : 0.0f;
    uniforms->source_size[3] = source_height > 0 ? 1.0f / source_height :
        0.0f;
    uniforms->frame_count = frame_count;
    uniforms->frame_direction = frame_direction;

} /* end shader_gpu_common_uniforms_fill */

static t_shader_gpu_uniform_block * shader_gpu_block_for_resource (
        t_shader_gpu_uniform_blocks *blocks, const char *stage,
        const char *resource ) {

    if ( !blocks || !stage || !resource )
        return NULL;

    if ( !strcmp(stage, "vertex") ) {
        if ( !strcmp(resource, "UBO") )
            return &blocks->vertex_common;
        if ( !strcmp(resource, "Push") )
            return &blocks->vertex_param;
    }
    else if ( !strcmp(stage, "fragment") ) {
        if ( !strcmp(resource, "UBO") )
            return &blocks->fragment_common;
        if ( !strcmp(resource, "Push") )
            return &blocks->fragment_param;
    }

    return NULL;

} /* end shader_gpu_block_for_resource */

static int shader_gpu_block_write ( t_shader_gpu_uniform_block *block,
        int offset, const void *data, Uint32 size ) {

    Uint32 end;

    if ( !block || !data || offset < 0 ||
            offset + (int) size > SHADER_GPU_UNIFORM_BLOCK_MAX )
        return -1;

    memcpy ( block->data + offset, data, size );
    end = (Uint32) offset + size;
    if ( end > block->size )
        block->size = end;
    return 0;

} /* end shader_gpu_block_write */

static int shader_gpu_block_finalize ( t_shader_gpu_uniform_block *block,
        int block_size ) {

    if ( !block || block_size <= 0 ||
            block_size > SHADER_GPU_UNIFORM_BLOCK_MAX ||
            block->size > (Uint32) block_size )
        return -1;

    block->size = (Uint32) block_size;
    return 0;

} /* end shader_gpu_block_finalize */

/*
 * Resolve a texture's size vec4 (w, h, 1/w, 1/h) by its semantic base name
 * (the uniform name minus the trailing "Size"), e.g. "PALETTE" or "FirstPass".
 * Returns 0 on success.  Lets the graph fill <LUT>Size and <Alias>Size uniforms
 * the fixed semantic set does not cover.
 */
typedef int (*t_shader_gpu_size_fn) ( void *ctx, const char *base_name,
        float out_size[4] );

static int shader_gpu_pack_semantic_uniform (
        t_shader_gpu_uniform_block *block,
        const t_shader_bundle_uniform *uniform,
        const t_shader_gpu_common_uniforms *values,
        t_shader_gpu_size_fn size_fn, void *size_ctx ) {

    float vec2[2];
    float size_vec[4];
    float scalar_float;
    Uint32 scalar_uint;
    Sint32 scalar_int;
    size_t name_len;

    if ( !block || !uniform || !uniform->name || !values )
        return -1;

    if ( !strcmp(uniform->name, "MVP") ||
            !strcmp(uniform->name, "MVPMatrix") ) {
        return shader_gpu_block_write ( block, uniform->offset, values->mvp,
                (Uint32) sizeof(values->mvp) );
    }

    if ( !strcmp(uniform->name, "OutputSize") ) {
        return shader_gpu_block_write ( block, uniform->offset,
                values->output_size, (Uint32) sizeof(values->output_size) );
    }

    if ( !strcmp(uniform->name, "OriginalSize") ) {
        return shader_gpu_block_write ( block, uniform->offset,
                values->original_size,
                (Uint32) sizeof(values->original_size) );
    }

    if ( !strcmp(uniform->name, "SourceSize") ) {
        return shader_gpu_block_write ( block, uniform->offset,
                values->source_size, (Uint32) sizeof(values->source_size) );
    }

    if ( !strcmp(uniform->name, "InputSize") ) {
        vec2[0] = values->source_size[0];
        vec2[1] = values->source_size[1];
        return shader_gpu_block_write ( block, uniform->offset, vec2,
                (Uint32) sizeof(vec2) );
    }

    if ( !strcmp(uniform->name, "TextureSize") ) {
        vec2[0] = values->source_size[0];
        vec2[1] = values->source_size[1];
        return shader_gpu_block_write ( block, uniform->offset, vec2,
                (Uint32) sizeof(vec2) );
    }

    if ( !strcmp(uniform->name, "FrameCount") ) {
        if ( uniform->type && strstr(uniform->type, "int") &&
                !strstr(uniform->type, "uint") ) {
            scalar_int = (Sint32) values->frame_count;
            return shader_gpu_block_write ( block, uniform->offset,
                    &scalar_int, (Uint32) sizeof(scalar_int) );
        }
        if ( uniform->type && strstr(uniform->type, "float") ) {
            scalar_float = (float) values->frame_count;
            return shader_gpu_block_write ( block, uniform->offset,
                    &scalar_float, (Uint32) sizeof(scalar_float) );
        }
        scalar_uint = values->frame_count;
        return shader_gpu_block_write ( block, uniform->offset,
                &scalar_uint, (Uint32) sizeof(scalar_uint) );
    }

    if ( !strcmp(uniform->name, "FrameDirection") ) {
        if ( uniform->type && strstr(uniform->type, "float") ) {
            scalar_float = (float) values->frame_direction;
            return shader_gpu_block_write ( block, uniform->offset,
                    &scalar_float, (Uint32) sizeof(scalar_float) );
        }
        scalar_int = values->frame_direction;
        return shader_gpu_block_write ( block, uniform->offset, &scalar_int,
                (Uint32) sizeof(scalar_int) );
    }

    /* Dynamic <Texture>Size uniform: ask the size provider (LUTs, aliases). */
    name_len = strlen ( uniform->name );
    if ( size_fn && name_len > 4 &&
            !strcmp(uniform->name + name_len - 4, "Size") ) {
        char base[128];

        if ( name_len - 4 >= sizeof(base) )
            return -1;
        memcpy ( base, uniform->name, name_len - 4 );
        base[name_len - 4] = '\0';
        if ( size_fn(size_ctx, base, size_vec) )
            return -1;
        return shader_gpu_block_write ( block, uniform->offset, size_vec,
                (Uint32) sizeof(size_vec) );
    }

    return -1;

} /* end shader_gpu_pack_semantic_uniform */

static int shader_gpu_pack_stage_uniforms (
        const t_shader_bundle_pass *pass, const t_shader_bundle_stage *stage,
        const t_shader_gpu_common_uniforms *uniforms,
        t_shader_gpu_uniform_blocks *blocks, t_shader_gpu_size_fn size_fn,
        void *size_ctx ) {

    t_shader_gpu_uniform_block *block;

    if ( !pass || !stage || !stage->stage || !blocks )
        return -1;

    for ( int i = 0; i < stage->num_uniforms; ++i ) {
        const t_shader_bundle_uniform *uniform = &stage->uniforms[i];

        block = shader_gpu_block_for_resource ( blocks, stage->stage,
                uniform->resource );
        if ( !block || shader_gpu_pack_semantic_uniform(block, uniform,
                    uniforms, size_fn, size_ctx) )
            return -1;
    }

    for ( int i = 0; i < pass->num_params; ++i ) {
        const t_shader_bundle_param *param = &pass->params[i];

        if ( !shader_gpu_param_stage_matches(param, stage->stage) )
            continue;

        block = shader_gpu_block_for_resource ( blocks, stage->stage,
                param->resource );
        if ( !block || shader_gpu_block_write(block, param->offset,
                    &param->val, (Uint32) sizeof(param->val)) )
            return -1;
    }

    if ( stage->common_uniform_slot >= 0 ) {
        block = shader_gpu_block_for_resource ( blocks, stage->stage, "UBO" );
        if ( !block || shader_gpu_block_finalize(block,
                    stage->common_uniform_size) )
            return -1;
    }
    if ( stage->param_uniform_slot >= 0 ) {
        block = shader_gpu_block_for_resource ( blocks, stage->stage, "Push" );
        if ( !block || shader_gpu_block_finalize(block,
                    stage->param_uniform_size) )
            return -1;
    }

    return 0;

} /* end shader_gpu_pack_stage_uniforms */

static int shader_gpu_pack_uniform_blocks_impl ( const t_shader_bundle *bundle,
        int pass_index, const t_shader_gpu_common_uniforms *uniforms,
        t_shader_gpu_uniform_blocks *blocks, t_shader_gpu_size_fn size_fn,
        void *size_ctx ) {

    if ( !bundle || !uniforms || !blocks )
        return -1;

    memset ( blocks, 0, sizeof(*blocks) );

    for ( int i = 0; i < bundle->num_passes; ++i ) {
        const t_shader_bundle_pass *pass = &bundle->passes[i];

        if ( pass->index != pass_index )
            continue;

        for ( int j = 0; j < pass->num_stages; ++j ) {
            if ( shader_gpu_pack_stage_uniforms(pass, &pass->stages[j],
                        uniforms, blocks, size_fn, size_ctx) )
                return -1;
        }

        return 0;
    }

    return -1;

} /* end shader_gpu_pack_uniform_blocks_impl */

int shader_gpu_pack_uniform_blocks ( const t_shader_bundle *bundle,
        int pass_index, const t_shader_gpu_common_uniforms *uniforms,
        t_shader_gpu_uniform_blocks *blocks ) {

    return shader_gpu_pack_uniform_blocks_impl ( bundle, pass_index, uniforms,
            blocks, NULL, NULL );

} /* end shader_gpu_pack_uniform_blocks */

SDL_GPUShaderFormat shader_gpu_select_format (
        SDL_GPUShaderFormat available_formats, const t_shader_bundle *bundle ) {

    static const SDL_GPUShaderFormat preferred_formats[] = {
        SDL_GPU_SHADERFORMAT_SPIRV,
        SDL_GPU_SHADERFORMAT_MSL,
        SDL_GPU_SHADERFORMAT_DXIL
    };

    for ( int i = 0; i < (int) ( sizeof(preferred_formats) /
                sizeof(preferred_formats[0]) ); ++i ) {
        SDL_GPUShaderFormat format = preferred_formats[i];

        if ( (available_formats & format) &&
                shader_gpu_bundle_supports_format(bundle, format) )
            return format;
    }

    return SDL_GPU_SHADERFORMAT_INVALID;

} /* end shader_gpu_select_format */

const char * shader_gpu_format_name ( SDL_GPUShaderFormat format ) {

    switch ( format ) {
        case SDL_GPU_SHADERFORMAT_SPIRV:
            return "SPIR-V";
        case SDL_GPU_SHADERFORMAT_MSL:
            return "MSL";
        case SDL_GPU_SHADERFORMAT_DXIL:
            return "DXIL";
        case SDL_GPU_SHADERFORMAT_DXBC:
            return "DXBC";
        case SDL_GPU_SHADERFORMAT_METALLIB:
            return "METALLIB";
        default:
            return "INVALID";
    }

} /* end shader_gpu_format_name */

const char * shader_gpu_stage_blob_path ( const t_shader_bundle_stage *stage,
        SDL_GPUShaderFormat format ) {

    if ( !stage )
        return NULL;

    switch ( format ) {
        case SDL_GPU_SHADERFORMAT_SPIRV:
            return stage->spirv;
        case SDL_GPU_SHADERFORMAT_MSL:
            return stage->msl;
        case SDL_GPU_SHADERFORMAT_DXIL:
            return stage->dxil;
        default:
            return NULL;
    }

} /* end shader_gpu_stage_blob_path */

SDL_GPUTextureFormat shader_gpu_default_color_format ( void ) {

    return SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM;

} /* end shader_gpu_default_color_format */

/******************************************************************************
**  N-pass graph renderer
******************************************************************************/

#define SHADER_GPU_GRAPH_MAX_STAGE_SAMPLERS 16

static void shader_gpu_set_graph_error ( t_shader_gpu_graph *graph,
        const char *message, const char *detail ) {

    shader_gpu_log_error ( graph ? graph->error : NULL,
            graph ? sizeof(graph->error) : 0, message, detail );

} /* end shader_gpu_set_graph_error */

static int shader_gpu_parse_bool ( const char *value, int fallback ) {

    if ( !value || !strlen(value) )
        return fallback;

    if ( !strcmp(value, "true") || !strcmp(value, "1") )
        return 1;
    if ( !strcmp(value, "false") || !strcmp(value, "0") )
        return 0;

    return fallback;

} /* end shader_gpu_parse_bool */

static SDL_GPUSamplerAddressMode shader_gpu_parse_wrap ( const char *value ) {

    if ( value ) {
        if ( !strcmp(value, "repeat") )
            return SDL_GPU_SAMPLERADDRESSMODE_REPEAT;
        if ( !strcmp(value, "mirrored_repeat") )
            return SDL_GPU_SAMPLERADDRESSMODE_MIRRORED_REPEAT;
        /* SDL GPU has no clamp_to_border; clamp_to_edge is the closest match. */
    }

    return SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;

} /* end shader_gpu_parse_wrap */

/*
 * Resolve which bundle pass produced the texture a binding samples.  Returns
 * the producing pass index, or -1 to mean "the original/source frame" (which
 * has no producing pass).  consuming_pass is the pass doing the sampling.
 */
/* For LUT bindings there is no producing pass; callers special-case SEM_LUT. */
static int shader_gpu_binding_producer ( const t_shader_gpu_graph_binding *bind,
        int consuming_pass ) {

    switch ( bind->semantic ) {
        case SHADER_GPU_SEM_ORIGINAL:
            return -1;
        case SHADER_GPU_SEM_SOURCE:
            return consuming_pass - 1;          /* -1 => original for pass 0 */
        case SHADER_GPU_SEM_PASS_OUTPUT:
            return bind->ref;
        case SHADER_GPU_SEM_PASS_PREV:
            return consuming_pass - bind->ref;
        case SHADER_GPU_SEM_LUT:
            return -1;
        case SHADER_GPU_SEM_ORIGINAL_HISTORY:
            return -1;                          /* source-frame ring */
        case SHADER_GPU_SEM_PASS_FEEDBACK:
            return bind->ref;                   /* prior output of pass ref */
    }

    return -1;

} /* end shader_gpu_binding_producer */

static int shader_gpu_resolve_semantic ( const t_shader_bundle *bundle,
        const char *name, t_shader_gpu_semantic *semantic, int *ref ) {

    int value = 0;

    if ( !bundle || !name || !semantic || !ref )
        return -1;

    if ( !strcmp(name, "Source") ) {
        *semantic = SHADER_GPU_SEM_SOURCE;
        *ref = 0;
        return 0;
    }
    if ( !strcmp(name, "Original") ) {
        *semantic = SHADER_GPU_SEM_ORIGINAL;
        *ref = 0;
        return 0;
    }
    if ( sscanf(name, "PassOutput%d", &value) == 1 && value >= 0 ) {
        *semantic = SHADER_GPU_SEM_PASS_OUTPUT;
        *ref = value;
        return 0;
    }
    if ( sscanf(name, "PassPrev%d", &value) == 1 && value >= 1 ) {
        *semantic = SHADER_GPU_SEM_PASS_PREV;
        *ref = value;
        return 0;
    }
    if ( sscanf(name, "OriginalHistory%d", &value) == 1 && value >= 0 ) {
        *semantic = SHADER_GPU_SEM_ORIGINAL_HISTORY;
        *ref = value;
        return 0;
    }
    if ( sscanf(name, "PassFeedback%d", &value) == 1 && value >= 0 ) {
        *semantic = SHADER_GPU_SEM_PASS_FEEDBACK;
        *ref = value;
        return 0;
    }

    /* A pass alias resolves to that pass's output texture; <Alias>Feedback to
       that pass's previous-frame output. */
    {
        size_t name_len = strlen ( name );
        const char *feedback_suffix = "Feedback";
        size_t suffix_len = strlen ( feedback_suffix );
        int is_feedback = name_len > suffix_len &&
                !strcmp(name + name_len - suffix_len, feedback_suffix);

        for ( int i = 0; i < bundle->num_passes; ++i ) {
            const char *alias = bundle->passes[i].alias;
            if ( !alias || !strlen(alias) )
                continue;
            if ( !strcmp(alias, name) ) {
                *semantic = SHADER_GPU_SEM_PASS_OUTPUT;
                *ref = bundle->passes[i].index;
                return 0;
            }
            if ( is_feedback && strlen(alias) == name_len - suffix_len &&
                    !strncmp(alias, name, name_len - suffix_len) ) {
                *semantic = SHADER_GPU_SEM_PASS_FEEDBACK;
                *ref = bundle->passes[i].index;
                return 0;
            }
        }
    }

    /* A declared LUT texture name. */
    for ( int i = 0; i < bundle->num_textures; ++i ) {
        if ( bundle->textures[i].name &&
                !strcmp(bundle->textures[i].name, name) ) {
            *semantic = SHADER_GPU_SEM_LUT;
            *ref = i;
            return 0;
        }
    }

    return -1;

} /* end shader_gpu_resolve_semantic */

void shader_gpu_graph_init ( t_shader_gpu_graph *graph ) {

    if ( graph )
        memset ( graph, 0, sizeof(t_shader_gpu_graph) );

} /* end shader_gpu_graph_init */

static void shader_gpu_graph_free_targets ( t_shader_gpu_graph *graph ) {

    if ( !graph || !graph->passes || !graph->device )
        return;

    for ( int i = 0; i < graph->num_passes; ++i ) {
        if ( graph->passes[i].texture ) {
            SDL_ReleaseGPUTexture ( graph->device, graph->passes[i].texture );
            graph->passes[i].texture = NULL;
        }
        if ( graph->passes[i].feedback ) {
            SDL_ReleaseGPUTexture ( graph->device, graph->passes[i].feedback );
            graph->passes[i].feedback = NULL;
        }
        graph->passes[i].width = 0;
        graph->passes[i].height = 0;
        graph->passes[i].mip_levels = 0;
    }

    if ( graph->final_offscreen ) {
        SDL_ReleaseGPUTexture ( graph->device, graph->final_offscreen );
        graph->final_offscreen = NULL;
    }
    graph->final_offscreen_w = 0;
    graph->final_offscreen_h = 0;

    if ( graph->history ) {
        for ( int i = 0; i < graph->history_size; ++i ) {
            if ( graph->history[i] )
                SDL_ReleaseGPUTexture ( graph->device, graph->history[i] );
        }
        free ( graph->history );
        graph->history = NULL;
    }
    graph->history_head = 0;
    graph->history_valid = 0;
    graph->history_width = 0;
    graph->history_height = 0;

} /* end shader_gpu_graph_free_targets */

void shader_gpu_graph_free ( t_shader_gpu_graph *graph ) {

    if ( !graph )
        return;

    if ( graph->passes ) {
        shader_gpu_graph_free_targets ( graph );
        for ( int i = 0; i < graph->num_passes; ++i )
            free ( graph->passes[i].bindings );
        free ( graph->passes );
    }

    if ( graph->device ) {
        if ( graph->dummy_texture )
            SDL_ReleaseGPUTexture ( graph->device, graph->dummy_texture );
        if ( graph->history_transfer )
            SDL_ReleaseGPUTransferBuffer ( graph->device,
                    graph->history_transfer );
        for ( int i = 0; i < graph->num_samplers_cached; ++i ) {
            if ( graph->sampler_cache[i].sampler )
                SDL_ReleaseGPUSampler ( graph->device,
                        graph->sampler_cache[i].sampler );
        }
        if ( graph->luts ) {
            for ( int i = 0; i < graph->num_luts; ++i ) {
                if ( graph->luts[i].texture )
                    SDL_ReleaseGPUTexture ( graph->device,
                            graph->luts[i].texture );
            }
        }
    }
    free ( graph->luts );

    shader_gpu_resources_free ( &graph->resources );
    shader_gpu_pipeline_free ( &graph->pipeline );
    shader_gpu_graph_init ( graph );

} /* end shader_gpu_graph_free */

/* Samplers are deduplicated by (filter, wrap, mipmap) so an N-pass graph that
   keeps re-using the same sampler state allocates each combination once. */
static SDL_GPUSampler * shader_gpu_graph_get_sampler (
        t_shader_gpu_graph *graph, int linear, int wrap, int mipmap ) {

    SDL_GPUSampler *sampler;

    for ( int i = 0; i < graph->num_samplers_cached; ++i ) {
        if ( graph->sampler_cache[i].linear == linear &&
                graph->sampler_cache[i].wrap == wrap &&
                graph->sampler_cache[i].mipmap == mipmap )
            return graph->sampler_cache[i].sampler;
    }

    if ( graph->num_samplers_cached >= SHADER_GPU_SAMPLER_CACHE_MAX )
        return NULL;

    sampler = SDL_CreateGPUSampler ( graph->device,
            &(SDL_GPUSamplerCreateInfo) {
                .min_filter = linear ? SDL_GPU_FILTER_LINEAR :
                    SDL_GPU_FILTER_NEAREST,
                .mag_filter = linear ? SDL_GPU_FILTER_LINEAR :
                    SDL_GPU_FILTER_NEAREST,
                .mipmap_mode = mipmap ? SDL_GPU_SAMPLERMIPMAPMODE_LINEAR :
                    SDL_GPU_SAMPLERMIPMAPMODE_NEAREST,
                .address_mode_u = (SDL_GPUSamplerAddressMode) wrap,
                .address_mode_v = (SDL_GPUSamplerAddressMode) wrap,
                .address_mode_w = (SDL_GPUSamplerAddressMode) wrap
            } );
    if ( !sampler )
        return NULL;

    graph->sampler_cache[graph->num_samplers_cached].linear = linear;
    graph->sampler_cache[graph->num_samplers_cached].wrap = wrap;
    graph->sampler_cache[graph->num_samplers_cached].mipmap = mipmap;
    graph->sampler_cache[graph->num_samplers_cached].sampler = sampler;
    graph->num_samplers_cached++;

    return sampler;

} /* end shader_gpu_graph_get_sampler */

/*
 * Resolve the sampler for one binding.  For pass/source textures filter and
 * wrap belong to the producing pass while mipmap belongs to the consuming pass
 * (mipmap_input).  LUT samplers use the LUT's own filter/wrap/mipmap flags.
 */
static SDL_GPUSampler * shader_gpu_graph_make_sampler (
        t_shader_gpu_graph *graph, const t_shader_gpu_graph_binding *bind,
        int consuming_pass ) {

    const t_shader_bundle *bundle = graph->bundle;
    int linear;
    int mipmap;
    SDL_GPUSamplerAddressMode wrap;

    if ( bind->semantic == SHADER_GPU_SEM_LUT ) {
        const t_shader_bundle_texture *lut = &bundle->textures[bind->ref];
        linear = shader_gpu_parse_bool ( lut->filter_linear, 0 );
        wrap = shader_gpu_parse_wrap ( lut->wrap_mode );
        mipmap = shader_gpu_parse_bool ( lut->mipmap, 0 );
    }
    else {
        const t_shader_bundle_pass *consumer =
                &bundle->passes[consuming_pass];
        int producer_index = shader_gpu_binding_producer ( bind,
                consuming_pass );

        /* The original/source frame has no producing pass; treat pass 0's
           filter and wrap as its owner so single-pass bundles render
           unchanged. */
        if ( producer_index < 0 )
            producer_index = 0;

        linear = shader_gpu_parse_bool (
                bundle->passes[producer_index].filter_linear, 0 );
        wrap = shader_gpu_parse_wrap (
                bundle->passes[producer_index].wrap_mode );
        mipmap = shader_gpu_parse_bool ( consumer->mipmap_input, 0 );
    }

    return shader_gpu_graph_get_sampler ( graph, linear, (int) wrap, mipmap );

} /* end shader_gpu_graph_make_sampler */

static int shader_gpu_graph_resolve_pass_bindings ( t_shader_gpu_graph *graph,
        int pass_index ) {

    const t_shader_bundle_pass *pass = &graph->bundle->passes[pass_index];
    t_shader_gpu_graph_pass *runtime = &graph->passes[pass_index];
    int total = 0;
    int out = 0;

    for ( int s = 0; s < pass->num_stages; ++s )
        total += pass->stages[s].num_sampler_bindings;

    runtime->num_bindings = total;
    if ( !total )
        return 0;

    runtime->bindings = calloc ( (size_t) total,
            sizeof(t_shader_gpu_graph_binding) );
    if ( !runtime->bindings ) {
        shader_gpu_set_graph_error ( graph, "Out of memory resolving bindings",
                "" );
        return -1;
    }

    for ( int s = 0; s < pass->num_stages; ++s ) {
        const t_shader_bundle_stage *stage = &pass->stages[s];
        int is_fragment = stage->stage && !strcmp(stage->stage, "fragment");

        for ( int b = 0; b < stage->num_sampler_bindings; ++b ) {
            const t_shader_bundle_sampler *src = &stage->sampler_bindings[b];
            t_shader_gpu_graph_binding *dst = &runtime->bindings[out];
            int producer;

            if ( shader_gpu_resolve_semantic(graph->bundle, src->name,
                        &dst->semantic, &dst->ref) ) {
                shader_gpu_set_graph_error ( graph,
                        "Unresolved active sampler semantic", src->name );
                return -1;
            }

            dst->slot = src->slot;
            dst->fragment = is_fragment;

            if ( src->slot < 0 ||
                    src->slot >= SHADER_GPU_GRAPH_MAX_STAGE_SAMPLERS ) {
                shader_gpu_set_graph_error ( graph,
                        "Sampler slot exceeds the per-stage binding limit",
                        src->name );
                return -1;
            }

            if ( dst->semantic == SHADER_GPU_SEM_LUT ) {
                if ( dst->ref < 0 || dst->ref >= graph->num_luts ) {
                    shader_gpu_set_graph_error ( graph,
                            "Sampler references an out-of-range LUT",
                            src->name );
                    return -1;
                }
            }
            else if ( dst->semantic == SHADER_GPU_SEM_ORIGINAL_HISTORY ) {
                if ( dst->ref < 0 ) {
                    shader_gpu_set_graph_error ( graph,
                            "Sampler references a negative history index",
                            src->name );
                    return -1;
                }
            }
            else if ( dst->semantic == SHADER_GPU_SEM_PASS_FEEDBACK ) {
                /* Feedback reads the previous frame, so a pass referencing its
                   own (or a later pass's) feedback is valid. */
                if ( dst->ref < 0 || dst->ref >= graph->num_passes ) {
                    shader_gpu_set_graph_error ( graph,
                            "Sampler references an out-of-range feedback pass",
                            src->name );
                    return -1;
                }
            }
            else {
                producer = shader_gpu_binding_producer ( dst, pass_index );
                if ( producer >= pass_index ) {
                    shader_gpu_set_graph_error ( graph,
                            "Sampler references a pass output not yet produced",
                            src->name );
                    return -1;
                }
                if ( producer < -1 || ( producer >= 0 &&
                            producer >= graph->num_passes ) ) {
                    shader_gpu_set_graph_error ( graph,
                            "Sampler references an out-of-range pass",
                            src->name );
                    return -1;
                }
            }

            dst->sampler = shader_gpu_graph_make_sampler ( graph, dst,
                    pass_index );
            if ( !dst->sampler ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create graph sampler", SDL_GetError() );
                return -1;
            }

            out++;
        }
    }

    return 0;

} /* end shader_gpu_graph_resolve_pass_bindings */

static int shader_gpu_mip_levels ( int width, int height ) {

    int max_dim = width > height ? width : height;
    int levels = 1;

    while ( max_dim > 1 ) {
        max_dim >>= 1;
        levels++;
    }

    return levels;

} /* end shader_gpu_mip_levels */

/*
 * Load every declared LUT with SDL3_image, upload it to a GPU texture, and
 * generate mipmaps when the LUT requests them.  All uploads share one command
 * buffer so the graph is ready to sample LUTs after a single submit.
 */
static int shader_gpu_graph_load_luts ( t_shader_gpu_graph *graph ) {

    const t_shader_bundle *bundle = graph->bundle;
    SDL_GPUCommandBuffer *command_buffer = NULL;
    SDL_GPUCopyPass *copy_pass = NULL;
    SDL_GPUTransferBuffer **transfers = NULL;
    int generate_any = 0;
    int result = -1;

    if ( bundle->num_textures <= 0 )
        return 0;

    graph->num_luts = bundle->num_textures;
    graph->luts = calloc ( (size_t) graph->num_luts,
            sizeof(t_shader_gpu_lut) );
    transfers = calloc ( (size_t) graph->num_luts,
            sizeof(SDL_GPUTransferBuffer *) );
    if ( !graph->luts || !transfers ) {
        shader_gpu_set_graph_error ( graph, "Out of memory loading LUTs", "" );
        goto done;
    }

    command_buffer = SDL_AcquireGPUCommandBuffer ( graph->device );
    if ( !command_buffer ) {
        shader_gpu_set_graph_error ( graph,
                "Failed to acquire command buffer for LUT upload",
                SDL_GetError() );
        goto done;
    }

    copy_pass = SDL_BeginGPUCopyPass ( command_buffer );
    if ( !copy_pass ) {
        shader_gpu_set_graph_error ( graph, "Failed to begin LUT copy pass",
                SDL_GetError() );
        goto done;
    }

    for ( int i = 0; i < graph->num_luts; ++i ) {
        const t_shader_bundle_texture *meta = &bundle->textures[i];
        char path[KAT_PATH_MAX];
        SDL_Surface *surface;
        SDL_Surface *converted;
        Uint32 bytes;
        Uint8 *map;
        int want_mipmap = shader_gpu_parse_bool ( meta->mipmap, 0 );
        int levels;
        SDL_GPUTextureUsageFlags usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;

        if ( snprintf(path, sizeof(path), "%s/%s", bundle->dir, meta->file) >=
                (int) sizeof(path) ) {
            shader_gpu_set_graph_error ( graph, "LUT path too long",
                    meta->name );
            goto done;
        }

        surface = IMG_Load ( path );
        if ( !surface ) {
            shader_gpu_set_graph_error ( graph, "Failed to load LUT image",
                    SDL_GetError() );
            goto done;
        }

        converted = SDL_ConvertSurface ( surface, SDL_PIXELFORMAT_RGBA32 );
        SDL_DestroySurface ( surface );
        if ( !converted ) {
            shader_gpu_set_graph_error ( graph, "Failed to convert LUT image",
                    SDL_GetError() );
            goto done;
        }

        levels = want_mipmap ?
                shader_gpu_mip_levels ( converted->w, converted->h ) : 1;
        if ( levels > 1 )
            usage |= SDL_GPU_TEXTUREUSAGE_COLOR_TARGET;

        graph->luts[i].width = converted->w;
        graph->luts[i].height = converted->h;
        graph->luts[i].texture = SDL_CreateGPUTexture ( graph->device,
                &(SDL_GPUTextureCreateInfo) {
                    .type = SDL_GPU_TEXTURETYPE_2D,
                    .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
                    .usage = usage,
                    .width = (Uint32) converted->w,
                    .height = (Uint32) converted->h,
                    .layer_count_or_depth = 1,
                    .num_levels = (Uint32) levels,
                    .sample_count = SDL_GPU_SAMPLECOUNT_1
                } );
        if ( !graph->luts[i].texture ) {
            shader_gpu_set_graph_error ( graph, "Failed to create LUT texture",
                    SDL_GetError() );
            SDL_DestroySurface ( converted );
            goto done;
        }

        bytes = (Uint32) converted->w * (Uint32) converted->h * 4;
        transfers[i] = SDL_CreateGPUTransferBuffer ( graph->device,
                &(SDL_GPUTransferBufferCreateInfo) {
                    .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
                    .size = bytes
                } );
        if ( !transfers[i] ) {
            shader_gpu_set_graph_error ( graph,
                    "Failed to create LUT transfer buffer", SDL_GetError() );
            SDL_DestroySurface ( converted );
            goto done;
        }

        map = SDL_MapGPUTransferBuffer ( graph->device, transfers[i], false );
        if ( !map ) {
            shader_gpu_set_graph_error ( graph,
                    "Failed to map LUT transfer buffer", SDL_GetError() );
            SDL_DestroySurface ( converted );
            goto done;
        }
        for ( int y = 0; y < converted->h; ++y )
            memcpy ( map + (size_t) y * (size_t) converted->w * 4,
                    (const Uint8 *) converted->pixels +
                    (size_t) y * (size_t) converted->pitch,
                    (size_t) converted->w * 4 );
        SDL_UnmapGPUTransferBuffer ( graph->device, transfers[i] );

        SDL_UploadToGPUTexture ( copy_pass,
                &(SDL_GPUTextureTransferInfo) {
                    .transfer_buffer = transfers[i],
                    .offset = 0,
                    .pixels_per_row = (Uint32) converted->w,
                    .rows_per_layer = (Uint32) converted->h
                },
                &(SDL_GPUTextureRegion) {
                    .texture = graph->luts[i].texture,
                    .w = (Uint32) converted->w,
                    .h = (Uint32) converted->h,
                    .d = 1
                },
                false );

        SDL_DestroySurface ( converted );
        if ( levels > 1 )
            generate_any = 1;
    }

    SDL_EndGPUCopyPass ( copy_pass );
    copy_pass = NULL;

    if ( generate_any ) {
        for ( int i = 0; i < graph->num_luts; ++i ) {
            if ( graph->luts[i].texture &&
                    shader_gpu_mip_levels(graph->luts[i].width,
                        graph->luts[i].height) > 1 &&
                    shader_gpu_parse_bool(bundle->textures[i].mipmap, 0) )
                SDL_GenerateMipmapsForGPUTexture ( command_buffer,
                        graph->luts[i].texture );
        }
    }

    if ( !SDL_SubmitGPUCommandBuffer(command_buffer) ) {
        shader_gpu_set_graph_error ( graph, "Failed to submit LUT upload",
                SDL_GetError() );
        command_buffer = NULL;
        goto done;
    }
    command_buffer = NULL;
    result = 0;

done:
    if ( copy_pass )
        SDL_EndGPUCopyPass ( copy_pass );
    if ( command_buffer )
        SDL_CancelGPUCommandBuffer ( command_buffer );
    if ( transfers ) {
        for ( int i = 0; i < graph->num_luts; ++i ) {
            if ( transfers[i] )
                SDL_ReleaseGPUTransferBuffer ( graph->device, transfers[i] );
        }
        free ( transfers );
    }

    return result;

} /* end shader_gpu_graph_load_luts */

/* Free the partially built graph but keep its error text so the caller can log
   a real reason (shader_gpu_graph_free zeroes the whole struct). */
static int shader_gpu_graph_create_fail ( t_shader_gpu_graph *graph ) {

    char saved[512];

    snprintf ( saved, sizeof(saved), "%s", graph->error );
    shader_gpu_graph_free ( graph );
    snprintf ( graph->error, sizeof(graph->error), "%s", saved );

    return -1;

} /* end shader_gpu_graph_create_fail */

/*
 * Resolve a pass's output framebuffer format.  float_framebuffer wins over
 * srgb_framebuffer (and *both is reported so the caller can log it); a pass
 * with neither flag uses the graph's default color format.
 */
static SDL_GPUTextureFormat shader_gpu_format_for_pass (
        const t_shader_bundle_pass *pass, SDL_GPUTextureFormat default_format,
        int *float_over_srgb ) {

    int is_float = shader_gpu_parse_bool ( pass->float_framebuffer, 0 );
    int is_srgb = shader_gpu_parse_bool ( pass->srgb_framebuffer, 0 );

    if ( float_over_srgb )
        *float_over_srgb = is_float && is_srgb;

    if ( is_float )
        return SDL_GPU_TEXTUREFORMAT_R16G16B16A16_FLOAT;
    if ( is_srgb )
        return SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM_SRGB;

    return default_format;

} /* end shader_gpu_format_for_pass */

static int shader_gpu_format_supported ( SDL_GPUDevice *device,
        SDL_GPUTextureFormat format ) {

    return SDL_GPUTextureSupportsFormat ( device, format,
            SDL_GPU_TEXTURETYPE_2D,
            SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER );

} /* end shader_gpu_format_supported */

int shader_gpu_graph_create ( SDL_GPUDevice *device,
        const t_shader_bundle *bundle, SDL_GPUTextureFormat color_format,
        int source_width, int source_height, t_shader_gpu_graph *graph ) {

    SDL_GPUTextureFormat *formats = NULL;

    if ( !graph )
        return -1;

    shader_gpu_graph_init ( graph );

    if ( !device || !bundle || bundle->num_passes <= 0 ) {
        shader_gpu_set_graph_error ( graph,
                "Cannot create shader graph without device/bundle", "" );
        return -1;
    }

    /* The shader may be selected from the menu before a frame exists, so the
       source dimensions can still be zero here.  The source texture and the
       intermediate targets are reallocated on the first upload/render, so seed
       a 1x1 placeholder rather than refusing to build the graph. */
    if ( source_width <= 0 )
        source_width = 1;
    if ( source_height <= 0 )
        source_height = 1;

    if ( color_format == SDL_GPU_TEXTUREFORMAT_INVALID )
        color_format = shader_gpu_default_color_format();

    graph->device = device;
    graph->bundle = bundle;
    graph->color_format = color_format;
    graph->num_passes = bundle->num_passes;
    graph->source_width = source_width;
    graph->source_height = source_height;

    graph->passes = calloc ( (size_t) graph->num_passes,
            sizeof(t_shader_gpu_graph_pass) );
    if ( !graph->passes ) {
        shader_gpu_set_graph_error ( graph, "Out of memory creating graph",
                "" );
        return shader_gpu_graph_create_fail ( graph );
    }

    /* Resolve each pass output format (float/sRGB/default), confirm the device
       supports it, and record it for pipeline and target creation. */
    formats = calloc ( (size_t) graph->num_passes, sizeof(*formats) );
    if ( !formats ) {
        shader_gpu_set_graph_error ( graph, "Out of memory for pass formats",
                "" );
        return shader_gpu_graph_create_fail ( graph );
    }
    for ( int i = 0; i < graph->num_passes; ++i ) {
        int both = 0;

        formats[i] = shader_gpu_format_for_pass ( &bundle->passes[i],
                color_format, &both );
        graph->passes[i].format = formats[i];

        if ( both ) {
            char detail[128];
            snprintf ( detail, sizeof(detail),
                    "pass %d sets float and srgb framebuffer; using float", i );
            logger_log_message ( LOG_INFO, detail, LOG_SRC );
        }

        if ( formats[i] != color_format &&
                !shader_gpu_format_supported(device, formats[i]) ) {
            shader_gpu_set_graph_error ( graph,
                    "GPU does not support a required framebuffer format",
                    bundle->id ? bundle->id : "" );
            free ( formats );
            return shader_gpu_graph_create_fail ( graph );
        }
    }

    if ( shader_gpu_pipeline_create(device, bundle, color_format, formats,
                &graph->pipeline) ) {
        snprintf ( graph->error, sizeof(graph->error), "%s",
                graph->pipeline.error );
        free ( formats );
        return shader_gpu_graph_create_fail ( graph );
    }
    free ( formats );
    formats = NULL;

    if ( shader_gpu_resources_create(device, source_width, source_height, 0,
                &graph->resources) ) {
        snprintf ( graph->error, sizeof(graph->error), "%s",
                graph->resources.error );
        return shader_gpu_graph_create_fail ( graph );
    }

    if ( shader_gpu_graph_load_luts(graph) )
        return shader_gpu_graph_create_fail ( graph );

    /* 1x1 fallback for unused (sparse) reflected sampler slots. */
    graph->dummy_texture = SDL_CreateGPUTexture ( device,
            &(SDL_GPUTextureCreateInfo) {
                .type = SDL_GPU_TEXTURETYPE_2D,
                .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
                .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
                .width = 1,
                .height = 1,
                .layer_count_or_depth = 1,
                .num_levels = 1,
                .sample_count = SDL_GPU_SAMPLECOUNT_1
            } );
    graph->dummy_sampler = shader_gpu_graph_get_sampler ( graph, 0,
            (int) SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE, 0 );
    if ( !graph->dummy_texture || !graph->dummy_sampler ) {
        shader_gpu_set_graph_error ( graph, "Failed to create dummy sampler",
                SDL_GetError() );
        return shader_gpu_graph_create_fail ( graph );
    }

    for ( int i = 0; i < graph->num_passes; ++i ) {
        if ( shader_gpu_graph_resolve_pass_bindings(graph, i) )
            return shader_gpu_graph_create_fail ( graph );
    }

    /* Flag every pass whose output a later pass samples with mipmap_input so
       its target gets a mip chain that is regenerated after each render. */
    for ( int c = 0; c < graph->num_passes; ++c ) {
        const t_shader_gpu_graph_pass *pass = &graph->passes[c];

        if ( !shader_gpu_parse_bool(bundle->passes[c].mipmap_input, 0) )
            continue;

        for ( int b = 0; b < pass->num_bindings; ++b ) {
            const t_shader_gpu_graph_binding *bind = &pass->bindings[b];
            int producer;

            /* LUTs carry their own mip flag and history is source-sized; every
               other semantic (including PassFeedback) maps to a pass output
               whose ping-pong pair must then carry mips. */
            if ( bind->semantic == SHADER_GPU_SEM_LUT ||
                    bind->semantic == SHADER_GPU_SEM_ORIGINAL_HISTORY )
                continue;
            producer = shader_gpu_binding_producer ( bind, c );
            if ( producer >= 0 && producer < graph->num_passes )
                graph->passes[producer].needs_mipmap = 1;
        }
    }

    /* Derive the OriginalHistory ring depth and flag feedback-target passes by
       scanning every resolved binding for history and feedback semantics. */
    for ( int c = 0; c < graph->num_passes; ++c ) {
        const t_shader_gpu_graph_pass *pass = &graph->passes[c];

        for ( int b = 0; b < pass->num_bindings; ++b ) {
            const t_shader_gpu_graph_binding *bind = &pass->bindings[b];

            if ( bind->semantic == SHADER_GPU_SEM_ORIGINAL_HISTORY ) {
                if ( bind->ref > graph->history_depth )
                    graph->history_depth = bind->ref;
                if ( bind->ref + 1 > graph->history_size )
                    graph->history_size = bind->ref + 1;
            }
            else if ( bind->semantic == SHADER_GPU_SEM_PASS_FEEDBACK ) {
                graph->passes[bind->ref].is_feedback = 1;
                graph->has_feedback = 1;
            }
        }
    }

    return 0;

} /* end shader_gpu_graph_create */

/* RetroArch-style per-axis output sizing for an intermediate pass. */
static int shader_gpu_scaled_dim ( const char *scale_type, const char *scale,
        int input_dim, int viewport_dim ) {

    float factor = 1.0f;
    int result;

    if ( scale && strlen(scale) )
        factor = strtof ( scale, NULL );

    if ( scale_type && !strcmp(scale_type, "absolute") )
        result = (int) ( factor + 0.5f );
    else if ( scale_type && !strcmp(scale_type, "viewport") )
        result = (int) ( (float) viewport_dim * factor + 0.5f );
    else
        result = (int) ( (float) input_dim * factor + 0.5f );

    return result > 0 ? result : 1;

} /* end shader_gpu_scaled_dim */

static void shader_gpu_pass_output_size ( const t_shader_bundle_pass *pass,
        int input_w, int input_h, int viewport_w, int viewport_h,
        int *out_w, int *out_h ) {

    const char *type_x = pass->scale_type_x ? pass->scale_type_x :
        pass->scale_type;
    const char *type_y = pass->scale_type_y ? pass->scale_type_y :
        pass->scale_type;
    const char *scale_x = pass->scale_x ? pass->scale_x : pass->scale;
    const char *scale_y = pass->scale_y ? pass->scale_y : pass->scale;

    *out_w = shader_gpu_scaled_dim ( type_x, scale_x, input_w, viewport_w );
    *out_h = shader_gpu_scaled_dim ( type_y, scale_y, input_h, viewport_h );

} /* end shader_gpu_pass_output_size */

/* Clear every feedback read buffer to transparent black (first-frame default). */
static int shader_gpu_graph_clear_feedback ( t_shader_gpu_graph *graph ) {

    SDL_GPUCommandBuffer *command_buffer;

    command_buffer = SDL_AcquireGPUCommandBuffer ( graph->device );
    if ( !command_buffer ) {
        shader_gpu_set_graph_error ( graph,
                "Failed to acquire command buffer to clear feedback",
                SDL_GetError() );
        return -1;
    }

    for ( int i = 0; i < graph->num_passes; ++i ) {
        SDL_GPUColorTargetInfo color_target;
        SDL_GPURenderPass *render_pass;

        if ( !graph->passes[i].feedback )
            continue;

        SDL_zero ( color_target );
        color_target.texture = graph->passes[i].feedback;
        color_target.clear_color = (SDL_FColor) { 0.0f, 0.0f, 0.0f, 0.0f };
        color_target.load_op = SDL_GPU_LOADOP_CLEAR;
        color_target.store_op = SDL_GPU_STOREOP_STORE;
        render_pass = SDL_BeginGPURenderPass ( command_buffer, &color_target,
                1, NULL );
        if ( !render_pass ) {
            shader_gpu_set_graph_error ( graph,
                    "Failed to begin feedback clear pass", SDL_GetError() );
            SDL_CancelGPUCommandBuffer ( command_buffer );
            return -1;
        }
        SDL_EndGPURenderPass ( render_pass );
    }

    if ( !SDL_SubmitGPUCommandBuffer(command_buffer) ) {
        shader_gpu_set_graph_error ( graph, "Failed to clear feedback targets",
                SDL_GetError() );
        return -1;
    }

    return 0;

} /* end shader_gpu_graph_clear_feedback */

int shader_gpu_graph_set_sizes ( t_shader_gpu_graph *graph, int source_width,
        int source_height, int viewport_width, int viewport_height ) {

    int input_w;
    int input_h;

    if ( !graph || !graph->device || !graph->passes )
        return -1;

    if ( source_width <= 0 || source_height <= 0 || viewport_width <= 0 ||
            viewport_height <= 0 )
        return -1;

    /* Coalesce: nothing to rebuild when the driving sizes are unchanged.  Only
       the intermediate target textures depend on size, so a resize never
       recompiles pipelines or re-resolves sampler bindings. */
    if ( graph->source_width == source_width &&
            graph->source_height == source_height &&
            graph->viewport_width == viewport_width &&
            graph->viewport_height == viewport_height &&
            ( graph->num_passes <= 1 || graph->passes[0].texture ) )
        return 0;

    graph->source_width = source_width;
    graph->source_height = source_height;
    graph->viewport_width = viewport_width;
    graph->viewport_height = viewport_height;

    shader_gpu_graph_free_targets ( graph );

    /* The final pass renders straight into the caller target, so only passes
       0..N-2 need intermediate output textures. */
    input_w = source_width;
    input_h = source_height;
    for ( int i = 0; i < graph->num_passes - 1; ++i ) {
        int out_w;
        int out_h;
        int levels;

        shader_gpu_pass_output_size ( &graph->bundle->passes[i], input_w,
                input_h, viewport_width, viewport_height, &out_w, &out_h );

        levels = graph->passes[i].needs_mipmap ?
                shader_gpu_mip_levels ( out_w, out_h ) : 1;

        graph->passes[i].texture = SDL_CreateGPUTexture ( graph->device,
                &(SDL_GPUTextureCreateInfo) {
                    .type = SDL_GPU_TEXTURETYPE_2D,
                    .format = graph->passes[i].format,
                    .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                        SDL_GPU_TEXTUREUSAGE_SAMPLER,
                    .width = (Uint32) out_w,
                    .height = (Uint32) out_h,
                    .layer_count_or_depth = 1,
                    .num_levels = (Uint32) levels,
                    .sample_count = SDL_GPU_SAMPLECOUNT_1
                } );
        if ( !graph->passes[i].texture ) {
            shader_gpu_set_graph_error ( graph,
                    "Failed to create intermediate pass target",
                    SDL_GetError() );
            shader_gpu_graph_free_targets ( graph );
            return -1;
        }
        graph->passes[i].mip_levels = levels;

        /* Feedback passes need a second, identically configured buffer for the
           previous frame's output (ping-pong).  It must mirror the write
           buffer's mip levels because the two are swapped each frame. */
        if ( graph->passes[i].is_feedback ) {
            graph->passes[i].feedback = SDL_CreateGPUTexture ( graph->device,
                    &(SDL_GPUTextureCreateInfo) {
                        .type = SDL_GPU_TEXTURETYPE_2D,
                        .format = graph->passes[i].format,
                        .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                            SDL_GPU_TEXTUREUSAGE_SAMPLER,
                        .width = (Uint32) out_w,
                        .height = (Uint32) out_h,
                        .layer_count_or_depth = 1,
                        .num_levels = (Uint32) levels,
                        .sample_count = SDL_GPU_SAMPLECOUNT_1
                    } );
            if ( !graph->passes[i].feedback ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create feedback target", SDL_GetError() );
                shader_gpu_graph_free_targets ( graph );
                return -1;
            }
        }

        graph->passes[i].width = out_w;
        graph->passes[i].height = out_h;
        input_w = out_w;
        input_h = out_h;
    }

    /* Decide how the final pass presents.  If its declared output size matches
       the target viewport it renders straight into the caller target; if not
       it renders to an offscreen target sized by its scale metadata and is
       blitted to the viewport rect (clearing the letterbox bars). */
    {
        int final_pass = graph->num_passes - 1;
        SDL_GPUTextureFormat final_format = graph->passes[final_pass].format;
        int final_w;
        int final_h;

        shader_gpu_pass_output_size ( &graph->bundle->passes[final_pass],
                input_w, input_h, viewport_width, viewport_height, &final_w,
                &final_h );

        /* Direct-final requires the output to match the target viewport AND the
           swapchain format; a float/sRGB final pass must render offscreen and
           be blitted (which converts to the swapchain format).  A final pass
           that is sampled as feedback also renders offscreen so its previous
           frame output can be preserved by the normal ping-pong swap. */
        graph->passes[final_pass].width = final_w;
        graph->passes[final_pass].height = final_h;
        if ( final_w == viewport_width && final_h == viewport_height &&
                final_format == graph->color_format &&
                !graph->passes[final_pass].is_feedback ) {
            graph->final_direct = 1;
        }
        else if ( graph->passes[final_pass].is_feedback ) {
            int levels = graph->passes[final_pass].needs_mipmap ?
                    shader_gpu_mip_levels ( final_w, final_h ) : 1;

            graph->final_direct = 0;
            graph->passes[final_pass].texture =
                    SDL_CreateGPUTexture ( graph->device,
                    &(SDL_GPUTextureCreateInfo) {
                        .type = SDL_GPU_TEXTURETYPE_2D,
                        .format = final_format,
                        .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                            SDL_GPU_TEXTUREUSAGE_SAMPLER,
                        .width = (Uint32) final_w,
                        .height = (Uint32) final_h,
                        .layer_count_or_depth = 1,
                        .num_levels = (Uint32) levels,
                        .sample_count = SDL_GPU_SAMPLECOUNT_1
                    } );
            if ( !graph->passes[final_pass].texture ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create feedback final target",
                        SDL_GetError() );
                shader_gpu_graph_free_targets ( graph );
                return -1;
            }

            graph->passes[final_pass].feedback =
                    SDL_CreateGPUTexture ( graph->device,
                    &(SDL_GPUTextureCreateInfo) {
                        .type = SDL_GPU_TEXTURETYPE_2D,
                        .format = final_format,
                        .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                            SDL_GPU_TEXTUREUSAGE_SAMPLER,
                        .width = (Uint32) final_w,
                        .height = (Uint32) final_h,
                        .layer_count_or_depth = 1,
                        .num_levels = (Uint32) levels,
                        .sample_count = SDL_GPU_SAMPLECOUNT_1
                    } );
            if ( !graph->passes[final_pass].feedback ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create feedback final read target",
                        SDL_GetError() );
                shader_gpu_graph_free_targets ( graph );
                return -1;
            }
            graph->passes[final_pass].mip_levels = levels;
        }
        else {
            graph->final_direct = 0;
            graph->final_offscreen = SDL_CreateGPUTexture ( graph->device,
                    &(SDL_GPUTextureCreateInfo) {
                        .type = SDL_GPU_TEXTURETYPE_2D,
                        .format = final_format,
                        .usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET |
                            SDL_GPU_TEXTUREUSAGE_SAMPLER,
                        .width = (Uint32) final_w,
                        .height = (Uint32) final_h,
                        .layer_count_or_depth = 1,
                        .num_levels = 1,
                        .sample_count = SDL_GPU_SAMPLECOUNT_1
                    } );
            if ( !graph->final_offscreen ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create offscreen final target",
                        SDL_GetError() );
                shader_gpu_graph_free_targets ( graph );
                return -1;
            }
            graph->final_offscreen_w = final_w;
            graph->final_offscreen_h = final_h;
        }
    }

    /* Allocate the source history ring (source-sized).  It is (re)filled from
       the first frame after this (re)allocation, so mark it invalid. */
    if ( graph->history_size > 0 ) {
        graph->history = calloc ( (size_t) graph->history_size,
                sizeof(SDL_GPUTexture *) );
        if ( !graph->history ) {
            shader_gpu_set_graph_error ( graph, "Out of memory for history",
                    "" );
            shader_gpu_graph_free_targets ( graph );
            return -1;
        }
        for ( int i = 0; i < graph->history_size; ++i ) {
            graph->history[i] = SDL_CreateGPUTexture ( graph->device,
                    &(SDL_GPUTextureCreateInfo) {
                        .type = SDL_GPU_TEXTURETYPE_2D,
                        .format = SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM,
                        .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
                        .width = (Uint32) source_width,
                        .height = (Uint32) source_height,
                        .layer_count_or_depth = 1,
                        .num_levels = 1,
                        .sample_count = SDL_GPU_SAMPLECOUNT_1
                    } );
            if ( !graph->history[i] ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to create history texture", SDL_GetError() );
                shader_gpu_graph_free_targets ( graph );
                return -1;
            }
        }
        graph->history_head = 0;
        graph->history_valid = 0;
        graph->history_width = source_width;
        graph->history_height = source_height;
    }

    /* Initialize feedback read buffers to transparent black so the first frame
       reads defined data.  On failure drop the targets and the cached driving
       sizes so the next call rebuilds (and re-clears) instead of hitting the
       coalesce early-return with uncleared feedback. */
    if ( graph->has_feedback && shader_gpu_graph_clear_feedback(graph) ) {
        shader_gpu_graph_free_targets ( graph );
        graph->source_width = 0;
        graph->source_height = 0;
        graph->viewport_width = 0;
        graph->viewport_height = 0;
        return -1;
    }

    return 0;

} /* end shader_gpu_graph_set_sizes */

static SDL_GPUTexture * shader_gpu_graph_binding_texture (
        const t_shader_gpu_graph *graph, const t_shader_gpu_graph_binding *bind,
        int consuming_pass ) {

    int producer;

    if ( bind->semantic == SHADER_GPU_SEM_LUT )
        return graph->luts[bind->ref].texture;

    if ( bind->semantic == SHADER_GPU_SEM_PASS_FEEDBACK )
        return graph->passes[bind->ref].feedback;

    if ( bind->semantic == SHADER_GPU_SEM_ORIGINAL_HISTORY ) {
        int slot;
        if ( !graph->history || graph->history_size <= 0 )
            return NULL;
        slot = ( graph->history_head - bind->ref + graph->history_size )
                % graph->history_size;
        return graph->history[slot];
    }

    producer = shader_gpu_binding_producer ( bind, consuming_pass );
    if ( producer < 0 ) {
        /* Source/Original of pass 0 come from the history ring head when
           history is active, otherwise the plain uploaded source texture. */
        if ( graph->history )
            return graph->history[graph->history_head];
        return graph->resources.source_texture;
    }

    return graph->passes[producer].texture;

} /* end shader_gpu_graph_binding_texture */

static int shader_gpu_graph_bind_stage_samplers (
        SDL_GPURenderPass *render_pass, const t_shader_gpu_graph *graph,
        int pass_index, int fragment ) {

    const t_shader_gpu_graph_pass *pass = &graph->passes[pass_index];
    SDL_GPUTextureSamplerBinding slots[SHADER_GPU_GRAPH_MAX_STAGE_SAMPLERS];
    int count = 0;

    SDL_zeroa ( slots );

    for ( int b = 0; b < pass->num_bindings; ++b ) {
        const t_shader_gpu_graph_binding *bind = &pass->bindings[b];
        SDL_GPUTexture *texture;

        if ( bind->fragment != fragment )
            continue;
        if ( bind->slot < 0 || bind->slot >= SHADER_GPU_GRAPH_MAX_STAGE_SAMPLERS )
            return -1;

        texture = shader_gpu_graph_binding_texture ( graph, bind, pass_index );
        if ( !texture )
            return -1;

        slots[bind->slot].texture = texture;
        slots[bind->slot].sampler = bind->sampler;
        if ( bind->slot + 1 > count )
            count = bind->slot + 1;
    }

    if ( !count )
        return 0;

    /* Reflected slots can be sparse (a shared header may declare samplers a
       pass does not use); bind a 1x1 dummy in any gap so the whole [0,count)
       range has a valid descriptor. */
    for ( int i = 0; i < count; ++i ) {
        if ( !slots[i].texture || !slots[i].sampler ) {
            slots[i].texture = graph->dummy_texture;
            slots[i].sampler = graph->dummy_sampler;
        }
    }

    if ( fragment )
        SDL_BindGPUFragmentSamplers ( render_pass, 0, slots, (Uint32) count );
    else
        SDL_BindGPUVertexSamplers ( render_pass, 0, slots, (Uint32) count );

    return 0;

} /* end shader_gpu_graph_bind_stage_samplers */

static int shader_gpu_graph_draw_pass ( SDL_GPUCommandBuffer *command_buffer,
        SDL_GPURenderPass *render_pass, t_shader_gpu_graph *graph,
        int pass_index, const t_shader_gpu_uniform_blocks *uniform_blocks ) {

    const t_shader_gpu_pass *pass = &graph->pipeline.passes[pass_index];
    SDL_GPUBufferBinding vertex_binding;
    SDL_GPUBufferBinding index_binding;

    if ( !pass->pipeline )
        return -1;

    SDL_BindGPUGraphicsPipeline ( render_pass, pass->pipeline );

    SDL_zero ( vertex_binding );
    vertex_binding.buffer = graph->resources.vertex_buffer;
    SDL_BindGPUVertexBuffers ( render_pass, 0, &vertex_binding, 1 );

    SDL_zero ( index_binding );
    index_binding.buffer = graph->resources.index_buffer;
    SDL_BindGPUIndexBuffer ( render_pass, &index_binding,
            SDL_GPU_INDEXELEMENTSIZE_16BIT );

    if ( shader_gpu_graph_bind_stage_samplers(render_pass, graph, pass_index,
                1) )
        return -1;
    if ( shader_gpu_graph_bind_stage_samplers(render_pass, graph, pass_index,
                0) )
        return -1;

    if ( uniform_blocks && pass->vertex_common_uniform_slot >= 0 &&
            uniform_blocks->vertex_common.size )
        SDL_PushGPUVertexUniformData ( command_buffer,
                (Uint32) pass->vertex_common_uniform_slot,
                uniform_blocks->vertex_common.data,
                uniform_blocks->vertex_common.size );
    if ( uniform_blocks && pass->fragment_common_uniform_slot >= 0 &&
            uniform_blocks->fragment_common.size )
        SDL_PushGPUFragmentUniformData ( command_buffer,
                (Uint32) pass->fragment_common_uniform_slot,
                uniform_blocks->fragment_common.data,
                uniform_blocks->fragment_common.size );
    if ( uniform_blocks && uniform_blocks->vertex_param.size &&
            pass->vertex_param_uniform_slot >= 0 )
        SDL_PushGPUVertexUniformData ( command_buffer,
                (Uint32) pass->vertex_param_uniform_slot,
                uniform_blocks->vertex_param.data,
                uniform_blocks->vertex_param.size );
    if ( uniform_blocks && uniform_blocks->fragment_param.size &&
            pass->fragment_param_uniform_slot >= 0 )
        SDL_PushGPUFragmentUniformData ( command_buffer,
                (Uint32) pass->fragment_param_uniform_slot,
                uniform_blocks->fragment_param.data,
                uniform_blocks->fragment_param.size );

    SDL_DrawGPUIndexedPrimitives ( render_pass, 6, 1, 0, 0, 0 );

    return 0;

} /* end shader_gpu_graph_draw_pass */

/* Context the dynamic <Texture>Size resolver needs while packing one pass. */
typedef struct {
    t_shader_gpu_graph *graph;
    int consuming_pass;
    int input_width;
    int input_height;
} t_shader_gpu_size_ctx;

static void shader_gpu_size_vec ( int w, int h, float out[4] ) {

    out[0] = (float) w;
    out[1] = (float) h;
    out[2] = w > 0 ? 1.0f / (float) w : 0.0f;
    out[3] = h > 0 ? 1.0f / (float) h : 0.0f;

} /* end shader_gpu_size_vec */

static int shader_gpu_graph_size_lookup ( void *ctx_ptr, const char *base_name,
        float out_size[4] ) {

    t_shader_gpu_size_ctx *ctx = ctx_ptr;
    t_shader_gpu_graph *graph = ctx->graph;
    t_shader_gpu_semantic semantic;
    int ref;

    if ( shader_gpu_resolve_semantic(graph->bundle, base_name, &semantic,
                &ref) )
        return -1;

    switch ( semantic ) {
        case SHADER_GPU_SEM_LUT:
            shader_gpu_size_vec ( graph->luts[ref].width,
                    graph->luts[ref].height, out_size );
            return 0;
        case SHADER_GPU_SEM_ORIGINAL:
            shader_gpu_size_vec ( graph->source_width, graph->source_height,
                    out_size );
            return 0;
        case SHADER_GPU_SEM_SOURCE:
            shader_gpu_size_vec ( ctx->input_width, ctx->input_height,
                    out_size );
            return 0;
        case SHADER_GPU_SEM_ORIGINAL_HISTORY:
            /* Every history frame is the source size. */
            shader_gpu_size_vec ( graph->source_width, graph->source_height,
                    out_size );
            return 0;
        case SHADER_GPU_SEM_PASS_FEEDBACK:
            if ( ref < 0 || ref >= graph->num_passes )
                return -1;
            shader_gpu_size_vec ( graph->passes[ref].width,
                    graph->passes[ref].height, out_size );
            return 0;
        case SHADER_GPU_SEM_PASS_OUTPUT:
        case SHADER_GPU_SEM_PASS_PREV: {
            int producer;
            t_shader_gpu_graph_binding bind;

            bind.semantic = semantic;
            bind.ref = ref;
            producer = shader_gpu_binding_producer ( &bind,
                    ctx->consuming_pass );
            if ( producer < 0 )
                shader_gpu_size_vec ( graph->source_width,
                        graph->source_height, out_size );
            else if ( producer < graph->num_passes )
                shader_gpu_size_vec ( graph->passes[producer].width,
                        graph->passes[producer].height, out_size );
            else
                return -1;
            return 0;
        }
    }

    return -1;

} /* end shader_gpu_graph_size_lookup */

/*
 * Upload the current source frame into the history ring.  On the first frame
 * after (re)allocation the frame fills every slot so OriginalHistory# reads a
 * defined image; afterwards only the head slot is written and the ring is
 * rotated post-submit by shader_gpu_graph_advance.
 */
static int shader_gpu_graph_upload_history ( SDL_GPUCommandBuffer *command_buffer,
        t_shader_gpu_graph *graph, const void *pixels, int width, int height,
        int pitch ) {

    Uint32 row_bytes = (Uint32) width * 4;
    Uint32 bytes = row_bytes * (Uint32) height;
    SDL_GPUCopyPass *copy_pass;
    Uint8 *map;
    const Uint8 *src = pixels;

    if ( width != graph->history_width || height != graph->history_height ) {
        shader_gpu_set_graph_error ( graph,
                "History ring size does not match source frame", "" );
        return -1;
    }

    if ( !graph->history_transfer ||
            graph->history_transfer_capacity < bytes ) {
        if ( graph->history_transfer )
            SDL_ReleaseGPUTransferBuffer ( graph->device,
                    graph->history_transfer );
        graph->history_transfer = SDL_CreateGPUTransferBuffer ( graph->device,
                &(SDL_GPUTransferBufferCreateInfo) {
                    .usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD,
                    .size = bytes
                } );
        if ( !graph->history_transfer ) {
            graph->history_transfer_capacity = 0;
            shader_gpu_set_graph_error ( graph,
                    "Failed to create history transfer buffer",
                    SDL_GetError() );
            return -1;
        }
        graph->history_transfer_capacity = bytes;
    }

    /* Cycle the staging buffer: rewritten every frame and a prior frame's
       history upload may still reference it.  Destination textures cycle
       separately in the upload loop below. */
    map = SDL_MapGPUTransferBuffer ( graph->device, graph->history_transfer,
            true );
    if ( !map ) {
        shader_gpu_set_graph_error ( graph,
                "Failed to map history transfer buffer", SDL_GetError() );
        return -1;
    }
    for ( int y = 0; y < height; ++y )
        memcpy ( map + (size_t) y * row_bytes, src + (size_t) y * pitch,
                row_bytes );
    SDL_UnmapGPUTransferBuffer ( graph->device, graph->history_transfer );

    copy_pass = SDL_BeginGPUCopyPass ( command_buffer );
    if ( !copy_pass ) {
        shader_gpu_set_graph_error ( graph,
                "Failed to begin history copy pass", SDL_GetError() );
        return -1;
    }

    for ( int i = 0; i < graph->history_size; ++i ) {
        /* First frame fills every slot; later frames only the head. */
        if ( graph->history_valid && i != graph->history_head )
            continue;

        SDL_UploadToGPUTexture ( copy_pass,
                &(SDL_GPUTextureTransferInfo) {
                    .transfer_buffer = graph->history_transfer,
                    .offset = 0,
                    .pixels_per_row = (Uint32) width,
                    .rows_per_layer = (Uint32) height
                },
                &(SDL_GPUTextureRegion) {
                    .texture = graph->history[i],
                    .w = (Uint32) width,
                    .h = (Uint32) height,
                    .d = 1
                },
                true );
    }
    SDL_EndGPUCopyPass ( copy_pass );

    /* history_valid is only promoted post-submit (in shader_gpu_graph_advance)
       so a failed frame re-fills every slot instead of leaving stale ones. */

    return 0;

} /* end shader_gpu_graph_upload_history */

int shader_gpu_graph_render_to_texture ( SDL_GPUCommandBuffer *command_buffer,
        t_shader_gpu_graph *graph, SDL_GPUTexture *final_target,
        const SDL_GPUViewport *final_viewport, const void *pixels, int width,
        int height, int pitch, Uint32 frame_count, Sint32 frame_direction ) {

    int input_w;
    int input_h;

    if ( !command_buffer || !graph || !final_target || !graph->bundle ||
            graph->num_passes <= 0 )
        return -1;

    /* When history is active the source frame is uploaded into the ring (which
       also feeds Source/Original of pass 0); otherwise use the plain source
       texture. */
    if ( graph->history ) {
        if ( shader_gpu_graph_upload_history(command_buffer, graph, pixels,
                    width, height, pitch) )
            return -1;
    }
    else if ( shader_gpu_resources_upload_source(command_buffer,
                &graph->resources, pixels, width, height, pitch) ) {
        snprintf ( graph->error, sizeof(graph->error), "%s",
                graph->resources.error );
        return -1;
    }

    input_w = width;
    input_h = height;
    for ( int i = 0; i < graph->num_passes; ++i ) {
        int is_final = ( i == graph->num_passes - 1 );
        int offscreen_final = is_final && !graph->final_direct &&
                ( graph->final_offscreen || graph->passes[i].texture );
        SDL_GPUColorTargetInfo color_target;
        SDL_GPURenderPass *render_pass;
        SDL_GPUTexture *render_target;
        SDL_GPUViewport viewport;
        t_shader_gpu_common_uniforms uniforms;
        t_shader_gpu_uniform_blocks uniform_blocks;
        t_shader_gpu_size_ctx size_ctx;
        int out_w;
        int out_h;

        if ( is_final && graph->final_direct ) {
            out_w = final_viewport ? (int) final_viewport->w : input_w;
            out_h = final_viewport ? (int) final_viewport->h : input_h;
            render_target = final_target;
        }
        else if ( offscreen_final ) {
            out_w = graph->passes[i].width;
            out_h = graph->passes[i].height;
            render_target = graph->passes[i].texture ?
                    graph->passes[i].texture : graph->final_offscreen;
        }
        else {
            out_w = graph->passes[i].width;
            out_h = graph->passes[i].height;
            render_target = graph->passes[i].texture;
            if ( !render_target || out_w <= 0 || out_h <= 0 ) {
                shader_gpu_set_graph_error ( graph,
                        "Graph sizes not set before render", "" );
                return -1;
            }
        }

        size_ctx.graph = graph;
        size_ctx.consuming_pass = i;
        size_ctx.input_width = input_w;
        size_ctx.input_height = input_h;

        shader_gpu_common_uniforms_fill ( &uniforms, input_w, input_h,
                graph->source_width, graph->source_height, out_w, out_h,
                frame_count, frame_direction );
        if ( shader_gpu_pack_uniform_blocks_impl(graph->bundle,
                    graph->bundle->passes[i].index, &uniforms,
                    &uniform_blocks, shader_gpu_graph_size_lookup,
                    &size_ctx) ) {
            shader_gpu_set_graph_error ( graph,
                    "Failed to pack reflected uniform blocks", "" );
            return -1;
        }

        SDL_zero ( color_target );
        color_target.texture = render_target;
        color_target.clear_color = (SDL_FColor) { 0.0f, 0.0f, 0.0f, 1.0f };
        color_target.load_op = SDL_GPU_LOADOP_CLEAR;
        color_target.store_op = SDL_GPU_STOREOP_STORE;

        render_pass = SDL_BeginGPURenderPass ( command_buffer, &color_target, 1,
                NULL );
        if ( !render_pass ) {
            shader_gpu_set_graph_error ( graph, "Failed to begin render pass",
                    SDL_GetError() );
            return -1;
        }

        if ( is_final && graph->final_direct && final_viewport ) {
            SDL_SetGPUViewport ( render_pass, (SDL_GPUViewport *) final_viewport );
        }
        else {
            SDL_zero ( viewport );
            viewport.w = (float) out_w;
            viewport.h = (float) out_h;
            viewport.max_depth = 1.0f;
            SDL_SetGPUViewport ( render_pass, &viewport );
        }

        if ( shader_gpu_graph_draw_pass(command_buffer, render_pass, graph, i,
                    &uniform_blocks) ) {
            SDL_EndGPURenderPass ( render_pass );
            shader_gpu_set_graph_error ( graph, "Failed to draw graph pass",
                    "" );
            return -1;
        }

        SDL_EndGPURenderPass ( render_pass );

        /* Refresh the mip chain before a later pass samples this output with
           mipmap_input.  Must happen outside any render/copy pass. */
        if ( graph->passes[i].needs_mipmap && graph->passes[i].texture &&
                graph->passes[i].mip_levels > 1 )
            SDL_GenerateMipmapsForGPUTexture ( command_buffer,
                    graph->passes[i].texture );

        /* Offscreen final pass: clear the caller target (letterbox bars) and
           blit the scaled output into the viewport rect. */
        if ( offscreen_final ) {
            SDL_GPUBlitInfo blit;
            SDL_GPURenderPass *clear_pass;

            SDL_zero ( color_target );
            color_target.texture = final_target;
            color_target.clear_color = (SDL_FColor) { 0.0f, 0.0f, 0.0f, 1.0f };
            color_target.load_op = SDL_GPU_LOADOP_CLEAR;
            color_target.store_op = SDL_GPU_STOREOP_STORE;
            clear_pass = SDL_BeginGPURenderPass ( command_buffer, &color_target,
                    1, NULL );
            if ( !clear_pass ) {
                shader_gpu_set_graph_error ( graph,
                        "Failed to begin final clear pass", SDL_GetError() );
                return -1;
            }
            SDL_EndGPURenderPass ( clear_pass );

            SDL_zero ( blit );
            blit.source.texture = render_target;
            blit.source.w = (Uint32) out_w;
            blit.source.h = (Uint32) out_h;
            blit.destination.texture = final_target;
            if ( final_viewport ) {
                blit.destination.x = (Uint32) final_viewport->x;
                blit.destination.y = (Uint32) final_viewport->y;
                blit.destination.w = (Uint32) final_viewport->w;
                blit.destination.h = (Uint32) final_viewport->h;
            }
            else {
                blit.destination.w = (Uint32) out_w;
                blit.destination.h = (Uint32) out_h;
            }
            blit.load_op = SDL_GPU_LOADOP_LOAD;
            blit.filter = SDL_GPU_FILTER_LINEAR;
            SDL_BlitGPUTexture ( command_buffer, &blit );
        }

        input_w = out_w;
        input_h = out_h;
    }

    return 0;

} /* end shader_gpu_graph_render_to_texture */

void shader_gpu_graph_advance ( t_shader_gpu_graph *graph ) {

    if ( !graph )
        return;

    /* Promote the ring to valid (first successful frame filled every slot) and
       rotate so the just-rendered frame becomes History1 next frame. */
    if ( graph->history && graph->history_size > 0 ) {
        graph->history_valid = 1;
        graph->history_head = ( graph->history_head + 1 ) %
                graph->history_size;
    }

    /* Swap each feedback pass's write/read buffers so this frame's output is
       sampled as PassFeedback next frame. */
    for ( int i = 0; i < graph->num_passes; ++i ) {
        if ( graph->passes[i].is_feedback && graph->passes[i].feedback ) {
            SDL_GPUTexture *tmp = graph->passes[i].texture;
            graph->passes[i].texture = graph->passes[i].feedback;
            graph->passes[i].feedback = tmp;
        }
    }

} /* end shader_gpu_graph_advance */
