floooh / sokol

minimal cross-platform standalone C headers
https://floooh.github.io/sokol-html5
zlib License
7.09k stars 499 forks source link

Blend with sokol_sgl #1133

Closed leiradel closed 3 weeks ago

leiradel commented 4 weeks ago

I need to use blending when drawing sprites, but there no sgl_enable_blend function or something similar.

I tried to create a pipeline during setup

sg_desc const d = sg_query_desc();
sg_pipeline_desc desc = {};

desc.depth.pixel_format = d.environment.defaults.depth_format;
desc.colors[0].pixel_format = d.environment.defaults.color_format;
desc.colors[0].blend.enabled = true;
desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
desc.colors[0].blend.op_rgb = SG_BLENDOP_ADD;
desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
desc.colors[0].blend.op_alpha = SG_BLENDOP_ADD;
desc.color_count = 1;

s_pipeline = sgl_make_pipeline(&desc);

and then use it when starting passes

void rm::renderer::beginPass(RenderTarget const* target) {
    if (target == nullptr) {
        sgl_set_context(SGL_DEFAULT_CONTEXT);
    }
    else {
        auto const t = (SokolRenderTarget const*)target;
        sgl_set_context(t->_context);
    }

    sgl_defaults();
    sgl_load_pipeline(s_pipeline);

    if (target == nullptr) {
        sgl_ortho(0.0f, (float)s_size.width(), (float)s_size.height(), 0.0f, -1.0f, 1.0f);
    }
    else {
        sgl_ortho(0.0f, (float)target->size().width(), (float)target->size().height(), 0.0f, -1.0f, 1.0f);
    }
}

but I got a validation error:

[ERROR] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16783: VALIDATE_APIP_COLOR_FORMAT: sg_apply_pipeline: pipeline color attachment pixel format doesn't match pass color attachment pixel format
[ERROR] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16790: VALIDATE_APIP_DEPTH_FORMAT: sg_apply_pipeline: pipeline depth pixel_format doesn't match pass depth attachment pixel format
[FATAL] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16168: VALIDATION_FAILED: validation layer checks failed

Sorry, I'm at a loss here as to how to enable blending.

floooh commented 4 weeks ago

The validation layer errors say that the pipeline pixel formats don't match the render pass / sgl-context pixel formats you're using that pipeline to render to:

desc.depth.pixel_format = d.environment.defaults.depth_format;
desc.colors[0].pixel_format = d.environment.defaults.color_format;

E.g. the environment-default values would only work when using that pipeline for rendering into the swapchain framebuffer, not into your offscreen render target.

For rendering into your offscreen render target you'd probably want these value (taken from your example code here when initializing the 'offscreen context'):

desc.depth.pixel_format = SG_PIXELFORMAT_RGBA8;
desc.colors[0].pixel_format = SG_PIXELFORMAT_NONE;
desc.sample_count = 1;
leiradel commented 4 weeks ago

Thanks. I'm now creating two pipelines:

{
    sg_environment_defaults const defaults = sg_query_desc().environment.defaults;
    sg_pipeline_desc desc = {};

    desc.depth.pixel_format = defaults.depth_format;
    desc.colors[0].pixel_format = defaults.color_format;
    desc.colors[0].blend.enabled = true;
    desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
    desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    desc.colors[0].blend.op_rgb = SG_BLENDOP_ADD;
    desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
    desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    desc.colors[0].blend.op_alpha = SG_BLENDOP_ADD;
    desc.color_count = 1;
    desc.sample_count = defaults.sample_count;

    s_defaultPipeline = sgl_make_pipeline(&desc);
}

{
    sg_pipeline_desc desc = {};

    desc.depth.pixel_format = SG_PIXELFORMAT_NONE;
    desc.colors[0].pixel_format = SG_PIXELFORMAT_RGBA8;
    desc.colors[0].blend.enabled = true;
    desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
    desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    desc.colors[0].blend.op_rgb = SG_BLENDOP_ADD;
    desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
    desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
    desc.colors[0].blend.op_alpha = SG_BLENDOP_ADD;
    desc.color_count = 1;
    desc.sample_count = 1;

    s_rtPipeline = sgl_make_pipeline(&desc);
}

I'm using the appropriate one when I start a pass:

void rm::renderer::beginPass(RenderTarget const* target) {
    if (target == nullptr) {
        sgl_set_context(SGL_DEFAULT_CONTEXT);
        sgl_defaults();
        sgl_load_pipeline(s_defaultPipeline);
        sgl_ortho(0.0f, (float)s_size.width(), (float)s_size.height(), 0.0f, -1.0f, 1.0f);
    }
    else {
        auto const t = (SokolRenderTarget const*)target;
        sgl_set_context(t->_context);
        sgl_defaults();
        sgl_load_pipeline(s_rtPipeline);
        sgl_ortho(0.0f, (float)target->size().width(), (float)target->size().height(), 0.0f, -1.0f, 1.0f);
    }
}

But I'm still getting the same error:

[ERROR] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16783: VALIDATE_APIP_COLOR_FORMAT: sg_apply_pipeline: pipeline color attachment pixel format doesn't match pass color attachment pixel format
[ERROR] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16790: VALIDATE_APIP_DEPTH_FORMAT: sg_apply_pipeline: pipeline depth pixel_format doesn't match pass depth attachment pixel format
[FATAL] D:\git\RetroplayerWindows\Retroplayer\ThirdParty\sokol\sokol_gfx.h:16168: VALIDATION_FAILED: validation layer checks failed

I appreciate your patience, I can translate VU1 machine code to C that uses SIMD, but I struggle with graphics programming.

floooh commented 4 weeks ago

Hmm... what does your actual drawing code look like (e.g. where you call sgl_draw() inside a sokol-gfx sg_begin/end_pass)?

...just to make sure that there's no mixup between sokol-gfx render passes and sokol-gl contexts.

(tbh: the idea of 'contexts' in the higher level sokol headers is quite confusing and I should come up with something more intuitive which better matches the sokol-gfx api)

leiradel commented 3 weeks ago

This is how I setup everything:

void rm::renderer::init() {
    RM_INFO("Initializing renderer");

    {
        sg_desc desc = {};

        desc.environment = sglue_environment();
        desc.logger.func = logger;

        sg_setup(&desc);

        if (!sg_isvalid()) {
            RM_FATAL("Failed to setup Sokol GFX");
            return;
        }
    }

    {
        sgl_desc_t desc = {};
        desc.logger.func = logger;
        sgl_setup(&desc);
    }

    {
        sg_environment_defaults const defaults = sg_query_desc().environment.defaults;
        sg_pipeline_desc desc = {};

        desc.depth.pixel_format = defaults.depth_format;
        desc.colors[0].pixel_format = defaults.color_format;
        desc.colors[0].blend.enabled = true;
        desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
        desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.colors[0].blend.op_rgb = SG_BLENDOP_ADD;
        desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
        desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.colors[0].blend.op_alpha = SG_BLENDOP_ADD;
        desc.color_count = 1;
        desc.sample_count = defaults.sample_count;

        s_defaultPipeline = sgl_make_pipeline(&desc);
    }

    {
        sg_pipeline_desc desc = {};

        desc.depth.pixel_format = SG_PIXELFORMAT_NONE;
        desc.colors[0].pixel_format = SG_PIXELFORMAT_RGBA8;
        desc.colors[0].blend.enabled = true;
        desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
        desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.colors[0].blend.op_rgb = SG_BLENDOP_ADD;
        desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
        desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.colors[0].blend.op_alpha = SG_BLENDOP_ADD;
        desc.color_count = 1;
        desc.sample_count = 1;

        s_rtPipeline = sgl_make_pipeline(&desc);
    }

    {
        sg_sampler_desc desc = {};

        desc.min_filter = desc.mag_filter = SG_FILTER_NEAREST;
        desc.wrap_u = desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
        s_samplerNearest = sg_make_sampler(&desc);

        if (sg_query_sampler_state(s_samplerNearest) != SG_RESOURCESTATE_VALID) {
            RM_FATAL("Failed to create the nearest sampler");
        }
    }

    {
        sg_sampler_desc desc = {};

        desc.min_filter = desc.mag_filter = SG_FILTER_LINEAR;
        desc.wrap_u = desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
        s_samplerLinear = sg_make_sampler(&desc);

        if (sg_query_sampler_state(s_samplerLinear) != SG_RESOURCESTATE_VALID) {
            RM_FATAL("Failed to create the linear sampler");
        }
    }
}

This is the code to begin and end passes:

void rm::renderer::beginPass(RenderTarget const* target) {
    if (target == nullptr) {
        sgl_set_context(SGL_DEFAULT_CONTEXT);
        sgl_defaults();
        sgl_load_pipeline(s_defaultPipeline);
        sgl_ortho(0.0f, (float)s_size.width(), (float)s_size.height(), 0.0f, -1.0f, 1.0f);
    }
    else {
        auto const t = (SokolRenderTarget const*)target;
        sgl_set_context(t->_context);
        sgl_defaults();
        sgl_load_pipeline(s_rtPipeline);
        sgl_ortho(0.0f, (float)target->size().width(), (float)target->size().height(), 0.0f, -1.0f, 1.0f);
    }
}

void rm::renderer::endPass(RenderTarget const* target) {
    sg_pass pass = {};
    pass.action.colors[0].load_action = SG_LOADACTION_CLEAR;
    pass.action.colors[0].clear_value = { 0.0f, 0.0f, 0.0f, 1.0f };

    if (target == nullptr) {
        pass.swapchain = sglue_swapchain();

        sg_begin_pass(&pass);
        sgl_context_draw(SGL_DEFAULT_CONTEXT);
        sg_end_pass();
    }
    else {
        auto const t = (SokolRenderTarget const*)target;
        pass.attachments = t->_attachments;

        sg_begin_pass(&pass);
        sgl_context_draw(t->_context);
        sg_end_pass();
    }
}

This is how I draw solid quads, textured quads, and quads textured with render targets:

void rm::renderer::draw(rm::Quad const& quad, Post post) {
    sg_image image = { SG_INVALID_ID };
    draw(quad, image, rm::renderer::Filter::Nearest, post, false);
}

void rm::renderer::draw(rm::Quad const& quad, rm::renderer::Texture const* const texture, Filter const filter, Post const post) {
    auto t = (SokolTexture const* const)texture;
    draw(quad, t->_texture, filter, post, false);
}

void rm::renderer::draw(rm::Quad const& quad, rm::renderer::RenderTarget const* const target, Filter const filter, Post const post) {
    auto t = (SokolRenderTarget const* const)target;
    sg_features const features = sg_query_features();
    bool const flip_y = !features.origin_top_left;
    draw(quad, t->_target, filter, post, flip_y);
}

And this is the actual draw code:

static void draw(rm::Quad const& quad, sg_image image, rm::renderer::Filter filter, rm::renderer::Post post, bool flip_y) {
    sg_sampler sampler = (filter == rm::renderer::Filter::Linear) ? s_samplerLinear : s_samplerNearest;
    sgl_enable_texture();
    sgl_texture(image, sampler);

    sgl_begin_quads();

    {
        rm::Point       const& position = quad.position();
        rm::Point       const& center   = quad.center();
        rm::Vec2        const& scale    = quad.scale();
        float           const  angle    = -quad.angle();
        rm::Color       const& color    = quad.color();
        rm::Size<float> const& size     = quad.size();

        rm::Point const screencenter(position.x() + center.x(), position.y() + center.y());

        rm::renderer::resetTransforms();
        rm::renderer::scaleAt(scale.x(), scale.y(), center.x(), center.y());
        rm::renderer::rotateAt(angle, center.x(), center.y());

        rm::Point tl = quad.position();
        rm::Point tr(tl.x() + size.width(), tl.y());
        rm::Point bl(tl.x(), tl.y() + size.height());
        rm::Point br(tl.x() + size.width(), tl.y() + size.height());

        tl = rm::renderer::transform(tl);
        tr = rm::renderer::transform(tr);
        bl = rm::renderer::transform(bl);
        br = rm::renderer::transform(br);

        float const r = color.r();
        float const g = color.g();
        float const b = color.b();
        float const a = color.a();

        if (flip_y) {
            sgl_v2f_t2f_c4f(tl.x(), tl.y(), 0.0f, 1.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(tr.x(), tr.y(), 1.0f, 1.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(br.x(), br.y(), 1.0f, 0.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(bl.x(), bl.y(), 0.0f, 0.0f, r, g, b, a);
        }
        else {
            sgl_v2f_t2f_c4f(tl.x(), tl.y(), 0.0f, 0.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(tr.x(), tr.y(), 1.0f, 0.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(br.x(), br.y(), 1.0f, 1.0f, r, g, b, a);
            sgl_v2f_t2f_c4f(bl.x(), bl.y(), 0.0f, 1.0f, r, g, b, a);
        }
    }

    sgl_end();
}
floooh commented 3 weeks ago

Sorry I'm quite distracted with other things ATM. From staring at the code I'm not seeing anything obviously wrong, but this sort of visual remote debugging is also quite hard :)

It might help to set a breakpoint at the sokol_gfx.h validation code line that's printed in the validation errors and then check in the debugger call stack where exactly that error is happening in your code, maybe that pinpoints the problem.

leiradel commented 3 weeks ago

Sure, I appreciate you're doing this during your free time.

I'm not getting any validation errors or any other messages from Sokol, everything just works except there's no alpha blending, my sprites have a black background.

I'll try to write a minimum program that uses sokol_gl this weekend and that reproduces the issue. If it works, I'll probably get enough insight to fix the issue myself, otherwise you'll have a program that you can build and try yourself, time permitting of course.

Thanks for all the help so far!

leiradel commented 3 weeks ago

Oh well, my minimum test program works. I'll add a render target to it and see if it still works.

test_H4Vnl3SvQP

leiradel commented 3 weeks ago

When I try to render the above images to a render target, I get these errors:

[error] ./sokol_gfx.h:16783: VALIDATE_APIP_COLOR_FORMAT: sg_apply_pipeline: pipeline color attachment pixel format doesn't match pass color attachment pixel format
[error] ./sokol_gfx.h:16790: VALIDATE_APIP_DEPTH_FORMAT: sg_apply_pipeline: pipeline depth pixel_format doesn't match pass depth attachment pixel format
[fatal] ./sokol_gfx.h:16168: VALIDATION_FAILED: validation layer checks failed
[error] ./sokol_gfx.h:16814: VALIDATE_ABND_PIPELINE: sg_apply_bindings: must be called after sg_apply_pipeline
[error] ./sokol_gfx.h:16816: VALIDATE_ABND_PIPELINE_EXISTS: sg_apply_bindings: currently applied pipeline object no longer alive
[fatal] ./sokol_gfx.h:16168: VALIDATION_FAILED: validation layer checks failed
[error] ./sokol_gfx.h:17021: VALIDATE_AUB_NO_PIPELINE: sg_apply_uniforms: must be called after sg_apply_pipeline()
Assertion failed: pip && (pip->slot.id == _sg.cur_pipeline.id), file ./sokol_gfx.h, line 17023

If I comment out the code to render to the render target, but render the render target to the display, it works (but I get a black screen ofc). If I render the images directly to the display, it works.

I'm not sure what isn't matching that is causing the error messages... The program is here, in case you have some time to give it a try.

sprite.zip

I'm building it in a Clang MSYS2 prompt with clang -O3 -std=c11 -o test main.c -lgdi32 -lkernel32 -luser32 -ldxgi -ld3d11 -ldinput8 -ldxguid.

I'll continue trying random stuff on my end and see if I can make it work.

floooh commented 3 weeks ago

The validation errors about sg_apply_bindings and sg_apply_uniforms not being called at the right time might be the actual source of a lot of followup problems.

Is this still purely sokol_gl.h code? Because this type of errors shouldn't happen inside sokol_gl.h, hmm...

I'm currently still a bit overloaded with other stuff, just keep pining this thread from time to time, and I'll find some time to check your example ;)

floooh commented 3 weeks ago

Ok, on macOS I can build with clang -std=c11 -ObjC -o test main.c -framework Cocoa -framework Metal -framework MetalKit -framework Quartz (after changing SOKOL_D3D11 to SOKOL_METAL) and get the same validation errors.

I'll try to have a quick look in the debugger...

PS: or rather clang -std=c11 -ObjC -o test main.c -framework Cocoa -framework Metal -framework MetalKit -framework Quartz to get a debuggable executable...

floooh commented 3 weeks ago

Aaah, ok, you stumbled over a little detail which I also forgot about...

I found two issues:

The validation errors are because of an admittedly very obscure behavior of sgl_make_pipeline(). It will ignore the incoming pixel formats and instead patch them with the pixel formats from the current sokol-gl context :/ I have to admit that this is a pretty stupid decision in the sokol-gl API design (I'm not happy with those 'render context' in any of the sokol utility headers and need to come up with something better...).

Long story short, the validation errors can be fixed like this:

        sg_pipeline_desc desc = {0};

        desc.colors[0].blend.enabled = true;
        desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
        desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
        desc.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
        desc.color_count = 1;
        s_rtPipeline = sgl_context_make_pipeline(s_context, &desc)

...I also removed some other items which are default-initialized to the same value anyway.

...this fixes all validation errors, but still doesn't render anything. But this is simply because you forgot to call endDefaultPass() before sg_commit() (this should probably also be a sokol-gfx validation error... or at least an assert...).

With those two fixes I get the following output on Metal (don't know yet why the output is only in the top-right corner, the sgl_ortho() calls look right to me):

Screenshot 2024-10-27 at 13 34 14

This is the main.c file with my fixes:

main.c.zip

Btw, feedback like this is very helpful for me to find gaps and weird things which need at least better error checking :)

floooh commented 3 weeks ago

PS: you'll need to change the SOKOL_METAL define at the top back to SOKOL_D3D11 (I forgot that).

leiradel commented 3 weeks ago

Woot it works!

Thanks so much, I've made the changes directly in my engine instead of the sample program I wrote and the alpha blending is working just fine. Thanks!

Just one question before closing this issue: is it ok to create one render target context to use with all render targets? Does it create any issue with the rendering order?

Thanks again!

leiradel commented 3 weeks ago

Clarification: I was creating one sgl context per render target, but since it must be created before the pipeline, I'm creating only one and using it for all render targets.

My application only has one render target for now, but could have more, hence the question.

floooh commented 3 weeks ago

You should create a separate sgl context for each render target, and only do a single sgl_draw() per context and frame (this is because sgl_draw() will render everything that has been recorded in a specific context in one frame, so doing this multiple times you will end up with rendering the same things multiple times).

The recorded rendering commands in each sgl context are reset at the end of a frame in the sg_commit() sokol-gfx call.

leiradel commented 3 weeks ago

Ok, I'll have to re-design this part a bit as it's possible to create render targets at will right now. Maybe I'll create a bunch of context at startup and limit the number of simultaneous render targets in use.

Nice, thanks again for all the help!