Skip to content

How to use the canvas C API

This example shows how to write a standalone C app using only the canvas API, not the scene API. We'll render a triangle without using existing graphics, visuals, panels, and so on. We'll follow these steps:

  • Creating a graphics with custom shaders,
  • Creating a function callback for command buffer refill,
  • Creating a vertex buffer manually.

// code from `examples/standalone/standalone_canvas.c`:

/*************************************************************************************************/
/*  Example of a standalone application using the vklite API in a canvas.                        */
/*  This script opens a live canvas with a triangle rendered on the GPU.                         */
/*************************************************************************************************/

/// Import the library public header.
#include <datoviz/datoviz.h>

// Objects we'll need in the refill callback.
// NOTE: Using static global variables in production code is bad practice.
static DvzBufferRegions vertex_buffer;
static DvzGraphics graphics;
static DvzBindings bindings;

// Refill callback. This function is called by the canvas whenever it needs to recreate its command
// buffers, for example when initializing the canvas, and when resizing it.
static void _triangle_refill(DvzCanvas* canvas, DvzEvent ev)
{
    ASSERT(canvas != NULL);

    // The callback is passed an event object with a list of command buffers to recreate. In
    // this particular example, we just use a single command buffer.
    ASSERT(ev.u.rf.cmd_count == 1);
    DvzCommands* cmds = ev.u.rf.cmds[0];
    // There is by default just one command buffer, linked to the render queue.
    ASSERT(cmds->queue_idx == DVZ_DEFAULT_QUEUE_RENDER);

    // This is the current swapchain image index for which we need to refill the command buffer.
    // There is one command buffer per swapchain image, so whenever a command buffer refill is
    // needed, this callback function is called three times for example, if using triple buffering.
    uint32_t idx = ev.u.rf.img_idx;

    // We begin recording the command buffer here.
    dvz_cmd_begin(cmds, idx);

    // We begin the default render pass.
    dvz_cmd_begin_renderpass(cmds, idx, &canvas->renderpass, &canvas->framebuffers);

    // We set the viewport to the entire framebuffer size.
    dvz_cmd_viewport(cmds, idx, canvas->viewport.viewport);

    // We bind the vertex buffer for the upcoming drawing command.
    dvz_cmd_bind_vertex_buffer(cmds, idx, vertex_buffer, 0);

    // We bind the graphics pipeline.
    dvz_cmd_bind_graphics(cmds, idx, &graphics, &bindings, 0);

    // We render 3 vertices (1 triangle).
    dvz_cmd_draw(cmds, idx, 0, 3);

    // End of the render pass and command buffer.
    dvz_cmd_end_renderpass(cmds, idx);
    dvz_cmd_end(cmds, idx);
}

// Entry point.
int main(int argc, char** argv)
{
    // We create a singleton application with a GLFW backend.
    DvzApp* app = dvz_app(DVZ_BACKEND_GLFW);

    // We use the first detected GPU. The last argument is the GPU index.
    DvzGpu* gpu = dvz_gpu_best(app);

    // We create a new canvas with the size specified. The last argument is for optional flags.
    DvzCanvas* canvas = dvz_canvas(gpu, 1024, 768, 0);

    // Graphics pipeline.
    {
        // We create a new graphics pipeline.
        graphics = dvz_graphics(gpu);

        // We set the renderpass.
        dvz_graphics_renderpass(&graphics, &canvas->renderpass, 0);

        // We specify the primitive.
        dvz_graphics_topology(&graphics, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST);

        // We set the shaders.
        dvz_graphics_shader(&graphics, VK_SHADER_STAGE_VERTEX_BIT, "triangle.vert.spv");
        dvz_graphics_shader(&graphics, VK_SHADER_STAGE_FRAGMENT_BIT, "triangle.frag.spv");

        // We specify the vertex structure size.
        dvz_graphics_vertex_binding(&graphics, 0, sizeof(DvzVertex));

        // We specify the two vertex attributes.
        dvz_graphics_vertex_attr(
            &graphics, 0, 0, VK_FORMAT_R32G32B32_SFLOAT, offsetof(DvzVertex, pos));
        dvz_graphics_vertex_attr(
            &graphics, 0, 1, VK_FORMAT_R8G8B8A8_UNORM, offsetof(DvzVertex, color));

        // Once we've set up the graphics pipeline, we create it.
        dvz_graphics_create(&graphics);
    }

    // We create the (empty) bindings: the shaders do not necessitate any uniform or
    // texture in this example.
    bindings = dvz_bindings(&graphics.slots, 1);
    dvz_bindings_update(&bindings);

    // Vertex data and GPU buffer.
    DvzBuffer buffer = dvz_buffer(gpu);
    {
        // We create the GPU buffer holding the vertex data.
        // NOTE: in real applications, once should use few, even a single large vertex buffer for
        // all graphics pipelines in the application. Defining many small GPU buffers is bad
        // practice.

        // There will be three vertices for 1 triangle.
        VkDeviceSize size = 3 * sizeof(DvzVertex);
        dvz_buffer_size(&buffer, size);

        // We declare that the buffer will be used as a vertex buffer.
        dvz_buffer_usage(&buffer, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT);

        // The buffer should be accessible directly from the CPU in this example (bad practice in
        // real applications).
        dvz_buffer_memory(
            &buffer, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);

        // Once set up, we create the GPU buffer.
        dvz_buffer_create(&buffer);

        // We define a view (buffer region) on the entire buffer.
        vertex_buffer = dvz_buffer_regions(&buffer, 1, 0, size, 0);

        // Define the vertex data.
        // NOTE: in this example, we don't include the common.glsl shader and we use raw Vulkan
        // shaders. So the Vulkan coordinate system is used, with the y axis going down.
        DvzVertex data[3] = {
            {{-1, +1, 0}, {255, 0, 0, 255}}, // bottom left, red
            {{+1, +1, 0}, {0, 255, 0, 255}}, // bottom right, green
            {{+0, -1, 0}, {0, 0, 255, 255}}, // top, blue
        };

        // We upload the data to the GPU vertex buffer.
        dvz_buffer_regions_upload(&vertex_buffer, 0, data);
    }

    // We set the command buffer refill callback, the function that will be called whenever the
    // canvas needs to refill its command buffers. The callback generates the GPU commands to draw
    // the triangle.
    dvz_event_callback(canvas, DVZ_EVENT_REFILL, 0, DVZ_EVENT_MODE_SYNC, _triangle_refill, NULL);

    // We run the application. The last argument is the number of frames to run, or 0 for infinite
    // loop (stop when escape is pressed or when the window is closed).
    dvz_app_run(app, 0);

    // We need to clean up all objects handled by Datoviz at the end.
    dvz_graphics_destroy(&graphics);
    dvz_buffer_destroy(&buffer);
    dvz_app_destroy(app);

    return 0;
}