floooh / sokol

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

[question] sokol-gfx: Advice on how to implement a simple 2D renderer in sokol. #925

Closed roig closed 7 months ago

roig commented 8 months ago

Hello, first of all thank you for making this library, it's awesome!

I'm porting a codebase to sokol and adding some features while doing it. The 2d renderer looks like this:

    init();
    while (true) {
        submitLine(u64: entity_id, f32 depth, ltw: Mat3, points: []Vec2, color: Color, ... other params);
        submitLine(u64: entity_id, f32 depth, ltw: Mat3, points: []Vec2, color: Color, ... other params);
        submitRect(u64: entity_id, f32 depth, ltw: Mat3, color: Color, ... other params);
        submitSprite(u64: entity_id, f32 depth, ltw: Mat3, ... other params);
        drawPass(camera_ltw: Mat3, camera_projection: Mat3, pass: ?sg.sg_pass, pass_action: ?sg.sg_pass_action);
        drawPass(camera_ltw: Mat3, camera_projection: Mat3, pass: ?sg.sg_pass, pass_action: ?sg.sg_pass_action);

        flush(); // deletes all the submitted primitives
        submitSprite(u64: entity_id, f32 depth, ltw: Mat3, ... other params);
        submitSprite(u64: entity_id, f32 depth, ltw: Mat3, ... other params);
        drawPass(camera_ltw: Mat3, camera_projection: Mat3, pass: ?sg.sg_pass, pass_action: ?sg.sg_pass_action);
        endFrame(); // calls flush() and calls sg.sg_commit();
    }
    shutdown();
  1. submitX functions: just adds the primitives to a list
  2. drawPass function: Orders and batch primitives list and renders it using the camera parameters passed. Vertex positions are CPU computed.
  3. flush function: clears the primitives list to start again
  4. endFrame: function: calls flush and sg.sg_commit

Very simple I know. But I have some question about the internals of this.

Thank you!

floooh commented 8 months ago

Currently I'm using a big VBO where I append the primitives data after batching and ordering by Z. The VBO is cleared when flush is called. Do you think this approach is correct?

API looks good, but I would suggest accumulating all vertices for one frame in an intermediate memory buffer (instead of appending directly to a sokol-gfx vertex buffer) and then copy this in one go using a single sg_update_buffer(), and do this every frame.

sg_append_buffer() unfortunately has known performance problems in the GL backend and is not really recommended for this use case.

The following headers use that approach, and it's the most efficient across all platforms

...where sokol_gl.h is probably the closest to your use case.

The vertex buffer is created here (note SG_USAGE_STREAM):

https://github.com/floooh/sokol/blob/9e0f1b4e550998127c8f884ff7cc63838cf61860/util/sokol_gl.h#L2829-L2836

...and various regular memory buffers are allocated here (for vertices, uniforms and 'render commands':

https://github.com/floooh/sokol/blob/9e0f1b4e550998127c8f884ff7cc63838cf61860/util/sokol_gl.h#L2819-L2824

...all sokol_gl.h functions except sgl_draw() will only write to those memory buffers and can be called outside sokol-gfx render passes.

Then in sgl_draw() (which must be called once per frame inside a sokol-gfx render pass):

First all the accumulated vertices for this frame are copied into the vertex buffer in one go:

https://github.com/floooh/sokol/blob/9e0f1b4e550998127c8f884ff7cc63838cf61860/util/sokol_gl.h#L3350-L3351

...and then the accumulated draw commands are "processed":

https://github.com/floooh/sokol/blob/9e0f1b4e550998127c8f884ff7cc63838cf61860/util/sokol_gl.h#L3354-L3402

A feature I want to add is to render the entity_id to another texture in order to have pixel perfect picking, is it possible to have 2 outputs in that default shader, one for the fragment color and one for the entity_id encoded? In order to avoid switching uniforms, the entity id would be in the vertex data casted from u32 -> f32: Vertex layout: []f32 : x, y, r, g, b, a, u, v, entity_id

...hmm, I never implemented such a picking mechanism, but if you need to multiple fragement shader outputs, you would use "multiple-render-targets" rendering, here's a sample:

https://floooh.github.io/sokol-html5/mrt-pixelformats-sapp.html

This creates a render pass for offscreen rendering with 3 color attachment images:

https://github.com/floooh/sokol-samples/blob/3e9a5a21e886f5f62cb7d26d852b2c541f1a1782/sapp/mrt-pixelformats-sapp.c#L92-L100

...and the fragment shader has 3 outputs:

https://github.com/floooh/sokol-samples/blob/3e9a5a21e886f5f62cb7d26d852b2c541f1a1782/sapp/mrt-pixelformats-sapp.glsl#L36-L44

PS: the pipeline object also needs to know about those 3 render targets:

https://github.com/floooh/sokol-samples/blob/3e9a5a21e886f5f62cb7d26d852b2c541f1a1782/sapp/mrt-pixelformats-sapp.c#L141-L146

Hope this helps!

PS: before investigating too much work in your picking code please be aware that you cannot read back information from a sokol-gfx texture to the CPU side currently. If you need this, you'll need to inject a backend-specific texture object yourself and mix sokol-gfx and native backend-API calls.

So far I tried to avoid pixel picking and used geometry picking on the CPU instead (point-in-triangle-tests on preprocessed triangle data, for instance here: https://floooh.github.io/visual6502remix/)

roig commented 8 months ago

Wow thank you for all the feedback and instructions!

I implemented the picking part following your instructions and using a pool of PBOs (to minimize gpu stall) to retrieve the pixel image data and it's working!! And as I'm only querying the pixel where the mouse is, seems really fast.

Now I want to organize everything in order to use "commands" internally.

Thank you for your help and this awesome library.