Metadata-Version: 2.5
Name: gl46
Version: 0.1.2
Summary: Python OpenGL 4.6 Direct State Access (DSA) utility library.
Project-URL: Homepage, https://github.com/EagleEatApple/gl46
Project-URL: Repository, https://github.com/EagleEatApple/gl46
Project-URL: Issues, https://github.com/EagleEatApple/gl46/issues
Author-email: Yiguo Tang <yiguo.tang@outlook.com>
License: MIT
License-File: LICENSE
Keywords: compute,dsa,gpu,graphics,opengl
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Multimedia :: Graphics :: 3D Rendering
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: glfw>=2.10.2
Requires-Dist: numpy
Requires-Dist: pyopengl
Requires-Dist: pyopengl-accelerate>=3.1.10
Provides-Extra: examples
Requires-Dist: pillow>=12.3.0; extra == 'examples'
Provides-Extra: imgui
Requires-Dist: imgui-bundle>=1.92.900; extra == 'imgui'
Provides-Extra: qt
Requires-Dist: pyside6>=6.5; extra == 'qt'
Provides-Extra: test
Requires-Dist: mypy; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-cov; extra == 'test'
Requires-Dist: ruff; extra == 'test'
Description-Content-Type: text/markdown

# 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](https://docs.astral.sh/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):

```bash
pip install gl46
```

Optional extras:

```bash
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](https://docs.astral.sh/uv/):

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

Development install inside this repository:

```bash
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.

```python
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**.

```bash
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()`).

```python
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).

```python
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

```bash
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.

```bash
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

```bash
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