Skip to content

Commit 282c96b

Browse files
committed
0.0.9 opengl interop
1 parent e46fc90 commit 282c96b

12 files changed

Lines changed: 483 additions & 24 deletions

File tree

‎.github/workflows/build.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
CIBW_SKIP: "cp36-* cp37-* cp38-* cp39-* pp* *-musllinux_*"
4040
CIBW_BUILD_VERBOSITY: 1
4141
CIBW_BEFORE_ALL_LINUX: >
42-
yum install -y yum-utils &&
42+
yum install -y yum-utils mesa-libGL-devel &&
4343
yum-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64/cuda-rhel8.repo &&
4444
yum install -y cuda-nvcc-12-5 cuda-cudart-devel-12-5
4545
CIBW_ENVIRONMENT_LINUX: >

‎CMakeLists.txt‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
cmake_minimum_required(VERSION 3.18)
22

33
set(CMAKE_CUDA_RUNTIME_LIBRARY Static)
4-
54
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
65

76
project(pyimagecuda_internal LANGUAGES CXX CUDA)
@@ -15,14 +14,19 @@ endif()
1514

1615
find_package(Python COMPONENTS Interpreter Development.Module REQUIRED)
1716
find_package(CUDAToolkit REQUIRED)
17+
find_package(OpenGL REQUIRED)
1818

1919
file(GLOB_RECURSE SOURCES "src/*.cu" "src/*.cpp")
2020

2121
python_add_library(pyimagecuda_internal MODULE ${SOURCES} WITH_SOABI)
2222

2323
target_include_directories(pyimagecuda_internal PRIVATE src)
2424

25-
target_link_libraries(pyimagecuda_internal PRIVATE CUDA::cudart_static Python::Module)
25+
target_link_libraries(pyimagecuda_internal PRIVATE
26+
CUDA::cudart_static
27+
Python::Module
28+
OpenGL::GL
29+
)
2630

2731
install(TARGETS pyimagecuda_internal DESTINATION pyimagecuda)
2832
install(DIRECTORY pyimagecuda DESTINATION .)

‎README.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# PyImageCUDA 0.0.8
1+
# PyImageCUDA 0.0.9
22

33
[![PyPI version](https://img.shields.io/pypi/v/pyimagecuda.svg)](https://pypi.org/project/pyimagecuda/)
44
[![Build Status](https://github.com/offerrall/pyimagecuda/actions/workflows/build.yml/badge.svg)](https://github.com/offerrall/pyimagecuda/actions)
@@ -38,13 +38,15 @@ with Image(1024, 1024) as bg:
3838
* ✅ **Ultra-lightweight:** library weighs **~1 MB**.
3939
* ✅ **Studio Quality:** 32-bit floating-point precision (float32) to prevent color banding.
4040
* ✅ **Advanced Memory Control:** Reuse GPU buffers across operations and resize without reallocation—critical for video processing and batch workflows.
41+
* ✅ **OpenGL Integration:** Direct GPU-to-GPU display for real-time preview widgets.
4142
* ✅ **API Simplicity:** Intuitive, Pythonic API designed for ease of use.
4243

4344
## Use Cases
4445

4546
* **Generative Art:** Create thousands of unique variations in seconds.
4647
* **Motion Graphics:** Process video frames or generate effects in real-time.
4748
* **Image Compositing:** Complex multi-layer designs with GPU-accelerated effects.
49+
* **Node Editors & Real-time Tools:** Build responsive image editors with instant preview.
4850
* **Game Development:** Procedural UI assets, icons, and sprite generation.
4951
* **Marketing Automation:** Mass-produce personalized graphics from templates.
5052
* **Data Augmentation:** High-speed batch transformations for ML datasets.
@@ -64,6 +66,7 @@ pip install pyimagecuda
6466
* [Getting Started Guide](https://offerrall.github.io/pyimagecuda/)
6567
* [Image & Memory](https://offerrall.github.io/pyimagecuda/image/) (Buffer management)
6668
* [IO](https://offerrall.github.io/pyimagecuda/io/) (Loading and Saving)
69+
* [OpenGL Integration](https://offerrall.github.io/pyimagecuda/opengl/) (Real-time preview, zero-copy display)
6770

6871
### Operations
6972
* [Fill](https://offerrall.github.io/pyimagecuda/fill/) (Solid colors, Gradients, Checkerboard, Grid, Stripes, Dots, Circle, Ngon, Noise, Perlin)
@@ -120,4 +123,4 @@ pytest tests/tests.py
120123
Contributions welcome! Open issues or submit PRs
121124

122125
## License
123-
MIT License. See [LICENSE](LICENSE) for details.
126+
MIT License. See [LICENSE](LICENSE) for details.

‎docs/io.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,30 @@ How it works:
133133
2. Downloads from GPU to CPU
134134
3. Encodes and writes to disk
135135

136+
### Saving U8 Images Directly
137+
138+
When you've manually converted an image to uint8 for another purpose (e.g., OpenGL display, external library) and also want to save it to disk:
139+
```python
140+
from pyimagecuda import Image, ImageU8, convert_float_to_u8, save_u8
141+
142+
f32_img = Image(1920, 1080)
143+
u8_img = ImageU8(1920, 1080)
144+
145+
# Process in float32
146+
process(f32_img)
147+
148+
# Convert once for multiple uses
149+
convert_float_to_u8(u8_img, f32_img)
150+
151+
# Use u8_img for display, OpenGL, etc.
152+
display_opengl(u8_img)
153+
154+
# Save the same u8 buffer
155+
save_u8(u8_img, "output.png")
156+
```
157+
158+
**Note:** Most workflows use `save()` directly with float32 images. This function eliminates redundant conversion when you already have the uint8 buffer.
159+
136160
---
137161

138162
## NumPy Integration

‎docs/opengl.md‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# OpenGL Integration
2+
3+
PyImageCUDA provides a GPU-to-GPU transfer with OpenGL through CUDA-OpenGL interop, enabling efficient real-time preview pipelines.
4+
5+
---
6+
7+
## Overview
8+
9+
**Traditional approach:**
10+
```
11+
GPU Processing → CPU Download → CPU Upload → OpenGL Display
12+
```
13+
14+
**With OpenGL interop:**
15+
```
16+
GPU Processing → GPU Copy → OpenGL Display
17+
```
18+
19+
**Performance:**
20+
21+
- Zero CPU involvement (all transfers GPU-side)
22+
- ~10x faster than CPU download/upload
23+
- Sub-millisecond updates for 2K images (~1ms measured)
24+
- Single GPU-GPU DMA copy (PBO→Texture, asynchronous)
25+
26+
---
27+
28+
## GLResource API
29+
30+
### Constructor
31+
```python
32+
GLResource(pbo_id: int)
33+
```
34+
Registers an OpenGL PBO with CUDA for interop.
35+
36+
**Parameters:**
37+
- `pbo_id`: OpenGL buffer ID from `glGenBuffers()`
38+
39+
**Raises:**
40+
- `ValueError`: If `pbo_id` is invalid
41+
- `RuntimeError`: If CUDA registration fails
42+
43+
---
44+
45+
### copy_from()
46+
```python
47+
gl_resource.copy_from(image: ImageU8) -> None
48+
```
49+
Copies ImageU8 data directly to PBO (GPU→GPU, zero CPU overhead).
50+
51+
**Parameters:**
52+
- `image`: Source ImageU8 buffer
53+
54+
**Raises:**
55+
- `TypeError`: If image is not ImageU8
56+
- `RuntimeError`: If resource has been freed
57+
58+
---
59+
60+
### free()
61+
```python
62+
gl_resource.free() -> None
63+
```
64+
Unregisters the resource. Must be called before deleting the PBO.
65+
66+
**Context Manager:**
67+
```python
68+
with GLResource(pbo) as gl_resource:
69+
gl_resource.copy_from(image)
70+
```
71+
72+
## Best Practices
73+
74+
### Display Integration
75+
- Invert texture Y-coordinates if image appears upside-down
76+
- Enable `GL_BLEND` for alpha channel support
77+
78+
### Resource Management
79+
- Create PBO with `GL_STREAM_DRAW` for best performance
80+
- Pre-allocate buffers to avoid reallocation overhead
81+
- Call `free()` before deleting OpenGL objects
82+
83+
### Performance
84+
- Reuse `ImageU8` buffers when possible
85+
- Use texture size matching image size (or larger)
86+
- `glTexSubImage2D` with PBO is asynchronous (no CPU stall)

‎mkdocs.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ nav:
5050
- Home: index.md
5151
- Image: image.md
5252
- Load and Save: io.md
53+
- OpenGL: opengl.md
5354
- Fill: fill.md
5455
- Text: text.md
5556
- Blend: blend.md

‎pyimagecuda/__init__.py‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ctypes
22
import sys
33

4-
__version__ = "0.0.7"
4+
__version__ = "0.0.9"
55

66
def _check_nvidia_driver():
77
try:
@@ -37,7 +37,7 @@ def _check_nvidia_driver():
3737
if _INTERNAL_LOADED:
3838
try:
3939
from .image import Image, ImageU8
40-
from .io import upload, download, copy, save, load, convert_float_to_u8, convert_u8_to_float, from_numpy, to_numpy
40+
from .io import upload, download, copy, save, load, convert_float_to_u8, convert_u8_to_float, from_numpy, to_numpy, save_u8
4141
from .fill import Fill
4242
from .resize import Resize
4343
from .blend import Blend
@@ -46,6 +46,7 @@ def _check_nvidia_driver():
4646
from .adjust import Adjust
4747
from .transform import Transform
4848
from .text import Text
49+
from .gl_interop import GLResource
4950
from .pyimagecuda_internal import cuda_sync # type: ignore
5051
except ImportError as e:
5152
print(f"Warning: Error importing Python wrappers: {e}")

‎pyimagecuda/gl_interop.py‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from .pyimagecuda_internal import ( # type: ignore
2+
register_gl_pbo,
3+
unregister_gl_resource,
4+
copy_to_gl_pbo
5+
)
6+
from .image import ImageU8
7+
8+
9+
class GLResource:
10+
"""
11+
CUDA-OpenGL interop resource for direct GPU-to-GPU transfers.
12+
13+
Represents a registered OpenGL PBO that can receive ImageU8 data
14+
directly without CPU roundtrips.
15+
16+
Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
17+
"""
18+
19+
def __init__(self, pbo_id: int):
20+
"""
21+
Parameters:
22+
pbo_id: Valid OpenGL PBO ID. The PBO must exist and remain
23+
valid for the lifetime of this GLResource.
24+
25+
Raises:
26+
ValueError: If pbo_id is invalid
27+
RuntimeError: If CUDA registration fails
28+
29+
Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
30+
"""
31+
if not isinstance(pbo_id, int) or pbo_id <= 0:
32+
raise ValueError(f"Invalid PBO ID: {pbo_id}")
33+
34+
self._handle = None
35+
self.pbo_id = pbo_id
36+
self._handle = register_gl_pbo(pbo_id)
37+
38+
def copy_from(self, image: ImageU8) -> None:
39+
"""
40+
Copies ImageU8 data directly to the registered PBO (GPU→GPU).
41+
42+
Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
43+
"""
44+
if not isinstance(image, ImageU8):
45+
raise TypeError(f"Expected ImageU8, got {type(image).__name__}")
46+
47+
if self._handle is None:
48+
raise RuntimeError("GLResource has been freed")
49+
50+
copy_to_gl_pbo(
51+
image._buffer._handle,
52+
self._handle,
53+
image.width,
54+
image.height
55+
)
56+
57+
def free(self) -> None:
58+
"""
59+
Unregisters the OpenGL resource from CUDA.
60+
61+
Docs & Examples: https://offerrall.github.io/pyimagecuda/opengl/
62+
"""
63+
if self._handle is not None:
64+
unregister_gl_resource(self._handle)
65+
self._handle = None
66+
67+
def __enter__(self):
68+
return self
69+
70+
def __exit__(self, exc_type, exc_value, traceback):
71+
self.free()
72+
return False
73+
74+
def __del__(self):
75+
if hasattr(self, '_handle') and self._handle is not None:
76+
try:
77+
self.free()
78+
except Exception:
79+
pass
80+
81+
def __repr__(self) -> str:
82+
status = "active" if getattr(self, '_handle', None) is not None else "freed"
83+
return f"GLResource(pbo_id={self.pbo_id}, status={status})"

‎pyimagecuda/io.py‎

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -121,26 +121,13 @@ def load(
121121
return f32_buffer if should_return else None
122122

123123

124-
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
125-
"""
126-
Saves the floating-point image to a file (using an 8-bit buffer for conversion).
127-
128-
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
129-
"""
130-
if u8_buffer is None:
131-
u8_buffer = ImageU8(image.width, image.height)
132-
owns_buffer = True
133-
else:
134-
ensure_capacity(u8_buffer, image.width, image.height)
135-
owns_buffer = False
136-
137-
convert_float_to_u8(u8_buffer, image)
138-
pixel_data = download(u8_buffer)
124+
def _save_internal(u8_image: ImageU8, filepath: str, quality: int | None = None) -> None:
125+
pixel_data = download(u8_image)
139126

140127
vips_img = pyvips.Image.new_from_memory(
141128
pixel_data,
142-
image.width,
143-
image.height,
129+
u8_image.width,
130+
u8_image.height,
144131
bands=4,
145132
format='uchar'
146133
)
@@ -157,10 +144,37 @@ def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality:
157144
save_kwargs['Q'] = quality
158145

159146
vips_img.write_to_file(filepath, **save_kwargs)
147+
148+
149+
def save(image: Image, filepath: str, u8_buffer: ImageU8 | None = None, quality: int | None = None) -> None:
150+
"""
151+
Saves the floating-point image to a file (using an 8-bit buffer for conversion).
152+
153+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
154+
"""
155+
if u8_buffer is None:
156+
u8_buffer = ImageU8(image.width, image.height)
157+
owns_buffer = True
158+
else:
159+
ensure_capacity(u8_buffer, image.width, image.height)
160+
owns_buffer = False
161+
162+
convert_float_to_u8(u8_buffer, image)
163+
_save_internal(u8_buffer, filepath, quality)
160164

161165
if owns_buffer:
162166
u8_buffer.free()
163167

168+
169+
def save_u8(image: ImageU8, filepath: str, quality: int | None = None) -> None:
170+
"""
171+
Saves an 8-bit unsigned integer image directly to a file.
172+
173+
Docs & Examples: https://offerrall.github.io/pyimagecuda/io/#saving-images
174+
"""
175+
_save_internal(image, filepath, quality)
176+
177+
164178
def from_numpy(array, f32_buffer: Image | None = None, u8_buffer: ImageU8 | None = None) -> Image:
165179
"""
166180
Creates a PyImageCUDA Image from a NumPy array (e.g. from OpenCV, Pillow, Matplotlib).

0 commit comments

Comments
 (0)