Sokol Module

GX includes a Sokol module for cross-platform graphics and windowing. Sokol is a set of single-header C libraries providing a platform-agnostic graphics API that compiles to OpenGL, Direct3D 11, Metal, WebGL2, and WebGPU. Unlike raylib’s polling game loop, Sokol uses a callback-based architecture — you provide init, frame, cleanup, and event functions.

The module has four sub-modules:

ModuleHeaderPurpose
sokol.appsokol_app.hWindow creation, event loop, input
sokol.gfxsokol_gfx.hLow-level GPU rendering (buffers, shaders, pipelines)
sokol.gpsokol_gp.hHigh-level 2D drawing (rects, triangles, lines, textures)
sokol.gluesokol_glue.hBridges sokol.app and sokol.gfx

Prerequisites

The module ships with all Sokol headers bundled — no external dependencies needed. The C source is compiled automatically via @cfile. On Windows, links against opengl32, gdi32, user32, shell32 automatically.

Usage

import sokol.app     // window + events
import sokol.gfx     // graphics API
import sokol.glue    // app↔gfx bridge
import sokol.gp      // 2D drawing (optional)

Build with -I modules:

gx myapp.gx -I modules -o myapp.exe

Quick Example — Animated Shapes

import sokol.app
import sokol.gfx
import sokol.glue
import sokol.gp
import math.scalar

var g_time: f32 = 0.0

@c_abi fn init() {
    var gfx_desc: sg_desc
    gfx_desc.environment = sglue_environment()
    sg_setup(&gfx_desc)

    var gp_desc: sgp_desc
    sgp_setup(&gp_desc)
}

@c_abi fn frame() {
    var w = sapp_width()
    var h = sapp_height()
    g_time = g_time + 0.016

    sgp_begin(w, h)
    sgp_viewport(0, 0, w, h)
    sgp_project(0.0, 800.0, 0.0, 600.0)

    // Dark background
    sgp_set_color(0.1, 0.1, 0.15, 1.0)
    sgp_clear()

    // Pulsing red rectangle
    var pulse = sinf(g_time * 3.0) * 20.0
    sgp_set_color(0.9, 0.2, 0.2, 1.0)
    sgp_draw_filled_rect(100.0 - pulse, 100.0 - pulse, 200.0 + pulse * 2.0, 150.0 + pulse * 2.0)

    // Spinning triangle
    var cx = 500.0
    var cy = 300.0
    sgp_set_blend_mode(BLENDMODE_BLEND)
    sgp_set_color(0.2, 0.6, 0.9, 0.8)
    sgp_draw_filled_triangle(
        cx + cosf(g_time) * 80.0, cy + sinf(g_time) * 80.0,
        cx + cosf(g_time + 2.094) * 80.0, cy + sinf(g_time + 2.094) * 80.0,
        cx + cosf(g_time + 4.189) * 80.0, cy + sinf(g_time + 4.189) * 80.0
    )

    // Flush 2D draws into Sokol GFX pass
    var pass: sg_pass
    pass.swapchain = sglue_swapchain()
    sg_begin_pass(&pass)
    sgp_flush()
    sgp_end()
    sg_end_pass()
    sg_commit()
}

@c_abi fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        if (ev.key_code == KEYCODE_ESCAPE) { sapp_quit() }
    }
}

@c_abi fn cleanup() {
    sgp_shutdown()
    sg_shutdown()
}

fn main() {
    var desc: sapp_desc
    desc.init_cb = init
    desc.frame_cb = frame
    desc.event_cb = event
    desc.cleanup_cb = cleanup
    desc.width = 800
    desc.height = 600
    desc.window_title = "GX + Sokol"
    sapp_run(&desc)
}

Callback Architecture

Sokol uses an event-driven model. You define four callback functions marked @c_abi and pass them via sapp_desc:

@c_abi fn init()                          // called once at startup
@c_abi fn frame()                         // called every frame
@c_abi fn event(ev: const *sapp_event)    // called on input/window events
@c_abi fn cleanup()                       // called on shutdown

The @c_abi attribute is required because Sokol calls these from C code.

Comparison with Raylib

RaylibSokol
Loop stylePolling (while !WindowShouldClose)Callback (init/frame/cleanup)
InputIsKeyPressed() per frameEvent callback with sapp_event
2D drawingDrawRectangle, DrawText, etc.sgp_draw_filled_rect, etc.
3D drawingDrawCube, LoadModel, etc.Manual shaders + pipelines
TextBuilt-in font renderingNo built-in text
SetupInitWindow() one-linersapp_desc + sg_setup + sgp_setup
Web supportLimitedFull (WebGL2/WebGPU via Emscripten)

App (sokol.app) — Window & Events

Application Descriptor

var desc: sapp_desc
desc.init_cb = init             // fn()
desc.frame_cb = frame           // fn()
desc.event_cb = event           // fn(const *sapp_event)
desc.cleanup_cb = cleanup       // fn()
desc.width = 800                // initial window width
desc.height = 600               // initial window height
desc.sample_count = 4           // MSAA samples (1 = off)
desc.swap_interval = 1          // V-sync (1 = 60fps on 60Hz)
desc.high_dpi = true            // HiDPI support
desc.fullscreen = false         // fullscreen mode
desc.window_title = "My App"    // window title
sapp_run(&desc)

Window Functions

FunctionSignatureDescription
sapp_run(const *sapp_desc)Start the application event loop
sapp_quit()Request application exit
sapp_width() → c_intCurrent window width in pixels
sapp_height() → c_intCurrent window height in pixels
sapp_frame_count() → u64Total frames rendered
sapp_frame_duration() → f64Last frame time in seconds
sapp_sample_count() → c_intCurrent MSAA sample count
sapp_color_format() → c_intFramebuffer color format
sapp_depth_format() → c_intFramebuffer depth format

Event Handling

Events arrive via the event_cb callback:

@c_abi fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        if (ev.key_code == KEYCODE_W) { move_forward() }
        if (ev.key_code == KEYCODE_ESCAPE) { sapp_quit() }
    }
    if (ev.type == EVENTTYPE_MOUSE_DOWN) {
        if (ev.mouse_button == MOUSEBUTTON_LEFT) {
            shoot(ev.mouse_x, ev.mouse_y)
        }
    }
    if (ev.type == EVENTTYPE_MOUSE_MOVE) {
        look_at(ev.mouse_dx, ev.mouse_dy)
    }
    if (ev.type == EVENTTYPE_MOUSE_SCROLL) {
        zoom(ev.scroll_y)
    }
}

Event Struct (sapp_event)

FieldTypeDescription
frame_countu64Frame number when event occurred
typesapp_event_typeEvent type enum
key_codesapp_keycodeKeyboard scancode
char_codeu32Unicode character (for CHAR events)
key_repeatc_boolTrue if key is auto-repeating
modifiersu32Modifier key bitmask
mouse_buttonsapp_mousebuttonWhich mouse button
mouse_x, mouse_yf32Mouse position
mouse_dx, mouse_dyf32Mouse delta since last event
scroll_x, scroll_yf32Scroll wheel delta
num_touchesc_intNumber of active touches
touchessapp_touchpoint[8]Touch point array
window_width, window_heightc_intWindow size
framebuffer_width, framebuffer_heightc_intFramebuffer size

Event Types

ConstantDescription
EVENTTYPE_KEY_DOWNKey pressed
EVENTTYPE_KEY_UPKey released
EVENTTYPE_CHARCharacter typed (Unicode)
EVENTTYPE_MOUSE_DOWNMouse button pressed
EVENTTYPE_MOUSE_UPMouse button released
EVENTTYPE_MOUSE_SCROLLMouse wheel scrolled
EVENTTYPE_MOUSE_MOVEMouse moved
EVENTTYPE_MOUSE_ENTERMouse entered window
EVENTTYPE_MOUSE_LEAVEMouse left window
EVENTTYPE_RESIZEDWindow resized
EVENTTYPE_FOCUSEDWindow gained focus
EVENTTYPE_UNFOCUSEDWindow lost focus
EVENTTYPE_QUIT_REQUESTEDClose button clicked

Keyboard Constants

All standard keycodes: KEYCODE_A through KEYCODE_Z, KEYCODE_0 through KEYCODE_9, KEYCODE_SPACE, KEYCODE_ESCAPE, KEYCODE_ENTER, KEYCODE_TAB, KEYCODE_BACKSPACE, KEYCODE_DELETE, arrow keys (KEYCODE_UP, KEYCODE_DOWN, KEYCODE_LEFT, KEYCODE_RIGHT), function keys (KEYCODE_F1 through KEYCODE_F25), modifiers (KEYCODE_LEFT_SHIFT, KEYCODE_LEFT_CONTROL, KEYCODE_LEFT_ALT), numpad (KEYCODE_KP_0 through KEYCODE_KP_9).

Mouse Button Constants

MOUSEBUTTON_LEFT, MOUSEBUTTON_RIGHT, MOUSEBUTTON_MIDDLE.

GFX (sokol.gfx) — GPU Rendering

The low-level graphics API for creating buffers, shaders, and pipelines.

Initialization

@c_abi fn init() {
    var gfx_desc: sg_desc
    gfx_desc.environment = sglue_environment()
    sg_setup(&gfx_desc)
}

Render Pass

A render pass clears the framebuffer and draws geometry:

@c_abi fn frame() {
    var pass: sg_pass
    pass.swapchain = sglue_swapchain()
    pass.action.colors[0].load_action = 1     // SG_LOADACTION_CLEAR
    pass.action.colors[0].clear_value.r = 0.1
    pass.action.colors[0].clear_value.g = 0.1
    pass.action.colors[0].clear_value.b = 0.15
    pass.action.colors[0].clear_value.a = 1.0
    sg_begin_pass(&pass)
    // ... draw ...
    sg_end_pass()
    sg_commit()
}

Core Functions

FunctionSignatureDescription
sg_setup(const *sg_desc)Initialize graphics system
sg_shutdown()Shutdown graphics system
sg_begin_pass(const *sg_pass)Begin a render pass
sg_end_pass()End current render pass
sg_commit()Submit all commands to GPU
sg_apply_pipeline(sg_pipeline)Set active pipeline
sg_apply_bindings(const *sg_bindings)Set active buffers/textures
sg_draw(c_int, c_int, c_int)Draw: base_element, num_elements, num_instances
sg_update_buffer(sg_buffer, const *sg_range)Update buffer data

Resource Creation

FunctionSignatureDescription
sg_make_buffer(const *sg_buffer_desc) → sg_bufferCreate vertex/index buffer
sg_make_shader(const *sg_shader_desc) → sg_shaderCreate shader program
sg_make_pipeline(const *sg_pipeline_desc) → sg_pipelineCreate pipeline
sg_destroy_buffer(sg_buffer)Free buffer
sg_destroy_shader(sg_shader)Free shader
sg_destroy_pipeline(sg_pipeline)Free pipeline

Custom Shader Example

For advanced rendering (e.g. a custom particle system), you create shaders, pipelines, and vertex buffers manually:

// Vertex data for a colored triangle
var vertices: f32[15] = [
    // x,    y,     r,   g,   b
     0.0,  0.5,   1.0, 0.0, 0.0,
     0.5, -0.5,   0.0, 1.0, 0.0,
    -0.5, -0.5,   0.0, 0.0, 1.0
]

// Create vertex buffer
var buf_desc: sg_buffer_desc
buf_desc.data.ptr = &vertices
buf_desc.data.size = 60   // 15 floats * 4 bytes
var vbuf = sg_make_buffer(&buf_desc)

// Create shader with inline GLSL
var shd_desc: sg_shader_desc
shd_desc.vertex_func.source = "
    #version 330
    layout(location=0) in vec2 pos;
    layout(location=1) in vec3 col;
    out vec3 v_color;
    void main() { gl_Position = vec4(pos, 0.0, 1.0); v_color = col; }
"
shd_desc.fragment_func.source = "
    #version 330
    in vec3 v_color;
    out vec4 frag_color;
    void main() { frag_color = vec4(v_color, 1.0); }
"
shd_desc.attrs[0].glsl_name = "pos"
shd_desc.attrs[1].glsl_name = "col"
var shd = sg_make_shader(&shd_desc)

// Create pipeline
var pip_desc: sg_pipeline_desc
pip_desc.shader = shd
pip_desc.layout.attrs[0].format = 3    // SG_VERTEXFORMAT_FLOAT2
pip_desc.layout.attrs[1].format = 5    // SG_VERTEXFORMAT_FLOAT3
var pip = sg_make_pipeline(&pip_desc)

// Draw each frame
sg_apply_pipeline(pip)
var bind: sg_bindings
bind.vertex_buffers[0] = vbuf
sg_apply_bindings(&bind)
sg_draw(0, 3, 1)

Handle Types

All GPU resources are opaque 32-bit handles:

TypeDescription
sg_bufferVertex or index buffer
sg_imageTexture image
sg_samplerTexture sampler
sg_shaderShader program
sg_pipelineRender pipeline
sg_attachmentsRender target attachments

GP (sokol.gp) — 2D Drawing

A high-level 2D drawing API built on top of sokol.gfx. Similar in concept to raylib’s shape drawing but with explicit batching and state management.

Initialization

@c_abi fn init() {
    // Initialize gfx first
    var gfx_desc: sg_desc
    gfx_desc.environment = sglue_environment()
    sg_setup(&gfx_desc)

    // Then initialize 2D painter
    var gp_desc: sgp_desc
    sgp_setup(&gp_desc)
}

Frame Structure

@c_abi fn frame() {
    var w = sapp_width()
    var h = sapp_height()

    sgp_begin(w, h)                    // begin 2D draw batch
    sgp_viewport(0, 0, w, h)          // set viewport
    sgp_project(0.0, 800.0, 0.0, 600.0) // coordinate system

    sgp_set_color(0.1, 0.1, 0.1, 1.0)
    sgp_clear()                        // clear background

    // ... draw shapes ...

    // Flush into Sokol GFX pass
    var pass: sg_pass
    pass.swapchain = sglue_swapchain()
    sg_begin_pass(&pass)
    sgp_flush()                        // submit 2D draws
    sgp_end()                          // end 2D batch
    sg_end_pass()
    sg_commit()
}

Drawing Primitives

FunctionSignatureDescription
sgp_clear()Clear framebuffer to current color
sgp_draw_point(f32, f32)Single point
sgp_draw_points(const *sgp_vec2, u32)Multiple points
sgp_draw_line(f32, f32, f32, f32)Line from (ax,ay) to (bx,by)
sgp_draw_lines(const *sgp_line, u32)Multiple lines
sgp_draw_lines_strip(const *sgp_vec2, u32)Connected line strip
sgp_draw_filled_triangle(f32, f32, f32, f32, f32, f32)Filled triangle
sgp_draw_filled_triangles(const *sgp_triangle, u32)Multiple triangles
sgp_draw_filled_triangles_strip(const *sgp_vec2, u32)Triangle strip
sgp_draw_filled_rect(f32, f32, f32, f32)Filled rectangle
sgp_draw_filled_rects(const *sgp_rect, u32)Multiple rectangles
sgp_draw_textured_rect(c_int, sgp_rect, sgp_rect)Textured rectangle
sgp_draw_textured_rects(c_int, const *sgp_textured_rect, u32)Multiple textured rects

Drawing a Game HUD

// Health bar background
sgp_set_color(0.3, 0.0, 0.0, 0.8)
sgp_draw_filled_rect(20.0, 20.0, 200.0, 20.0)

// Health bar fill (proportional)
var health_pct = 0.75  // 75% health
sgp_set_color(0.0, 0.9, 0.0, 0.9)
sgp_draw_filled_rect(20.0, 20.0, 200.0 * health_pct, 20.0)

// Minimap border
sgp_set_color(0.5, 0.5, 0.5, 0.8)
sgp_draw_filled_rect(600.0, 20.0, 164.0, 164.0)  // border
sgp_set_color(0.1, 0.1, 0.15, 0.9)
sgp_draw_filled_rect(602.0, 22.0, 160.0, 160.0)  // inner

// Player dot on minimap
sgp_set_color(0.0, 1.0, 0.0, 1.0)
sgp_draw_filled_rect(680.0, 100.0, 4.0, 4.0)

// Enemy dots
sgp_set_color(1.0, 0.0, 0.0, 1.0)
sgp_draw_filled_rect(650.0, 80.0, 3.0, 3.0)
sgp_draw_filled_rect(700.0, 130.0, 3.0, 3.0)

Color & Blending

FunctionSignatureDescription
sgp_set_color(f32, f32, f32, f32)Set RGBA color (0.0-1.0)
sgp_reset_color()Reset to white
sgp_set_blend_mode(sgp_blend_mode)Set blending mode
sgp_reset_blend_mode()Reset to no blending

Blend Modes:

ConstantDescription
BLENDMODE_NONENo blending (replace)
BLENDMODE_BLENDStandard alpha blending
BLENDMODE_ADDAdditive (glow effects)
BLENDMODE_MULMultiply (shadows)
BLENDMODE_BLEND_PREMULTIPLIEDPremultiplied alpha

Transforms

Apply 2D transforms to subsequent draw calls:

FunctionSignatureDescription
sgp_push_transform()Save transform state
sgp_pop_transform()Restore transform state
sgp_reset_transform()Reset to identity
sgp_translate(f32, f32)Translate
sgp_rotate(f32)Rotate (radians)
sgp_rotate_at(f32, f32, f32)Rotate around point
sgp_scale(f32, f32)Scale
sgp_scale_at(f32, f32, f32, f32)Scale around point
// Draw a rotated rectangle (like a spinning pickup item)
sgp_push_transform()
sgp_rotate_at(g_time * 2.0, 400.0, 300.0)  // spin around center
sgp_set_color(1.0, 0.8, 0.0, 1.0)
sgp_draw_filled_rect(385.0, 285.0, 30.0, 30.0)
sgp_pop_transform()

Textures

FunctionSignatureDescription
sgp_set_image(c_int, sg_image)Bind texture to channel
sgp_unset_image(c_int)Unbind texture
sgp_reset_image(c_int)Reset to default white
sgp_set_sampler(c_int, sg_sampler)Set sampler for channel
sgp_reset_sampler(c_int)Reset sampler
// Draw a textured sprite
sgp_set_image(0, sprite_texture)
sgp_set_color(1.0, 1.0, 1.0, 1.0)  // no tint
var dst: sgp_rect
dst.x = player_x; dst.y = player_y; dst.w = 64.0; dst.h = 64.0
var src: sgp_rect
src.x = 0.0; src.y = 0.0; src.w = 1.0; src.h = 1.0  // full texture
sgp_draw_textured_rect(0, dst, src)
sgp_unset_image(0)

Viewport & Scissor

FunctionSignatureDescription
sgp_viewport(c_int, c_int, c_int, c_int)Set rendering viewport
sgp_reset_viewport()Reset to full framebuffer
sgp_project(f32, f32, f32, f32)Set orthographic projection
sgp_reset_project()Reset projection
sgp_scissor(c_int, c_int, c_int, c_int)Set scissor clip region
sgp_reset_scissor()Disable scissor
// Draw only within a UI panel region
sgp_scissor(20, 100, 300, 400)  // clip to panel bounds
// ... draw panel contents ...
sgp_reset_scissor()

Batch Control

FunctionDescription
sgp_begin(w, h)Start a 2D draw batch
sgp_flush()Submit all queued draws to Sokol GFX
sgp_end()End the 2D batch

All sgp_draw_* calls are batched internally. sgp_flush() submits them as Sokol GFX draw commands inside a render pass.

Error Handling

FunctionSignatureDescription
sgp_is_valid() → c_boolCheck if GP initialized
sgp_get_last_error() → sgp_errorGet last error
sgp_get_error_message(sgp_error) → cstrError as string

Glue (sokol.glue) — App/GFX Bridge

Two functions that connect sokol.app to sokol.gfx:

FunctionSignatureDescription
sglue_environment() → sg_environmentGet platform environment for sg_setup
sglue_swapchain() → sg_swapchainGet default framebuffer for sg_begin_pass
// In init:
var desc: sg_desc
desc.environment = sglue_environment()
sg_setup(&desc)

// In frame:
var pass: sg_pass
pass.swapchain = sglue_swapchain()
sg_begin_pass(&pass)

Common Patterns

Game Loop Template

import sokol.app
import sokol.gfx
import sokol.glue
import sokol.gp

var g_time: f32 = 0.0

@c_abi fn init() {
    var gfx_desc: sg_desc
    gfx_desc.environment = sglue_environment()
    sg_setup(&gfx_desc)
    var gp_desc: sgp_desc
    sgp_setup(&gp_desc)
}

@c_abi fn frame() {
    var w = sapp_width()
    var h = sapp_height()
    g_time = g_time + sapp_frame_duration()

    sgp_begin(w, h)
    sgp_viewport(0, 0, w, h)
    sgp_project(0.0, 800.0, 0.0, 600.0)

    // Clear
    sgp_set_color(0.0, 0.0, 0.0, 1.0)
    sgp_clear()

    // Draw game world here...

    // Flush to screen
    var pass: sg_pass
    pass.swapchain = sglue_swapchain()
    sg_begin_pass(&pass)
    sgp_flush()
    sgp_end()
    sg_end_pass()
    sg_commit()
}

@c_abi fn event(ev: const *sapp_event) {
    if (ev.type == EVENTTYPE_KEY_DOWN) {
        if (ev.key_code == KEYCODE_ESCAPE) { sapp_quit() }
    }
}

@c_abi fn cleanup() {
    sgp_shutdown()
    sg_shutdown()
}

fn main() {
    var desc: sapp_desc
    desc.init_cb = init
    desc.frame_cb = frame
    desc.event_cb = event
    desc.cleanup_cb = cleanup
    desc.width = 800
    desc.height = 600
    desc.swap_interval = 1
    desc.window_title = "My Game"
    sapp_run(&desc)
}

Dynamic Vertex Buffer (Snake Game Pattern)

For games that update geometry every frame (tile maps, particle systems):

var g_verts: f32[5000]   // x, y, r, g, b per vertex
var g_vert_count: i32 = 0
var g_vbuf: sg_buffer

@c_abi fn init() {
    // ... setup gfx ...

    // Create dynamic vertex buffer
    var buf_desc: sg_buffer_desc
    buf_desc.size = 20000           // 5000 floats * 4 bytes
    buf_desc.usage.dynamic_update = true
    g_vbuf = sg_make_buffer(&buf_desc)
}

@c_abi fn frame() {
    // Rebuild vertex data
    g_vert_count = 0
    // ... push_vertex() calls to fill g_verts ...

    // Upload to GPU
    var range: sg_range
    range.ptr = &g_verts
    range.size = g_vert_count * 20   // 5 floats * 4 bytes per vertex
    sg_update_buffer(g_vbuf, &range)

    // Draw
    sg_apply_pipeline(g_pip)
    var bind: sg_bindings
    bind.vertex_buffers[0] = g_vbuf
    sg_apply_bindings(&bind)
    sg_draw(0, g_vert_count, 1)
}

Differences from Raylib

FeatureRaylibSokol
Text renderingBuilt-in (DrawText, LoadFont)None — use glyph atlas or Clay UI
Rounded rectanglesDrawRectangleRoundedManual triangle fans
3D modelsLoadModel, DrawModelManual shaders + vertex buffers
AudioBuilt-in (LoadSound, PlaySound)Not included (use separate lib)
Web exportLimitedFull (compile with Emscripten)
GPU backendsOpenGL onlyOpenGL, D3D11, Metal, WebGL2, WebGPU
Setup complexity1 line (InitWindow)3-4 structs (sapp_desc, sg_desc, sgp_desc)
PerformanceGoodExcellent (lower overhead, GPU-native)

Platform Notes

  • Windows: Uses OpenGL Core Profile. Links opengl32, gdi32, user32, shell32.
  • Linux: Links GL, X11, Xi, Xcursor, dl, pthread, m.
  • macOS: Uses Metal. Links Cocoa, QuartzCore, Metal, MetalKit.
  • Web: Compiles to WebGL2/WebGPU via Emscripten.
  • TCC: Supported for simple programs. Clang/GCC recommended for optimized builds.
  • SOKOL_GLCORE is the default backend on desktop. Change in sokol_impl.c for others.

Module Layout

modules/
  sokol/
    c/
      sokol_app.h        Platform windowing + events
      sokol_gfx.h        GPU graphics API
      sokol_gp.h         2D drawing painter
      sokol_glue.h       App ↔ GFX bridge
      sokol_impl.c       Single compilation unit (SOKOL_IMPL)
    gx/
      app.gx             Window, events, keycodes (build directives here)
      gfx.gx             Buffers, shaders, pipelines, passes
      gp.gx              2D drawing, transforms, blending
      glue.gx            Environment + swapchain bridge

Examples

ExampleLocationDescription
triangle.gxexamples/sokol/Low-level rendering: vertex buffer, shader, pipeline
input.gxexamples/sokol/Event handling: keyboard and mouse input
shapes.gxexamples/sokol/2D drawing: animated shapes with Sokol GP
snake.gxexamples/sokol/Full game: Snake with dynamic vertex buffer