Skip to main content

gl46

Python OpenGL 4.6 Direct State Access (DSA) utility library focused on compute and data-oriented workflows, wrapping PyOpenGL through an explicit GL protocol.

Overview

gl46 provides type-annotated, DSA-first GPU objects for OpenGL 4.6 core. Every GL call goes through a GL protocol (gl46.gl.GL), with a real PyOpenGLGL backend for the driver and a FakeGL recording backend for headless tests, so the same script runs against both.

Key classes, grouped by concern:

  • Buffers: Buffer, EBO, IndirectDrawBuffer (aliased as IBO), ShaderStorage
  • Textures: Texture2D, Texture2DArray, TextureCubeMapArray, TextureMultisample, TextureBuffer
  • Shaders and programs: Shader, ShaderStage, Program, ProgramPipeline, Sampler
  • Geometry and framebuffer state: VertexArray, VertexAttrib, Framebuffer, Renderbuffer, RenderbufferMultisample
  • Scene helpers: Material, Camera, Window
  • Compute, queries, and debug: Barrier (aliased as MemoryBarrier), Query, TransformFeedback, DebugOutput

Not included. Windowing and input are not part of the core API — thin optional backends (gl46.window_imgui, gl46.window_qt) provide windows, render loops, and input. There is no scene graph, no math types (use numpy / pyglm), and no bindless-texture or multi-draw-indirect layer yet.

gl46 is not a ModernGL replacement: it is a minimal, DSA-first, fully testable substrate for developers assembling their own rendering framework.

Requirements

  • Python 3.11+
  • glfw>=2.10.2
  • PyOpenGL and pyopengl-accelerate for the real GL backend
  • An OpenGL 4.5+ driver; Window requests a 4.6 core context by default.

Development inside this repository uses uv; see Install below.

The core package has no GUI-framework or image-library dependency — glfw is the only windowing dependency, and it is required only by gl46.window.Window. imgui_bundle and PySide6 are optional extras, needed only by examples/imgui_scene.py and examples/qt_scene.py (uv sync --extra imgui or --extra qt; equivalent pip form: pip install "gl46[imgui]" or pip install "gl46[qt]"). Pillow is a separate optional extra for examples/instancing_mrt.py: pip install "gl46[examples]".

The committed uv.lock is resolved against a China mirror (Huawei Cloud); international contributors may want to override the index locally. This has no effect on users installing gl46 from PyPI.

Install

From PyPI (any project):

pip install gl46

Optional extras:

pip install "gl46[imgui]"      # ImGui backend (gl46.window_imgui)
pip install "gl46[qt]"         # PySide6 backend (gl46.window_qt)
pip install "gl46[examples]"   # Pillow, for examples/instancing_mrt.py

If you use uv:

uv add gl46
uv add "gl46[imgui]"           # etc.

Development install inside this repository:

uv sync --all-extras

Quick Start

Every gl46 call needs a current OpenGL 4.5+ context. The built-in Window creates one (pass visible=False for an offscreen context); you can also point gl46 at any other context you already own.

import numpy as np
from gl46 import Buffer, Window
from gl46.constants import GL_MAP_READ_BIT, GL_MAP_WRITE_BIT

with Window(320, 240, visible=False):           # creates a GL context
    buf = Buffer(
        16 * 4,
        flags=GL_MAP_READ_BIT | GL_MAP_WRITE_BIT,
        initial_data=np.arange(16, dtype=np.float32),
    )
    with buf.map_range(0, buf.size_bytes, GL_MAP_READ_BIT) as mapped:
        out = mapped.as_numpy(np.float32, (16,))
    print(out)   # [ 0.  1.  2. ... 15.]

For a full rendering example with a visible window, see examples/triangle.py.

Examples

Run any example with uv run python examples/<name>.py from the repo root. Except for quickstart.py (a headless snippet), each opens a visible window. Most respond to SPACE by writing <name>_frame.png; closing the window writes <name>_close.png. Three behave differently:

  • quickstart.py creates an offscreen context (visible=False) and prints a numpy array — no window, no capture.
  • imgui_scene.py writes imgui_scene.png (scene + ImGui overlay) automatically on its second frame.
  • qt_scene.py writes qt_scene.png on first draw, and qt_scene_r.png when you press R.
uv run python examples/quickstart.py          # Quick Start demo
uv run python examples/query_timing.py        # DSA timer-query GPU timing
uv run python examples/triangle.py            # VAO/VBO + a rotating triangle
uv run python examples/offscreen_render.py    # offscreen FBO + fullscreen present
uv run python examples/compute_sobel.py       # compute-shader Sobel + timing
uv run python examples/instancing_mrt.py      # instancing & MRT scene
uv run python examples/imgui_scene.py         # gl46 + ImGui overlay (imgui_bundle/immapp)
uv run python examples/qt_scene.py            # gl46 hosted in a QOpenGLWidget (PySide6)

Three examples need extra dependencies beyond the core package:

  • imgui_scene.py needs imgui-bundle>=1.92.900. Its SMOKE callback raises RuntimeError if the RunnerParams.app_shall_exit hook added in that version is missing.
  • qt_scene.py needs PySide6 (the qt extra).
  • instancing_mrt.py uses Pillow to load its assets (the examples extra); its mesh and texture files live under examples/data/, shaders under examples/shaders/.

imgui_scene.py needs the second frame because the ImGui overlay is not yet present in the framebuffer on the first one.

Building your own framework

gl46 leaves the application shell to you, but ships two thin window backends.

GLFW + ImGui (imgui_bundle)

The loop belongs to immapp.run; you supply three callbacks. post_init runs once after the ImGui context is created but before the first frame — create all gl46 objects there, since that is the earliest point where a GL context is guaranteed. scene_render draws the 3D scene each frame (behind the ImGui layer), and gui draws the control panel. Input for your scene is read through the ImGui API (imgui.is_key_pressed / imgui.get_io()).

from gl46.window_imgui import run_imgui_app

class MyScene:
    def __init__(self) -> None:
        # Create gl46 objects here (Buffer, Program, VertexArray, ...).
        ...

    def render(self) -> None:
        # Draw the frame. Resize the viewport with the current
        # framebuffer size, e.g.
        #     w, h = glfw.get_framebuffer_size(glfw.get_current_context())
        #     gl.viewport(0, 0, w, h)
        ...

scene: MyScene | None = None

def post_init() -> None:
    global scene
    scene = MyScene()

def scene_render() -> None:
    if scene is not None:
        scene.render()

def gui() -> None:
    # Draw ImGui widgets. `scene` is available here for controls.
    ...

run_imgui_app(
    post_init=post_init,
    scene_render=scene_render,
    gui=gui,
    width=800,
    height=600,
    title="app",
)

For a complete runnable version — with an animated triangle, a tint slider, and a composed imgui_scene.png screenshot written on the second frame — see examples/imgui_scene.py.

run_imgui_app accepts two optional screenshot parameters: capture_path writes one PNG, and capture_composed=True waits until the ImGui overlay has rendered before capturing, so the file contains scene + UI. The default capture_composed=False captures only the pure scene.

PySide6 (Qt)

Subclass GL46Widget, override the three hooks, then host it with run_qt_app. The loop is Qt's event loop; repaint via a QTimer (default).

from gl46.window_qt import GL46Widget, run_qt_app

class AppWidget(GL46Widget):
    def initialize_gl(self) -> None: ...              # create gl46 objects
    def resize_gl(self, w: int, h: int) -> None: ...  # framebuffer pixels
    def paint_gl(self) -> None: ...                   # draw the frame

run_qt_app(AppWidget, title="app")

Testing

uv run pytest -q

The suite runs against the FakeGL backend (no GPU required).

For a real-GL smoke check of the ImGui and Qt examples, set GL46_SMOKE=1: the app will close itself after a few frames.

GL46_SMOKE=1 uv run python examples/imgui_scene.py
GL46_SMOKE=1 uv run python examples/qt_scene.py

On Windows: set GL46_SMOKE=1 before running.

Quality Gates

uv sync --all-extras
uv run ruff check src tests
uv run ruff format --check src tests
uv run mypy src

Project Structure

gl46/
|-- src/
|   `-- gl46/
|       |-- __init__.py
|       |-- py.typed
|       |-- barrier.py
|       |-- base.py
|       |-- buffer.py
|       |-- camera.py
|       |-- constants.py
|       |-- debug.py
|       |-- errors.py
|       |-- framebuffer.py
|       |-- gl.py
|       |-- material.py
|       |-- pipeline.py
|       |-- program.py
|       |-- query.py
|       |-- renderbuffer.py
|       |-- sampler.py
|       |-- shader.py
|       |-- texture.py
|       |-- transformfeedback.py
|       |-- version.py
|       |-- vertexarray.py
|       |-- window.py
|       |-- window_imgui.py
|       `-- window_qt.py
|-- tests/
|   |-- __init__.py
|   |-- conftest.py
|   |-- fake_gl.py
|   |-- test_barrier.py
|   |-- test_base.py
|   |-- test_buffer.py
|   |-- test_buffer_extra.py
|   |-- test_camera.py
|   |-- test_constants.py
|   |-- test_debug.py
|   |-- test_errors.py
|   |-- test_fake_gl.py
|   |-- test_framebuffer.py
|   |-- test_gl.py
|   |-- test_material.py
|   |-- test_pipeline.py
|   |-- test_program.py
|   |-- test_query.py
|   |-- test_renderbuffer.py
|   |-- test_sampler.py
|   |-- test_shader.py
|   |-- test_texture.py
|   |-- test_texture_extra.py
|   |-- test_transformfeedback.py
|   |-- test_utils.py
|   |-- test_vertexarray.py
|   |-- test_version.py
|   |-- test_window.py
|   |-- test_window_imgui.py
|   `-- test_window_qt.py
|-- examples/
|   |-- data/
|   |-- shaders/
|   |-- compute_sobel.py
|   |-- imgui_scene.py
|   |-- instancing_mrt.py
|   |-- offscreen_render.py
|   |-- qt_scene.py
|   |-- query_timing.py
|   |-- quickstart.py
|   `-- triangle.py
|-- pyproject.toml
|-- uv.lock
|-- README.md
`-- LICENSE

License

MIT

Metadata

Release files for gl46 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gl46 0.1.1
File Size Uploaded
gl46-0.1.1.tar.gz 3.7 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for gl46 0.1.1
File Interpreter ABI Platform
gl46-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 3.7 MB

Release files / gl46-0.1.1.tar.gz

Download URL gl46-0.1.1.tar.gz
Size 3.7 MB
Tags Source
SHA-256 checksum
How to use checksums
8ec7b5406b849ab3f52881e150ef84dbf7997c86d04314557e3aa3545d53ec74
BLAKE2b-256 checksum
How to use checksums
bc65170d298c421252ede550b0c943d11ca13326f81390ea3a950c566e08e18c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / gl46-0.1.1-py3-none-any.whl

Download URL gl46-0.1.1-py3-none-any.whl
Size 49.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b8d9c59c7cfa761ff62fba677c0704da74994fbc9973e125510c88201b4825c8
BLAKE2b-256 checksum
How to use checksums
b399dc85b334a6c592cc22cf1df7ebbccf0cce1f5d06ec59d12dd5dc29bbdfda
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.13 {"installer":{"name":"uv","version":"0.11.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.1.2

2 release files

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page