Skip to content

External Surface GLFW

This example embeds a Datoviz view inside a host-owned GLFW surface.

Preview

Run And Adapt

Commands below assume a Datoviz source checkout and start at the repository root. Use your configured build environment; Python routes additionally require local bindings.

Route Availability Command or action
C Canonical native source just example-c advanced/external_surface_glfw (build and run), or rerun ./build/examples/c/advanced/external_surface_glfw
Python No verified adaptation on this page Start from the C source.
Browser Native only external Vulkan surfaces are native host-integration objects

Use this example as capability or integration evidence, not as a minimal copy-paste template. Start from the nearest supported, copy-safe example and add this feature after verifying the linked API reference.

What To Look For

GLFW is only a surrogate for an external toolkit. The host asks Datoviz for a Vulkan instance, creates the VkSurfaceKHR itself, forwards framebuffer size, content scale, pointer, wheel, and key events, and calls dvz_view_render_once() from its own loop. The scene is deliberately small: three points with panzoom interaction make it easy to verify that hosted input and resize metadata reach Datoviz.

This workflow is useful when a scientific application already owns its native event loop and wants Datoviz to render into a provided Vulkan surface.

Source

/*
 * Copyright (c) 2021 Cyrille Rossant and contributors. All rights reserved.
 * Licensed under the MIT license. See LICENSE file in the project root for details.
 * SPDX-License-Identifier: MIT
 */

/* external_surface_glfw - This example embeds a Datoviz view inside a host-owned GLFW surface.
 *
 * Scenario: advanced_external_surface_glfw
 * Style: advanced, native-only, external window/surface host
 *
 * What to look for: GLFW is only a surrogate for an external toolkit. The host asks Datoviz for a
 * Vulkan instance, creates the VkSurfaceKHR itself, forwards framebuffer size, content scale,
 * pointer, wheel, and key events, and calls dvz_view_render_once() from its own loop. The scene is
 * deliberately small: three points with panzoom interaction make it easy to verify that hosted
 * input and resize metadata reach Datoviz.
 *
 * This workflow is useful when a scientific application already owns its native event loop and
 * wants Datoviz to render into a provided Vulkan surface.
 *
 * Build:  just example-c advanced/external_surface_glfw
 * Run:    ./build/examples/c/advanced/external_surface_glfw [frames]
 */

#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#include "datoviz/app.h"
#include "datoviz/app_interop.h"
#include "datoviz/canvas.h"
#include "datoviz/input/pointer.h"
#include "datoviz/scene.h"
#include "datoviz/window/backend.h"
#include "example_style.h"



typedef struct
{
    DvzView* view;
    bool repaint_requested;
    uint32_t request_count;
} HostedGlfwState;



/**
 * Mark that Datoviz requested a host repaint.
 *
 * @param win Datoviz view requesting a frame
 * @param user_data hosted GLFW state
 */
static void _request_frame_callback(DvzView* win, void* user_data)
{
    (void)win;
    HostedGlfwState* state = (HostedGlfwState*)user_data;
    if (state == NULL)
        return;
    state->repaint_requested = true;
    state->request_count++;
}



/**
 * Emit the current host framebuffer/logical size to Datoviz.
 *
 * @param window GLFW window owned by the host
 * @param view Datoviz hosted view
 */
static void _emit_resize(GLFWwindow* window, DvzView* view)
{
    if (window == NULL || view == NULL)
        return;

    int fb_width = 0;
    int fb_height = 0;
    int win_width = 0;
    int win_height = 0;
    float scale_x = 1.0f;
    float scale_y = 1.0f;
    glfwGetFramebufferSize(window, &fb_width, &fb_height);
    glfwGetWindowSize(window, &win_width, &win_height);
    glfwGetWindowContentScale(window, &scale_x, &scale_y);
    if (scale_x <= 0.0f)
        scale_x = 1.0f;
    if (scale_y <= 0.0f)
        scale_y = 1.0f;

    (void)dvz_view_emit_resize(
        view, fb_width > 0 ? (uint32_t)fb_width : 0,
        fb_height > 0 ? (uint32_t)fb_height : 0, win_width > 0 ? (uint32_t)win_width : 0,
        win_height > 0 ? (uint32_t)win_height : 0, scale_x, scale_y);
}



/**
 * Return the hosted Datoviz view associated with a GLFW host window.
 *
 * @param window GLFW window owned by the host
 * @return hosted view, or NULL when unavailable
 */
static DvzView* _hosted_view(GLFWwindow* window)
{
    if (window == NULL)
        return NULL;
    HostedGlfwState* state = (HostedGlfwState*)glfwGetWindowUserPointer(window);
    return state != NULL ? state->view : NULL;
}



/**
 * Forward GLFW cursor movement to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param xpos pointer x position
 * @param ypos pointer y position
 */
static void _cursor_pos_callback(GLFWwindow* window, double xpos, double ypos)
{
    DvzView* view = _hosted_view(window);
    if (view == NULL)
        return;
    int win_width = 0;
    int win_height = 0;
    glfwGetWindowSize(window, &win_width, &win_height);
    (void)dvz_view_emit_pointer(
        view, DVZ_POINTER_EVENT_MOVE, (float)xpos, (float)ypos, (float)win_width,
        (float)win_height, DVZ_POINTER_BUTTON_NONE, 0);
}



/**
 * Forward GLFW mouse-button events to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param button GLFW mouse button
 * @param action GLFW button action
 * @param mods keyboard modifier bit mask
 */
static void _mouse_button_callback(GLFWwindow* window, int button, int action, int mods)
{
    DvzView* view = _hosted_view(window);
    if (view == NULL)
        return;
    double xpos = 0.0;
    double ypos = 0.0;
    int win_width = 0;
    int win_height = 0;
    glfwGetCursorPos(window, &xpos, &ypos);
    glfwGetWindowSize(window, &win_width, &win_height);
    DvzPointerEventType type =
        action == GLFW_PRESS ? DVZ_POINTER_EVENT_PRESS : DVZ_POINTER_EVENT_RELEASE;
    (void)dvz_view_emit_pointer(
        view, type, (float)xpos, (float)ypos, (float)win_width, (float)win_height,
        dvz_pointer_button_from_glfw(button), mods);
}



/**
 * Forward GLFW scroll events to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param dx horizontal wheel delta
 * @param dy vertical wheel delta
 */
static void _scroll_callback(GLFWwindow* window, double dx, double dy)
{
    DvzView* view = _hosted_view(window);
    if (view == NULL)
        return;
    double xpos = 0.0;
    double ypos = 0.0;
    int win_width = 0;
    int win_height = 0;
    glfwGetCursorPos(window, &xpos, &ypos);
    glfwGetWindowSize(window, &win_width, &win_height);
#if defined(__APPLE__)
    dy = -dy;
#endif
    (void)dvz_view_emit_wheel(
        view, (float)xpos, (float)ypos, (float)win_width, (float)win_height, (float)dx,
        (float)dy, 0);
}



/**
 * Forward GLFW key events to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param key GLFW key code
 * @param scancode platform scancode
 * @param action GLFW key action
 * @param mods keyboard modifier bit mask
 */
static void _key_callback(GLFWwindow* window, int key, int scancode, int action, int mods)
{
    (void)scancode;
    DvzView* view = _hosted_view(window);
    if (view == NULL)
        return;
    DvzKeyboardEventType type = DVZ_KEYBOARD_EVENT_NONE;
    if (action == GLFW_PRESS)
        type = DVZ_KEYBOARD_EVENT_PRESS;
    else if (action == GLFW_REPEAT)
        type = DVZ_KEYBOARD_EVENT_REPEAT;
    else if (action == GLFW_RELEASE)
        type = DVZ_KEYBOARD_EVENT_RELEASE;
    if (type != DVZ_KEYBOARD_EVENT_NONE)
        (void)dvz_view_emit_key(view, type, (DvzKeyCode)key, mods);
}



/**
 * Forward GLFW framebuffer resize events to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param width framebuffer width
 * @param height framebuffer height
 */
static void _framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    (void)width;
    (void)height;
    _emit_resize(window, _hosted_view(window));
}



/**
 * Forward GLFW content-scale changes to Datoviz through the hosted input API.
 *
 * @param window GLFW window owned by the host
 * @param scale_x horizontal content scale
 * @param scale_y vertical content scale
 */
static void _content_scale_callback(GLFWwindow* window, float scale_x, float scale_y)
{
    (void)scale_x;
    (void)scale_y;
    _emit_resize(window, _hosted_view(window));
}



static DvzWindowExternalSurfaceInfo
_surface_info(GLFWwindow* window, VkInstance instance, VkSurfaceKHR surface, bool owned_by_datoviz)
{
    int fb_width = 0;
    int fb_height = 0;
    glfwGetFramebufferSize(window, &fb_width, &fb_height);

    float scale_x = 1.0f;
    float scale_y = 1.0f;
    glfwGetWindowContentScale(window, &scale_x, &scale_y);
    if (scale_x <= 0.0f)
        scale_x = 1.0f;
    if (scale_y <= 0.0f)
        scale_y = 1.0f;

    DvzWindowExternalSurfaceInfo info = {
        DVZ_STRUCT_INIT_FIELDS(DvzWindowExternalSurfaceInfo),
        .instance = instance,
        .surface = surface,
        .extent = {
            .width = fb_width > 0 ? (uint32_t)fb_width : 0,
            .height = fb_height > 0 ? (uint32_t)fb_height : 0,
        },
        .scale_x = scale_x,
        .scale_y = scale_y,
        .owned_by_datoviz = owned_by_datoviz,
    };
    return info;
}



static DvzScene* _make_scene(DvzFigure** out_figure, DvzPanel** out_panel)
{
    DvzScene* scene = dvz_scene();
    if (scene == NULL)
        return NULL;

    DvzFigure* figure = dvz_figure(scene, EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT, 0);
    if (figure == NULL)
    {
        dvz_scene_destroy(scene);
        return NULL;
    }

    DvzPanel* panel = dvz_panel_full(figure);
    DvzVisual* visual = panel != NULL ? dvz_point(scene, 0) : NULL;
    if (panel == NULL || visual == NULL)
    {
        dvz_scene_destroy(scene);
        return NULL;
    }

    vec3 positions[3] = {
        {-0.5f, -0.5f, 0.0f},
        { 0.5f, -0.5f, 0.0f},
        { 0.0f,  0.5f, 0.0f},
    };
    DvzColor colors[3] = {
        {255,   0,   0, 255},
        {  0, 255,   0, 255},
        {  0,   0, 255, 255},
    };
    float sizes[3] = {24.0f, 24.0f, 24.0f};

    DvzVisualDataUpdate updates[] = {
        {.attr_name = "position", .data = positions, .item_count = 3},
        {.attr_name = "color", .data = colors, .item_count = 3},
        {.attr_name = "diameter_px", .data = sizes, .item_count = 3},
    };
    int rc = dvz_visual_set_data_many(visual, updates, 3);
    if (rc != 0)
    {
        dvz_scene_destroy(scene);
        return NULL;
    }
    rc = dvz_panel_add_visual(panel, visual, NULL);
    if (rc != 0)
    {
        dvz_scene_destroy(scene);
        return NULL;
    }
    dvz_panel_set_background_color(panel, dvz_color_from_unit(0.08f, 0.10f, 0.14f, 1.0f));

    if (out_figure != NULL)
        *out_figure = figure;
    if (out_panel != NULL)
        *out_panel = panel;
    return scene;
}



int main(int argc, char** argv)
{
    uint32_t max_frames = 120;
    if (argc > 1)
        max_frames = (uint32_t)strtoul(argv[1], NULL, 10);

    if (!dvz_window_glfw_init())
    {
        fprintf(stderr, "external_surface_glfw: skipped, GLFW could not initialize\n");
        return 0;
    }

    uint32_t extension_count = 0;
    const char** extensions = glfwGetRequiredInstanceExtensions(&extension_count);
    if (extension_count == 0 || extensions == NULL)
    {
        fprintf(stderr, "external_surface_glfw: skipped, GLFW returned no Vulkan extensions\n");
        glfwTerminate();
        return 0;
    }

    DvzFigure* figure = NULL;
    DvzPanel* panel = NULL;
    DvzScene* scene = _make_scene(&figure, &panel);
    if (scene == NULL || figure == NULL || panel == NULL)
    {
        fprintf(stderr, "external_surface_glfw: failed to create scene\n");
        glfwTerminate();
        return 1;
    }

    DvzAppConfig app_cfg = dvz_app_config();
    app_cfg.instance_extension_count = extension_count;
    app_cfg.instance_extensions = extensions;
    app_cfg.enable_canvas_extensions = true;
    app_cfg.enable_glfw_extensions = false;
    DvzApp* app = dvz_app_with_config(scene, &app_cfg);
    if (app == NULL)
    {
        fprintf(stderr, "external_surface_glfw: skipped, Datoviz GPU context creation failed\n");
        dvz_scene_destroy(scene);
        glfwTerminate();
        return 0;
    }

    VkInstance instance = dvz_app_vk_instance(app);
    if (instance == VK_NULL_HANDLE)
    {
        fprintf(stderr, "external_surface_glfw: Datoviz returned no Vulkan instance\n");
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        glfwTerminate();
        return 1;
    }

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
    glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
    GLFWwindow* window = glfwCreateWindow(
        EXAMPLE_WINDOW_WIDTH, EXAMPLE_WINDOW_HEIGHT, "external_surface_glfw", NULL, NULL);
    if (window == NULL)
    {
        fprintf(stderr, "external_surface_glfw: skipped, GLFW window creation failed\n");
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        glfwTerminate();
        return 0;
    }

    VkSurfaceKHR surface = VK_NULL_HANDLE;
    VkResult surface_res = glfwCreateWindowSurface(instance, window, NULL, &surface);
    if (surface_res != VK_SUCCESS || surface == VK_NULL_HANDLE)
    {
        fprintf(
            stderr, "external_surface_glfw: skipped, surface creation failed (%d)\n",
            (int)surface_res);
        glfwDestroyWindow(window);
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        glfwTerminate();
        return 0;
    }

    DvzWindowExternalSurfaceInfo info = _surface_info(window, instance, surface, false);
    DvzView* view = dvz_view_external_surface(app, figure, &info);
    if (view == NULL)
    {
        fprintf(stderr, "external_surface_glfw: hosted view creation failed\n");
        vkDestroySurfaceKHR(instance, surface, NULL);
        glfwDestroyWindow(window);
        dvz_app_destroy(app);
        dvz_scene_destroy(scene);
        glfwTerminate();
        return 1;
    }

    HostedGlfwState host_state = {.view = view};
    glfwSetWindowUserPointer(window, &host_state);
    dvz_view_set_request_frame_callback(view, _request_frame_callback, &host_state);
    glfwSetCursorPosCallback(window, _cursor_pos_callback);
    glfwSetMouseButtonCallback(window, _mouse_button_callback);
    glfwSetScrollCallback(window, _scroll_callback);
    glfwSetKeyCallback(window, _key_callback);
    glfwSetFramebufferSizeCallback(window, _framebuffer_size_callback);
    glfwSetWindowContentScaleCallback(window, _content_scale_callback);
    _emit_resize(window, view);
    DvzPanzoom* panzoom = dvz_view_panzoom(view, panel, NULL);
    if (panzoom == NULL)
    {
        fprintf(stderr, "failed to create or bind panzoom controller\n");
        dvz_app_destroy(app);
        vkDestroySurfaceKHR(instance, surface, NULL);
        dvz_scene_destroy(scene);
        glfwDestroyWindow(window);
        glfwTerminate();
        return 1;
    }

    uint32_t frame = 0;
    while (!glfwWindowShouldClose(window) && (max_frames == 0 || frame < max_frames))
    {
        if (max_frames == 0 && !host_state.repaint_requested)
            glfwWaitEventsTimeout(0.1);
        else
            glfwPollEvents();

        info = _surface_info(window, instance, surface, false);
        if (dvz_view_update_external_surface(view, &info) != 0)
        {
            fprintf(stderr, "external_surface_glfw: surface update failed\n");
            break;
        }

        if (max_frames == 0 && !host_state.repaint_requested)
            continue;
        host_state.repaint_requested = false;
        int rc = dvz_view_render_once(view);
        if (rc < 0)
        {
            fprintf(stderr, "external_surface_glfw: render failed (%d)\n", rc);
            break;
        }
        if (rc == DVZ_CANVAS_FRAME_READY)
            frame++;
    }

    info = _surface_info(window, instance, surface, true);
    if (dvz_view_update_external_surface(view, &info) == 0)
        surface = VK_NULL_HANDLE;

    dvz_app_destroy(app);
    if (surface != VK_NULL_HANDLE)
        vkDestroySurfaceKHR(instance, surface, NULL);
    glfwDestroyWindow(window);
    dvz_scene_destroy(scene);
    glfwTerminate();

    printf(
        "external_surface_glfw: rendered %u frame(s), %u request(s)\n", frame,
        host_state.request_count);
    return 0;
}
Example details
  • ID: advanced_external_surface_glfw
  • Category: advanced
  • Lane: advanced
  • Status: experimental
  • Source: examples/c/advanced/external_surface_glfw.c
  • Approved adaptation starter: no
  • Browser support: Native only
  • Browser note: external Vulkan surfaces are native host-integration objects
  • Browser capability tags: native-view, external-surface, glfw
  • Validation: native-smoke

Tags

advanced, external-surface, glfw, host-integration

Data

Field Value
kind synthetic