filly 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. {filly-0.1.0 → filly-0.2.0}/NOTES.md +1 -1
  2. {filly-0.1.0 → filly-0.2.0}/PKG-INFO +25 -25
  3. {filly-0.1.0 → filly-0.2.0}/README.md +19 -19
  4. {filly-0.1.0 → filly-0.2.0}/docs/explanation/assumptions.md +3 -3
  5. {filly-0.1.0 → filly-0.2.0}/docs/explanation/design.md +128 -53
  6. filly-0.2.0/docs/explanation/filament.md +201 -0
  7. {filly-0.1.0 → filly-0.2.0}/docs/explanation/material-precompilation.md +5 -2
  8. {filly-0.1.0 → filly-0.2.0}/docs/explanation/web.md +10 -7
  9. {filly-0.1.0 → filly-0.2.0}/docs/how-to/build.md +47 -10
  10. {filly-0.1.0 → filly-0.2.0}/docs/how-to/gl-hosts.md +86 -71
  11. {filly-0.1.0 → filly-0.2.0}/docs/how-to/profile.md +16 -14
  12. {filly-0.1.0 → filly-0.2.0}/docs/how-to/psychopy.md +47 -37
  13. {filly-0.1.0 → filly-0.2.0}/docs/how-to/stress-test.md +5 -5
  14. {filly-0.1.0 → filly-0.2.0}/docs/how-to/web.md +129 -20
  15. {filly-0.1.0 → filly-0.2.0}/docs/reference/api.md +526 -190
  16. {filly-0.1.0 → filly-0.2.0}/docs/reference/performance.md +244 -6
  17. {filly-0.1.0 → filly-0.2.0}/docs/reference/sample-assets.md +4 -3
  18. filly-0.2.0/docs/reference/upstream-issues.md +596 -0
  19. {filly-0.1.0 → filly-0.2.0}/docs/reference/validation.md +26 -2
  20. {filly-0.1.0 → filly-0.2.0}/docs/tutorials/offscreen.md +6 -7
  21. {filly-0.1.0 → filly-0.2.0}/examples/drifting_grating.py +6 -8
  22. {filly-0.1.0 → filly-0.2.0}/examples/moderngl_shared.py +5 -6
  23. filly-0.2.0/examples/pick.py +206 -0
  24. {filly-0.1.0 → filly-0.2.0}/examples/psychopy_shared.py +18 -18
  25. {filly-0.1.0 → filly-0.2.0}/examples/psychopy_stress.py +22 -10
  26. {filly-0.1.0 → filly-0.2.0}/examples/pyglet_shared.py +7 -8
  27. {filly-0.1.0 → filly-0.2.0}/examples/screenshot.py +10 -12
  28. {filly-0.1.0 → filly-0.2.0}/examples/shapes.py +5 -3
  29. {filly-0.1.0 → filly-0.2.0}/examples/stereo.py +6 -7
  30. {filly-0.1.0 → filly-0.2.0}/examples/zengl_shared.py +6 -5
  31. {filly-0.1.0 → filly-0.2.0}/native/animation.inc +4 -0
  32. {filly-0.1.0 → filly-0.2.0}/native/archive_materials.cpp +3 -3
  33. {filly-0.1.0 → filly-0.2.0}/native/bindings.cpp +267 -131
  34. {filly-0.1.0 → filly-0.2.0}/native/features.inc +285 -164
  35. {filly-0.1.0 → filly-0.2.0}/native/gl_interop.cpp +189 -1
  36. {filly-0.1.0 → filly-0.2.0}/native/gl_interop.h +12 -0
  37. {filly-0.1.0 → filly-0.2.0}/native/gl_interop_web.cpp +66 -1
  38. {filly-0.1.0 → filly-0.2.0}/native/gltf_prepare.cpp +3 -2
  39. {filly-0.1.0 → filly-0.2.0}/native/gltf_prepare.h +5 -1
  40. {filly-0.1.0 → filly-0.2.0}/native/memory.inc +19 -12
  41. {filly-0.1.0 → filly-0.2.0}/native/renderer.cpp +906 -299
  42. {filly-0.1.0 → filly-0.2.0}/native/renderer.h +216 -76
  43. {filly-0.1.0 → filly-0.2.0}/native/resources.inc +28 -19
  44. {filly-0.1.0 → filly-0.2.0}/native/slots.inc +1 -1
  45. {filly-0.1.0 → filly-0.2.0}/pyproject.toml +10 -7
  46. {filly-0.1.0 → filly-0.2.0}/spec.md +86 -33
  47. {filly-0.1.0 → filly-0.2.0}/src/filly/__init__.py +11 -7
  48. {filly-0.1.0 → filly-0.2.0}/src/filly/benchmark.py +2 -2
  49. {filly-0.1.0 → filly-0.2.0}/src/filly/integrations/_host.py +53 -23
  50. filly-0.2.0/src/filly/integrations/moderngl.py +56 -0
  51. {filly-0.1.0 → filly-0.2.0}/src/filly/integrations/psychopy.py +68 -34
  52. filly-0.2.0/src/filly/integrations/pyglet.py +71 -0
  53. filly-0.2.0/src/filly/integrations/zengl.py +103 -0
  54. {filly-0.1.0 → filly-0.2.0}/tests/conftest.py +2 -2
  55. {filly-0.1.0 → filly-0.2.0}/tests/data/README.md +16 -2
  56. filly-0.2.0/tests/data/box/Box.gltf +142 -0
  57. filly-0.2.0/tests/data/box/Box0.bin +0 -0
  58. filly-0.2.0/tests/data/draco-box/Box.bin +0 -0
  59. filly-0.2.0/tests/data/draco-box/Box.gltf +152 -0
  60. {filly-0.1.0 → filly-0.2.0}/tests/test_animation_pointer.py +9 -7
  61. filly-0.2.0/tests/test_asset.py +344 -0
  62. {filly-0.1.0 → filly-0.2.0}/tests/test_camera_aspect.py +4 -4
  63. {filly-0.1.0 → filly-0.2.0}/tests/test_controls.py +2 -2
  64. filly-0.2.0/tests/test_draco.py +46 -0
  65. {filly-0.1.0 → filly-0.2.0}/tests/test_effects.py +57 -15
  66. {filly-0.1.0 → filly-0.2.0}/tests/test_encoding.py +24 -6
  67. {filly-0.1.0 → filly-0.2.0}/tests/test_features.py +15 -11
  68. {filly-0.1.0 → filly-0.2.0}/tests/test_framing.py +79 -10
  69. {filly-0.1.0 → filly-0.2.0}/tests/test_gl_hosts.py +236 -46
  70. {filly-0.1.0 → filly-0.2.0}/tests/test_gltf_surface_camera.py +15 -7
  71. {filly-0.1.0 → filly-0.2.0}/tests/test_headless.py +1 -1
  72. filly-0.1.0/tests/test_host_texture.py → filly-0.2.0/tests/test_imported_texture.py +12 -7
  73. {filly-0.1.0 → filly-0.2.0}/tests/test_interop.py +18 -18
  74. {filly-0.1.0 → filly-0.2.0}/tests/test_iridescence_dielectric.py +3 -2
  75. {filly-0.1.0 → filly-0.2.0}/tests/test_large_scene.py +2 -2
  76. {filly-0.1.0 → filly-0.2.0}/tests/test_lifecycle.py +80 -7
  77. {filly-0.1.0 → filly-0.2.0}/tests/test_light_pointer.py +2 -0
  78. filly-0.2.0/tests/test_lighting.py +250 -0
  79. {filly-0.1.0 → filly-0.2.0}/tests/test_material_limits.py +2 -4
  80. {filly-0.1.0 → filly-0.2.0}/tests/test_material_paths.py +18 -14
  81. {filly-0.1.0 → filly-0.2.0}/tests/test_mesh.py +2 -22
  82. {filly-0.1.0 → filly-0.2.0}/tests/test_node_tree.py +6 -2
  83. filly-0.2.0/tests/test_pick.py +376 -0
  84. {filly-0.1.0 → filly-0.2.0}/tests/test_psychopy.py +126 -45
  85. {filly-0.1.0 → filly-0.2.0}/tests/test_refraction.py +12 -17
  86. {filly-0.1.0 → filly-0.2.0}/tests/test_render_calls.py +6 -6
  87. {filly-0.1.0 → filly-0.2.0}/tests/test_renderer.py +42 -14
  88. {filly-0.1.0 → filly-0.2.0}/tests/test_rendering_options.py +24 -21
  89. {filly-0.1.0 → filly-0.2.0}/tests/test_samples.py +2 -1
  90. {filly-0.1.0 → filly-0.2.0}/tests/test_textures.py +26 -10
  91. {filly-0.1.0 → filly-0.2.0}/tests/test_volume.py +1 -3
  92. {filly-0.1.0 → filly-0.2.0}/tests/test_web.py +215 -19
  93. filly-0.2.0/tests/web/api.html +588 -0
  94. {filly-0.1.0 → filly-0.2.0}/tests/web/handover.html +5 -4
  95. {filly-0.1.0 → filly-0.2.0}/tools/benchmark_refraction.py +6 -6
  96. {filly-0.1.0 → filly-0.2.0}/tools/benchmark_uploads.py +4 -3
  97. {filly-0.1.0 → filly-0.2.0}/tools/build_filament_web.sh +4 -3
  98. filly-0.2.0/tools/build_web_container.ps1 +50 -0
  99. {filly-0.1.0 → filly-0.2.0}/tools/check_sample_assets.py +6 -6
  100. {filly-0.1.0 → filly-0.2.0}/tools/compare_reference.py +6 -6
  101. {filly-0.1.0 → filly-0.2.0}/tools/profile_frame.py +30 -33
  102. {filly-0.1.0 → filly-0.2.0}/tools/profile_matrix.py +5 -5
  103. {filly-0.1.0 → filly-0.2.0}/tools/profile_tracy.py +5 -40
  104. filly-0.2.0/tools/web/Dockerfile +12 -0
  105. filly-0.2.0/uv.lock +7895 -0
  106. {filly-0.1.0 → filly-0.2.0}/web/bindings.cpp +175 -77
  107. {filly-0.1.0 → filly-0.2.0}/web/filly.js +360 -187
  108. filly-0.1.0/src/filly/integrations/moderngl.py +0 -72
  109. filly-0.1.0/src/filly/integrations/pyglet.py +0 -87
  110. filly-0.1.0/src/filly/integrations/zengl.py +0 -80
  111. filly-0.1.0/tests/test_clone.py +0 -269
  112. filly-0.1.0/tests/test_lighting.py +0 -144
  113. filly-0.1.0/tests/web/api.html +0 -355
  114. {filly-0.1.0 → filly-0.2.0}/.github/workflows/wheels.yml +0 -0
  115. {filly-0.1.0 → filly-0.2.0}/.gitignore +0 -0
  116. {filly-0.1.0 → filly-0.2.0}/CMakeLists.txt +0 -0
  117. {filly-0.1.0 → filly-0.2.0}/LICENSE +0 -0
  118. {filly-0.1.0 → filly-0.2.0}/docs/how-to/reference-comparison.md +0 -0
  119. {filly-0.1.0 → filly-0.2.0}/docs/images/README.md +0 -0
  120. {filly-0.1.0 → filly-0.2.0}/docs/images/amber-orthographic.png +0 -0
  121. {filly-0.1.0 → filly-0.2.0}/docs/images/amber-perspective.png +0 -0
  122. {filly-0.1.0 → filly-0.2.0}/docs/images/horse.gif +0 -0
  123. {filly-0.1.0 → filly-0.2.0}/docs/images/horse.png +0 -0
  124. {filly-0.1.0 → filly-0.2.0}/docs/images/iridescence-environment.png +0 -0
  125. {filly-0.1.0 → filly-0.2.0}/docs/images/iridescence-studio.png +0 -0
  126. {filly-0.1.0 → filly-0.2.0}/docs/reference/sample-assets.csv +0 -0
  127. {filly-0.1.0 → filly-0.2.0}/examples/assets/ATTRIBUTION.md +0 -0
  128. {filly-0.1.0 → filly-0.2.0}/examples/assets/wooden_playground_horse.glb +0 -0
  129. {filly-0.1.0 → filly-0.2.0}/native/egl_platform.cpp +0 -0
  130. {filly-0.1.0 → filly-0.2.0}/native/egl_platform.h +0 -0
  131. {filly-0.1.0 → filly-0.2.0}/native/engine_config.h +0 -0
  132. {filly-0.1.0 → filly-0.2.0}/native/json.cpp +0 -0
  133. {filly-0.1.0 → filly-0.2.0}/native/json.h +0 -0
  134. {filly-0.1.0 → filly-0.2.0}/native/libwebp.cmake +0 -0
  135. {filly-0.1.0 → filly-0.2.0}/native/materials/check_refraction.cmake +0 -0
  136. {filly-0.1.0 → filly-0.2.0}/native/materials/diffuse_transmission.mat.in +0 -0
  137. {filly-0.1.0 → filly-0.2.0}/native/materials/embed.cmake +0 -0
  138. {filly-0.1.0 → filly-0.2.0}/native/materials/encode.mat.in +0 -0
  139. {filly-0.1.0 → filly-0.2.0}/native/materials/fxaa.mat +0 -0
  140. {filly-0.1.0 → filly-0.2.0}/native/materials/materials.cmake +0 -0
  141. {filly-0.1.0 → filly-0.2.0}/native/materials/output.cmake +0 -0
  142. {filly-0.1.0 → filly-0.2.0}/native/materials/specular_glossiness.mat.in +0 -0
  143. {filly-0.1.0 → filly-0.2.0}/native/materials/surface.mat.in +0 -0
  144. {filly-0.1.0 → filly-0.2.0}/native/materials/unlit.mat.in +0 -0
  145. {filly-0.1.0 → filly-0.2.0}/native/materials.h +0 -0
  146. {filly-0.1.0 → filly-0.2.0}/native/output_pass.cpp +0 -0
  147. {filly-0.1.0 → filly-0.2.0}/native/output_pass.h +0 -0
  148. {filly-0.1.0 → filly-0.2.0}/native/refraction.h +0 -0
  149. {filly-0.1.0 → filly-0.2.0}/native/shapes.cpp +0 -0
  150. {filly-0.1.0 → filly-0.2.0}/native/vendor/cgltf/LICENSE +0 -0
  151. {filly-0.1.0 → filly-0.2.0}/native/vendor/cgltf/README.md +0 -0
  152. {filly-0.1.0 → filly-0.2.0}/native/vendor/cgltf/cgltf.h +0 -0
  153. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/LICENSE.md +0 -0
  154. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/README.md +0 -0
  155. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/indexcodec.cpp +0 -0
  156. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/meshoptimizer.h +0 -0
  157. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/vertexcodec.cpp +0 -0
  158. {filly-0.1.0 → filly-0.2.0}/native/vendor/meshoptimizer/vertexfilter.cpp +0 -0
  159. {filly-0.1.0 → filly-0.2.0}/native/webp_provider.cpp +0 -0
  160. {filly-0.1.0 → filly-0.2.0}/native/webp_provider.h +0 -0
  161. {filly-0.1.0 → filly-0.2.0}/src/filly/_memory.py +0 -0
  162. {filly-0.1.0 → filly-0.2.0}/src/filly/_stress.py +0 -0
  163. {filly-0.1.0 → filly-0.2.0}/src/filly/integrations/__init__.py +0 -0
  164. {filly-0.1.0 → filly-0.2.0}/src/filly/samples/ATTRIBUTION.md +0 -0
  165. {filly-0.1.0 → filly-0.2.0}/src/filly/samples/__init__.py +0 -0
  166. {filly-0.1.0 → filly-0.2.0}/src/filly/samples/suzanne.glb +0 -0
  167. {filly-0.1.0 → filly-0.2.0}/tests/data/green.ktx2 +0 -0
  168. {filly-0.1.0 → filly-0.2.0}/tests/data/green.webp +0 -0
  169. {filly-0.1.0 → filly-0.2.0}/tests/data/quadrants.webp +0 -0
  170. {filly-0.1.0 → filly-0.2.0}/tests/test_logging.py +0 -0
  171. {filly-0.1.0 → filly-0.2.0}/tests/test_native_imports.py +0 -0
  172. {filly-0.1.0 → filly-0.2.0}/tests/test_native_preparation.py +0 -0
  173. {filly-0.1.0 → filly-0.2.0}/tests/test_scene_extensions.py +0 -0
  174. {filly-0.1.0 → filly-0.2.0}/tests/test_stress.py +0 -0
  175. {filly-0.1.0 → filly-0.2.0}/tests/web/package-lock.json +0 -0
  176. {filly-0.1.0 → filly-0.2.0}/tests/web/package.json +0 -0
  177. {filly-0.1.0 → filly-0.2.0}/tests/web/run_page.mjs +0 -0
  178. {filly-0.1.0 → filly-0.2.0}/tools/build_filament_linux.py +0 -0
  179. {filly-0.1.0 → filly-0.2.0}/tools/build_filly_web.sh +0 -0
  180. {filly-0.1.0 → filly-0.2.0}/tools/check_native_imports.py +0 -0
  181. {filly-0.1.0 → filly-0.2.0}/tools/ci_linux.sh +0 -0
  182. {filly-0.1.0 → filly-0.2.0}/tools/fetch_filament.py +0 -0
  183. {filly-0.1.0 → filly-0.2.0}/tools/make_suzanne.py +0 -0
  184. {filly-0.1.0 → filly-0.2.0}/tools/profile_rdc_replay.py +0 -0
  185. {filly-0.1.0 → filly-0.2.0}/web/CMakeLists.txt +0 -0
  186. {filly-0.1.0 → filly-0.2.0}/web/gl_tracking.js +0 -0
@@ -1,6 +1,6 @@
1
1
  Misc things
2
2
  - ~~psychopy frequently operates in floats, even for window sizes. Accept floats
3
- for SharedTarget and coerce to ints?~~ Done: target sizes accept integral floats;
3
+ for HostTarget and coerce to ints?~~ Done: target sizes accept integral floats;
4
4
  fractional or non-finite values raise ValueError.
5
5
  - KHR_xmp_json_ld?
6
6
  - Feels like wheel was reinvented unnecessarily, e.g. some of the custom shaders?
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: filly
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: A Python 3D renderer for glTF scenes, built on Google Filament
5
5
  Author: Alex Forrence
6
6
  License-Expression: MIT
@@ -9,19 +9,19 @@ Project-URL: Homepage, https://github.com/aforren1/filly
9
9
  Project-URL: Documentation, https://github.com/aforren1/filly#readme
10
10
  Project-URL: Issues, https://github.com/aforren1/filly/issues
11
11
  Requires-Python: >=3.10
12
- Requires-Dist: numpy>=1.24
12
+ Requires-Dist: numpy>=1.22
13
13
  Provides-Extra: examples
14
- Requires-Dist: Pillow>=10; extra == "examples"
14
+ Requires-Dist: Pillow>=9.1; extra == "examples"
15
15
  Provides-Extra: psychopy
16
- Requires-Dist: psychopy-lib==2026.2.4; extra == "psychopy"
16
+ Requires-Dist: psychopy>=2024.2; extra == "psychopy"
17
17
  Provides-Extra: pyglet
18
18
  Requires-Dist: pyglet>=1.4.11; extra == "pyglet"
19
19
  Provides-Extra: moderngl
20
20
  Requires-Dist: pyglet>=1.4.11; extra == "moderngl"
21
- Requires-Dist: moderngl>=5.12; extra == "moderngl"
21
+ Requires-Dist: moderngl>=5.7; extra == "moderngl"
22
22
  Provides-Extra: zengl
23
23
  Requires-Dist: pyglet>=1.4.11; extra == "zengl"
24
- Requires-Dist: zengl>=2.7; extra == "zengl"
24
+ Requires-Dist: zengl>=2.1; extra == "zengl"
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # filly
@@ -41,10 +41,9 @@ such as PsychoPy experiments or pyglet, moderngl, and zengl programs.
41
41
 
42
42
  ## Install
43
43
 
44
- filly is not on PyPI yet. [Build it from source](docs/how-to/build.md).
44
+ You can install filly from [PyPI](https://pypi.org/project/filly/) or [build it from source](docs/how-to/build.md).
45
45
 
46
- It needs Python 3.10 or later, Windows or Linux on x86_64, and an OpenGL 4.1 driver. On Linux,
47
- offscreen rendering works without a display. macOS is not supported at this time.
46
+ It needs Python 3.10 or later, Windows or Linux on x86_64, and an OpenGL 4.1 driver. On Linux, offscreen rendering works without a display. macOS is not supported at this time.
48
47
 
49
48
  ## Render an image
50
49
 
@@ -57,10 +56,8 @@ with filly.Renderer() as renderer:
57
56
  horse = scene.load("examples/assets/wooden_playground_horse.glb")
58
57
  horse.apply_animation(0, 1.25) # a pose from its spring animation
59
58
 
60
- camera = scene.create_camera()
61
- camera.set_perspective(fov_y=30, near=0.1, far=100)
62
- camera.frame(horse, direction=(-1, -0.3, -1), aspect=800 / 600)
63
- scene.camera = camera
59
+ scene.camera = renderer.create_camera()
60
+ scene.camera.frame(horse, direction=(-1, -0.3, -1))
64
61
  scene.add_directional_light(direction=(-1, -1, -2), intensity=100_000)
65
62
 
66
63
  target = renderer.create_render_target(width=800, height=600)
@@ -74,20 +71,18 @@ filly renders into a texture that PsychoPy draws directly, with no copy through
74
71
 
75
72
  ```python
76
73
  from psychopy import core, visual
77
- from filly.integrations.psychopy import SharedTarget, create_renderer
74
+ from filly.integrations.psychopy import HostTarget, create_renderer
78
75
 
79
76
  win = visual.Window((1024, 768), units="pix")
80
77
  renderer = create_renderer(win) # shares the window's OpenGL context
81
78
  scene = renderer.create_scene()
82
79
  horse = scene.load("examples/assets/wooden_playground_horse.glb")
83
- camera = scene.create_camera()
84
- camera.set_perspective(fov_y=30, near=0.1, far=100)
85
- camera.frame(horse, aspect=1)
86
- scene.camera = camera
80
+ scene.camera = renderer.create_camera()
81
+ scene.camera.frame(horse)
87
82
  scene.add_directional_light(direction=(-1, -1, -2), intensity=100_000)
88
83
 
89
- target = SharedTarget(renderer, win, 512, 512)
90
- stim = target.as_psychopy_texture() # an ImageStim
84
+ target = HostTarget(renderer, win, width=512, height=512)
85
+ stim = target.create_image_stim()
91
86
  clock = core.Clock()
92
87
  while clock.getTime() < 3:
93
88
  horse.apply_animation(0, clock.getTime())
@@ -95,12 +90,11 @@ while clock.getTime() < 3:
95
90
  stim.draw()
96
91
  win.flip()
97
92
 
98
- target.close()
99
93
  renderer.close()
100
94
  win.close()
101
95
  ```
102
96
 
103
- Close the target, then the renderer, then the window. The same pattern works with a
97
+ Close the renderer before the window. The same pattern works with a
104
98
  [pyglet, moderngl, or zengl window](docs/how-to/gl-hosts.md).
105
99
 
106
100
  ## Features
@@ -116,6 +110,8 @@ Close the target, then the renderer, then the window. The same pattern works wit
116
110
  - **Predictable output:** sRGB (default) or linear. Effects such as antialiasing,
117
111
  fog, and bloom are off until you enable them. With default settings, the same scene gives the same pixels on every frame.
118
112
  - **Several views per frame:** each `render()` call can take its own camera and viewport, for example for side-by-side stereo.
113
+ - **Picking:** find the model, node, and world position under a pixel of a rendered frame, for
114
+ example where a participant clicked. See [picking](docs/reference/api.md#picking).
119
115
  - **Browsers:** the same core compiles to WebAssembly for WebGL2. It renders into a texture that the page's own renderer draws, such as PIXI in PsychoJS. See
120
116
  [the web build](docs/explanation/web.md).
121
117
 
@@ -126,11 +122,13 @@ See the `examples/` folder.
126
122
  | Example | Shows |
127
123
  | --- | --- |
128
124
  | `screenshot.py` | The animation above (`--gif`), or a still image, rendered offscreen. |
129
- | `psychopy_shared.py` | A model in a PsychoPy window. Pass your own GLB path. |
130
- | `pyglet_shared.py`, `moderngl_shared.py`, `zengl_shared.py` | The same in other OpenGL hosts. |
125
+ | `psychopy_shared.py` | A model in a PsychoPy window with all the bells and whistles. Pass your own GLB path. |
126
+ | `psychopy_stress.py` | Repeated PsychoPy trials, with frame timing and memory reports. See [measure timing](docs/how-to/stress-test.md). |
127
+ | `pyglet_shared.py`, `moderngl_shared.py`, `zengl_shared.py` | The same (minus the bells and whistles) in other OpenGL hosts. |
131
128
  | `drifting_grating.py` | A texture updated every frame on a tilted plane. |
132
129
  | `stereo.py` | Two cameras side by side in one window. |
133
130
  | `shapes.py` | Generated and deforming meshes. |
131
+ | `pick.py` | Click a model in a pyglet window: picking, with the hit node, point, and latency. `--auto` clicks by itself. |
134
132
 
135
133
  ## Documentation
136
134
 
@@ -141,8 +139,10 @@ See the `examples/` folder.
141
139
  [compare with Filament's viewer](docs/how-to/reference-comparison.md),
142
140
  [use filly in a web page](docs/how-to/web.md)
143
141
  - Reference: [API](docs/reference/api.md), [performance](docs/reference/performance.md),
144
- [validation](docs/reference/validation.md), [sample assets](docs/reference/sample-assets.md)
142
+ [validation](docs/reference/validation.md), [sample assets](docs/reference/sample-assets.md),
143
+ [upstream Filament issues](docs/reference/upstream-issues.md)
145
144
  - Explanation: [design and limits](docs/explanation/design.md),
145
+ [filly and Filament](docs/explanation/filament.md),
146
146
  [tested assumptions](docs/explanation/assumptions.md),
147
147
  [material precompilation](docs/explanation/material-precompilation.md),
148
148
  [the web build](docs/explanation/web.md)
@@ -15,10 +15,9 @@ such as PsychoPy experiments or pyglet, moderngl, and zengl programs.
15
15
 
16
16
  ## Install
17
17
 
18
- filly is not on PyPI yet. [Build it from source](docs/how-to/build.md).
18
+ You can install filly from [PyPI](https://pypi.org/project/filly/) or [build it from source](docs/how-to/build.md).
19
19
 
20
- It needs Python 3.10 or later, Windows or Linux on x86_64, and an OpenGL 4.1 driver. On Linux,
21
- offscreen rendering works without a display. macOS is not supported at this time.
20
+ It needs Python 3.10 or later, Windows or Linux on x86_64, and an OpenGL 4.1 driver. On Linux, offscreen rendering works without a display. macOS is not supported at this time.
22
21
 
23
22
  ## Render an image
24
23
 
@@ -31,10 +30,8 @@ with filly.Renderer() as renderer:
31
30
  horse = scene.load("examples/assets/wooden_playground_horse.glb")
32
31
  horse.apply_animation(0, 1.25) # a pose from its spring animation
33
32
 
34
- camera = scene.create_camera()
35
- camera.set_perspective(fov_y=30, near=0.1, far=100)
36
- camera.frame(horse, direction=(-1, -0.3, -1), aspect=800 / 600)
37
- scene.camera = camera
33
+ scene.camera = renderer.create_camera()
34
+ scene.camera.frame(horse, direction=(-1, -0.3, -1))
38
35
  scene.add_directional_light(direction=(-1, -1, -2), intensity=100_000)
39
36
 
40
37
  target = renderer.create_render_target(width=800, height=600)
@@ -48,20 +45,18 @@ filly renders into a texture that PsychoPy draws directly, with no copy through
48
45
 
49
46
  ```python
50
47
  from psychopy import core, visual
51
- from filly.integrations.psychopy import SharedTarget, create_renderer
48
+ from filly.integrations.psychopy import HostTarget, create_renderer
52
49
 
53
50
  win = visual.Window((1024, 768), units="pix")
54
51
  renderer = create_renderer(win) # shares the window's OpenGL context
55
52
  scene = renderer.create_scene()
56
53
  horse = scene.load("examples/assets/wooden_playground_horse.glb")
57
- camera = scene.create_camera()
58
- camera.set_perspective(fov_y=30, near=0.1, far=100)
59
- camera.frame(horse, aspect=1)
60
- scene.camera = camera
54
+ scene.camera = renderer.create_camera()
55
+ scene.camera.frame(horse)
61
56
  scene.add_directional_light(direction=(-1, -1, -2), intensity=100_000)
62
57
 
63
- target = SharedTarget(renderer, win, 512, 512)
64
- stim = target.as_psychopy_texture() # an ImageStim
58
+ target = HostTarget(renderer, win, width=512, height=512)
59
+ stim = target.create_image_stim()
65
60
  clock = core.Clock()
66
61
  while clock.getTime() < 3:
67
62
  horse.apply_animation(0, clock.getTime())
@@ -69,12 +64,11 @@ while clock.getTime() < 3:
69
64
  stim.draw()
70
65
  win.flip()
71
66
 
72
- target.close()
73
67
  renderer.close()
74
68
  win.close()
75
69
  ```
76
70
 
77
- Close the target, then the renderer, then the window. The same pattern works with a
71
+ Close the renderer before the window. The same pattern works with a
78
72
  [pyglet, moderngl, or zengl window](docs/how-to/gl-hosts.md).
79
73
 
80
74
  ## Features
@@ -90,6 +84,8 @@ Close the target, then the renderer, then the window. The same pattern works wit
90
84
  - **Predictable output:** sRGB (default) or linear. Effects such as antialiasing,
91
85
  fog, and bloom are off until you enable them. With default settings, the same scene gives the same pixels on every frame.
92
86
  - **Several views per frame:** each `render()` call can take its own camera and viewport, for example for side-by-side stereo.
87
+ - **Picking:** find the model, node, and world position under a pixel of a rendered frame, for
88
+ example where a participant clicked. See [picking](docs/reference/api.md#picking).
93
89
  - **Browsers:** the same core compiles to WebAssembly for WebGL2. It renders into a texture that the page's own renderer draws, such as PIXI in PsychoJS. See
94
90
  [the web build](docs/explanation/web.md).
95
91
 
@@ -100,11 +96,13 @@ See the `examples/` folder.
100
96
  | Example | Shows |
101
97
  | --- | --- |
102
98
  | `screenshot.py` | The animation above (`--gif`), or a still image, rendered offscreen. |
103
- | `psychopy_shared.py` | A model in a PsychoPy window. Pass your own GLB path. |
104
- | `pyglet_shared.py`, `moderngl_shared.py`, `zengl_shared.py` | The same in other OpenGL hosts. |
99
+ | `psychopy_shared.py` | A model in a PsychoPy window with all the bells and whistles. Pass your own GLB path. |
100
+ | `psychopy_stress.py` | Repeated PsychoPy trials, with frame timing and memory reports. See [measure timing](docs/how-to/stress-test.md). |
101
+ | `pyglet_shared.py`, `moderngl_shared.py`, `zengl_shared.py` | The same (minus the bells and whistles) in other OpenGL hosts. |
105
102
  | `drifting_grating.py` | A texture updated every frame on a tilted plane. |
106
103
  | `stereo.py` | Two cameras side by side in one window. |
107
104
  | `shapes.py` | Generated and deforming meshes. |
105
+ | `pick.py` | Click a model in a pyglet window: picking, with the hit node, point, and latency. `--auto` clicks by itself. |
108
106
 
109
107
  ## Documentation
110
108
 
@@ -115,8 +113,10 @@ See the `examples/` folder.
115
113
  [compare with Filament's viewer](docs/how-to/reference-comparison.md),
116
114
  [use filly in a web page](docs/how-to/web.md)
117
115
  - Reference: [API](docs/reference/api.md), [performance](docs/reference/performance.md),
118
- [validation](docs/reference/validation.md), [sample assets](docs/reference/sample-assets.md)
116
+ [validation](docs/reference/validation.md), [sample assets](docs/reference/sample-assets.md),
117
+ [upstream Filament issues](docs/reference/upstream-issues.md)
119
118
  - Explanation: [design and limits](docs/explanation/design.md),
119
+ [filly and Filament](docs/explanation/filament.md),
120
120
  [tested assumptions](docs/explanation/assumptions.md),
121
121
  [material precompilation](docs/explanation/material-precompilation.md),
122
122
  [the web build](docs/explanation/web.md)
@@ -264,11 +264,11 @@ path and sets it in gltfio's parse, which cgltf then skips. No GLB is assembled.
264
264
  | Sponza | 0.88 s | 51 to 55 ms | 0.49 to 0.63 s | 243 to 269 ms |
265
265
  | NodePerformanceTest | 3.6 to 3.8 s | 1.4 to 1.8 s | 2.6 to 2.7 s | 2.3 to 2.6 s |
266
266
 
267
- - `Renderer.finish()` waits by request. `OffscreenTarget.read()` must wait for the readback.
267
+ - `Renderer.finish()` waits by request. `RenderTarget.read()` must wait for the readback.
268
268
  - `ImportedTarget.close()` must wait for imported textures, because the host can then delete or
269
269
  reuse the texture name. For owned targets the wait is conservative.
270
- - `set_environment()` waits after the prefilter work. Filament orders the destroy commands itself,
271
- so the wait is conservative. It keeps setup cost out of later frames.
270
+ - `Renderer.load_environment()` and `create_environment()` wait after the prefilter work.
271
+ Filament orders the destroy commands itself, so the wait is conservative. It keeps setup cost out of later frames.
272
272
 
273
273
  **Verdict.** The load wait is not needed for memory safety. It is useful for experiments.
274
274
 
@@ -5,10 +5,14 @@ Nanobind converts Python values at the boundary. Filament types stay in the nati
5
5
  The Windows WGL and Linux GLX adapters are isolated in `native/gl_interop.cpp`, and the Linux
6
6
  headless EGL platform in `native/egl_platform.cpp`.
7
7
 
8
+ For how filly's concepts map to Filament and gltfio, and which code depends on Filament
9
+ internals, see [filly and Filament](filament.md).
10
+
8
11
  ## Ownership
9
12
 
10
13
  Every native resource retains the engine state. The engine tracks resources through weak references.
11
- This prevents ownership cycles. A scene retains its models, cameras, and added lights.
14
+ This prevents ownership cycles. A scene retains its models, its added lights, and its current
15
+ camera. Cameras belong to the renderer.
12
16
  Objects can keep the engine alive after the Python `Renderer` reference is deleted.
13
17
 
14
18
  `renderer.close()` waits for outstanding work and destroys resources before the engine.
@@ -18,31 +22,46 @@ Model and light `close()` release scene ownership and queue native destruction.
18
22
  handles invalidate their nodes, node-local materials, and imported lights. A removed imported light
19
23
  keeps its transform entity so animation can continue. Added lights use a separate owner whose
20
24
  closed state prevents old handles from referring to a replacement light. `Scene.close()` releases
21
- the scene's children, detaches its cameras from other scenes, and then destroys its view,
22
- Filament scene, color grading, and environment.
23
-
24
- A loaded glTF asset is a shared native object. The first model and each `clone()` are gltfio
25
- instances of it: `AssetLoader::createInstance()` adds entities, material instances, lights,
26
- skins, and an animator, and reuses vertex buffers, textures, and materials. gltfio 1.77.1 has no
27
- way to destroy one instance, so closing a model only removes its entities from the scene; the
28
- asset and all its instances are destroyed when the last model closes. `createInstance()` needs
29
- the asset's source data, about the file size in CPU memory, and each instance needs the
30
- preparation tables for the custom materials, property animation, cameras, and morph weights. Cloning is
31
- therefore opt-in: `scene.load(..., clonable=True)` keeps them, and the default calls
32
- `releaseSourceData()` and drops the tables after the first instance. For DamagedHelmet
33
- (3.6 MiB file) this saves 3.7 MiB of private bytes per loaded asset. The process grows by
34
- 113 MiB per loaded DamagedHelmet on the tested Intel GPU, most of it texture memory that the
35
- integrated GPU allocates in system memory, so the saving is small for textured assets. Name
36
- lookups use each instance's own node table, because gltfio's name and entity lists cover all
37
- instances.
38
-
39
- The asset belongs to the renderer; each model holds it. A clone can therefore go into another
40
- scene of the same renderer, and closing a scene closes only its own models. Textures that the
41
- material provider decodes for filly's custom materials are kept by source in the asset's tables,
42
- so clones share them as they share gltfio's textures. Before, each clone decoded them again and
43
- kept the copies until the asset closed.
44
-
45
- `Model.memory` estimates what the asset holds. gltfio lists no textures or buffers, so the
25
+ the scene's children (a closing model detaches its imported cameras from every scene), and then
26
+ destroys its view, Filament scene, color grading, and the scene's environment light and skybox. Renderer cameras, assets, and
27
+ environments stay open for other scenes.
28
+
29
+ A loaded glTF asset is a shared native object that the renderer owns, as Filament's engine owns
30
+ its textures and buffers. Each model is a gltfio instance of it:
31
+ `AssetLoader::createInstance()` adds entities, material instances, lights, skins, and an
32
+ animator, and reuses vertex buffers, textures, and materials. gltfio 1.77.1 has no way to destroy
33
+ one instance, so closing a model only removes its entities from the scene; the asset and all
34
+ its instances are destroyed when the asset handle and the last model close.
35
+ `createInstance()` needs the asset's source data, about the file size in CPU memory, and each
36
+ instance needs the preparation tables for the custom materials, property animation, cameras, and
37
+ morph weights. An open `Asset` keeps them; `Asset.close()` calls `releaseSourceData()` and drops
38
+ the tables. `scene.load()` is a load, one `add()`, and that release, so a model that nothing
39
+ adds again costs no source data. For DamagedHelmet (3.6 MiB file) the release saves 3.7 MiB of
40
+ private bytes. The process grows by 113 MiB per loaded DamagedHelmet on the tested Intel GPU,
41
+ most of it texture memory that the integrated GPU allocates in system memory, so the saving is
42
+ small for textured assets. Name lookups use each instance's own node table, because gltfio's
43
+ name and entity lists cover all instances.
44
+
45
+ An asset handle replaced `scene.load(source, clonable=True)` and `model.clone(scene=...)`. With
46
+ those, an asset could only be created through a scene: keeping a prototype needed a hidden model
47
+ in some scene, and closing that scene closed the prototype. A clone also copied nothing that a
48
+ new instance lacks: it started from the asset's authored state, as an added model does. The
49
+ asset belongs to the renderer and each model holds it, so any scene of the renderer can add it,
50
+ and closing a scene closes only its own models. Textures that the material provider decodes for
51
+ filly's custom materials are kept by source in the asset's tables, so models share them as they
52
+ share gltfio's textures. Before, each instance decoded them again and kept the copies until the
53
+ asset closed.
54
+
55
+ Loading creates the first instance before `ResourceLoader::loadResources()`, and the first
56
+ `add()` uses it. gltfio uploads morph target data only in `loadResources()`, which runs once per
57
+ asset, and only for the instances that exist then; an asset loaded without instances gave every
58
+ model empty morph targets. Later instances get their
59
+ morph target positions from the source data and their morph tangent frames from gltfio's own
60
+ `TangentsJob`, under the same conditions as `loadResources()`, so every model of an asset
61
+ morphs the same. A test compares the lit, morphed first and second models pixel by pixel.
62
+ Before, clones had empty morph targets.
63
+
64
+ `Asset.memory` and `Model.memory` estimate what the asset holds. gltfio lists no textures or buffers, so the
46
65
  estimate collects them itself: a texture provider wrapper records the textures that gltfio's
47
66
  providers create, and each is sized from its internal format, dimensions, and mip levels. Vertex,
48
67
  index, morph target, and bone buffers are sized at preparation from the glTF accessors with the
@@ -88,8 +107,13 @@ the warning, and Filament has no other switch. A 60 Hz loop, or a 1 ms sleep per
88
107
  log it. In embedded mode, the host owns the window and every
89
108
  display-buffer swap. See [tested assumptions](assumptions.md).
90
109
 
91
- Shadows, refraction, FXAA, MSAA, SSAO, bloom, dithering, fog, depth of field, and vignette are
92
- scene properties and disabled by default. The PsychoPy demo enables refraction and FXAA.
110
+ Shadows, FXAA, MSAA, SSAO, bloom, dithering, fog, depth of field, and vignette are
111
+ scene properties and disabled by default. The PsychoPy demo enables FXAA. Screen-space
112
+ refraction is not an option. Every scene has it on: transmission and volume materials render as
113
+ flat white without it, and Filament runs the pass only in frames that draw such a material.
114
+ Until October 2026 it was a scene property. That property looked like a render option, but no
115
+ material depended on it; only a load-time warning did, which gave the rule "set it before
116
+ loading glass".
93
117
  Temporal antialiasing, TAA upscaling, screen-space reflections, and dynamic resolution are not
94
118
  exposed. In Filament 1.77.1 `PostProcessManager`, TAA and screen-space reflections import the
95
119
  previous frame's history buffer (`FrameHistory::getPrevious()`), and dynamic resolution scales
@@ -149,7 +173,7 @@ the UV sets of the glTF textures. Nothing is compiled.
149
173
  A generated mesh is loaded as a one-triangle glTF with the requested material and attributes,
150
174
  then its renderable gets the mesh's own vertex and index buffers (`setGeometryAt()`). The loader
151
175
  therefore gives it the same material as a glTF file with those factors, and the model gets
152
- nodes, material handles, clones, and closing without a second code path. The placeholder's
176
+ nodes, material handles, and closing without a second code path. The placeholder's
153
177
  accessor bounds are the mesh bounds. Positions, tangent frames, UVs, and colors are separate
154
178
  vertex buffers, so an update uploads only what changed. A mesh without colors gets a white
155
179
  `UBYTE4` color buffer, and `UV1` reads the `UV0` buffer: archive entries serve every material,
@@ -159,12 +183,13 @@ its method that writes into the staging buffer; a test checks that both give the
159
183
  Missing normals are area-weighted vertex normals, because shared vertices of an indexed mesh
160
184
  should be smooth; flat faces need their own vertices.
161
185
 
162
- A host texture input uses the same fences as a shared render target, in the other direction.
186
+ An imported texture (`ImportedTexture`) uses the same fences as a shared render target, in the
187
+ other direction.
163
188
  Leaving `write()` places a host fence, and the next render of a scene whose models sample the
164
189
  texture queues a Filament-side wait on it before the frame. After such a render, Filament places
165
- one fence, which the texture shares with the target and with the other sampled host textures,
190
+ one fence, which the texture shares with the target and with the other sampled imported textures,
166
191
  and entering `write()` queues a host-side wait on it. Each Filament fence costs a `glFlush()` on
167
- Filament's thread, so a render places at most one. A scene that does not sample a host texture
192
+ Filament's thread, so a render places at most one. A scene that does not sample an imported texture
168
193
  neither waits for its writes nor fences it. Neither side waits on the CPU.
169
194
 
170
195
  Two kinds of per-frame work run only when something changed. A camera whose projection follows
@@ -182,10 +207,33 @@ from the bind frame moves it away from the geometry. Preparation computes the bo
182
207
  asset: accessor boxes through node transforms for rigid meshes, and every vertex skinned with the
183
208
  rest pose for skinned meshes. A 1,000,000-vertex skinned mesh loaded in 120 ms against 65 ms
184
209
  for the same mesh without a skin; the difference also includes gltfio's own skin work and the
185
- joint and weight uploads (Iris Xe laptop, September 30, 2026). Clones share the box, and
210
+ joint and weight uploads (Iris Xe laptop, September 30, 2026). Models of an asset share the box, and
186
211
  frames do no bounds work. The same rest-pose box, in the mesh node's frame, replaces gltfio's
187
212
  culling box for skinned renderables, which had the same fault and could cull a visible mesh.
188
213
 
214
+ gltfio also has `FilamentInstance::recomputeBoundingBoxes()`. It skins every vertex with the
215
+ joints' current transforms and sets the culling boxes of one instance. filly does not use it.
216
+ A prototype that called it once per asset and copied the boxes to later models gave these results
217
+ (October 12, 2026, Iris Xe laptop):
218
+
219
+ - It ignores morph targets on skinned primitives. The box of a skinned mesh does not include its
220
+ morph targets.
221
+ - It reads `JOINTS_0` and `WEIGHTS_0` without checks. A skinned primitive without these
222
+ attributes stopped the process with an access violation; filly loads it. A joint index past
223
+ the end of the skin's joint list reads outside an array.
224
+ - One large primitive is slower: 149 ms against 54 ms for a skinned primitive with 1,000,000
225
+ vertices. gltfio runs one job per primitive, so the same vertices in 8 primitives took 34 ms
226
+ against 58 ms.
227
+ - It reads every position of rigid meshes: 6 ms for 1,000,000 vertices, against less than
228
+ 0.1 ms for accessor bounds. In the Khronos sample assets, the accessor bounds gave the same
229
+ boxes as the data.
230
+ - It joins the primitive boxes of a mesh in the node's frame. filly moves each primitive box into
231
+ the model's frame first, which gives a smaller box under a rotated node.
232
+
233
+ On the 149 Khronos sample assets that load (of 150) and the horse demo, `Model.bounds` and
234
+ `Node.bounds` from the two methods agreed within 0.01% of the box size, except in three assets
235
+ with rotated multi-primitive meshes. There, filly's boxes were smaller, by up to 0.14% of the model and 10% of one node.
236
+
189
237
  ## Output encoding
190
238
 
191
239
  The encoding does not depend on other options. Earlier, disabling postprocessing wrote linear
@@ -211,7 +259,7 @@ value is encoded:
211
259
  | --- | --- | --- |
212
260
  | None (default) | The scene view, rendering straight into the RGBA16F buffer | No |
213
261
  | MSAA | Filament's multisampled RGB16F buffer, resolved and copied into the RGBA16F buffer | No |
214
- | Refraction, SSAO, shadows, fog | Their passes; the color pass as without them | No |
262
+ | Refraction materials (transmission, volume), SSAO, shadows, fog | Their passes; the color pass as without them | No |
215
263
  | Transparent view | Filament's RGBA16F color buffer, blended into the RGBA16F buffer | No |
216
264
  | FXAA | Nothing: filly's FXAA pass runs after the encode pass, on the encoded image | No |
217
265
  | Dithering | Nothing: the encode pass adds the noise after the transfer function | No |
@@ -376,7 +424,8 @@ setting has no effect there.
376
424
 
377
425
  ## Shared OpenGL textures
378
426
 
379
- The default Filament WGL platform does not implement external sync creation. The wrapper supplies
427
+ Filament's desktop platforms (WGL, GLX, and EGL) do not implement external sync creation; only
428
+ Android's EGL platform does. See [upstream issues](../reference/upstream-issues.md). The wrapper supplies
380
429
  a small WGL, GLX, or EGL platform subclass through Filament's public platform interface, for
381
430
  offscreen engines too. It places GL fences, waits, and the `GL_FRAMEBUFFER_SRGB` setting in the Filament
382
431
  command stream through `Engine::createSync()`.
@@ -394,7 +443,23 @@ waited for it; its release replaces the unconsumed host fence.
394
443
  This orders both producer and consumer GPU work. Normal frames do not use `glFinish()` or CPU
395
444
  framebuffer copies. Explicit shutdown and image readback can block for completion.
396
445
 
397
- The PsychoPy adapter uses `ImageStim`'s texture-provider interface and owns the host texture.
446
+ The adapters own OpenGL objects that the renderer does not know: the host texture, a draw
447
+ program, and PsychoPy stimuli. An imported target therefore takes an optional callback, which
448
+ `Renderer.close()` calls before it releases anything else, while the host context can still be
449
+ made current. Each adapter passes a callback that closes its target. The callback holds the
450
+ target weakly: the renderer holds the callback, and a strong reference would form a cycle
451
+ through native objects that Python's garbage collector cannot see. The callbacks run in
452
+ `Renderer.close()` only, not when the renderer's state is destroyed: each target holds that
453
+ state, so no target exists then. Thus the renderer's close leaves no host objects behind, and
454
+ only the window must close last.
455
+
456
+ `draw()` blends premultiplied source-over by default in every adapter, because that is what
457
+ Filament writes. The pyglet and moderngl adapters draw a quad in native code that saves and
458
+ restores the OpenGL state that it changes. moderngl cannot read its blend state back, and
459
+ libraries that cache OpenGL state need it restored. A blit, which the pyglet adapter used
460
+ before, cannot blend.
461
+
462
+ The PsychoPy adapter draws with `ImageStim` and owns the host texture.
398
463
  Its draw method runs inside `acquire()`. PsychoPy remains responsible for
399
464
  `win.flip()`. PsychoPy skips a context switch when it believes its window is current, which is
400
465
  wrong after other code switched contexts through pyglet, so the adapter also checks pyglet's
@@ -420,8 +485,12 @@ adapter converts sampled RGB to straight RGB in a shader, before ImageStim appli
420
485
  The temporary shader selection is restored after each draw so regular PsychoPy stimuli keep their
421
486
  normal rendering behavior. The host window uses average blending.
422
487
 
423
- The adapter is pinned to psychopy-lib 2026.2.4 because it uses PsychoPy's context and texture
424
- internals. Other versions and backends need separate tests.
488
+ The adapter uses PsychoPy's context and texture internals, so each PsychoPy version needs its
489
+ own test. It does not use texture providers (an `image` with `colorTexture`), which
490
+ `ImageStim` takes in PsychoPy 2026.2 but not in 2024.2. It gives `ImageStim` a placeholder
491
+ image and then replaces the texture name. The legacy GL path records the program and the
492
+ texture in a display list, so the adapter rebuilds the list in its first draw, with the
493
+ premultiplied program in place. Other backends need separate tests.
425
494
 
426
495
  ## Dependencies
427
496
 
@@ -479,7 +548,7 @@ A load has these steps:
479
548
  7. `AssetLoader::createInstance()` with index markers (below), then `ResourceLoader`.
480
549
 
481
550
  The loaded buffer memory lives with the asset until gltfio releases its source data, which is
482
- after the first instance unless `clonable=True`.
551
+ until the `Asset` closes; `scene.load()` releases it after its one model.
483
552
 
484
553
  gltfio identifies nothing by glTF index. Its `MaterialProvider` callbacks get a key, a UV map, a
485
554
  label (the material name), and the extras text, and its entity lists are not in node order. While
@@ -536,16 +605,20 @@ the pinned SDK implements this path despite older wording in its LightManager he
536
605
  map size and bias settings use the same options for authored and imported lights. Shadows remain
537
606
  explicit opt-in at both scene and light levels.
538
607
 
539
- The scene retains the source environment cubemap and a Skybox object alongside the filtered IBL
540
- textures. Visibility attaches or detaches the skybox without changing lighting. IndirectLight's
608
+ An `Environment` holds the source environment cubemap and the filtered IBL textures, and the
609
+ renderer owns it. Each scene that uses it builds its own IndirectLight and Skybox from those
610
+ textures, because the IndirectLight holds the intensity and rotation, which are scene settings.
611
+ Building them took 0.03 to 0.3 ms per scene, where filtering the panorama again took 54 to 83 ms.
612
+ Visibility attaches or detaches the skybox without changing lighting. IndirectLight's
541
613
  rotation controls both the skybox and native IBL; custom diffuse backlighting receives the same
542
- rotation update. Clearing or replacing the environment detaches the skybox before destroying its
543
- texture. A visible skybox writes opaque background pixels even in a transparent view.
544
-
545
- Python normalizes modern meshopt buffers and expands instance attributes before animation
546
- decoding and native loading. A namespaced meshoptimizer 1.0 decoder avoids symbol collisions
547
- with the SDK's older decoder. Instance children share mesh resources, but this path does not
548
- reduce draw calls with explicit GPU batching. Node visibility is evaluated in parent-first order
614
+ rotation update. Removing or replacing a scene's environment destroys only that scene's light and
615
+ skybox; closing the environment removes it from every scene first, then destroys its
616
+ textures. A visible skybox writes opaque background pixels even in a transparent view.
617
+
618
+ Native [glTF preparation](#gltf-preparation) decodes meshopt buffers and expands
619
+ `EXT_mesh_gpu_instancing` before gltfio loads the document. Instance children share mesh resources and
620
+ material instances, so Filament's automatic instancing, which every renderer turns on, draws up
621
+ to 64 of them per call (8 in WebGL). Node visibility is evaluated in parent-first order
549
622
  without per-frame allocation and controls scene membership for meshes and lights.
550
623
 
551
624
  Volume materials use filly's solid refraction entries. Filament 1.77.1 passes only a mesh
@@ -583,14 +656,15 @@ cubemap is used as is, and the spherical harmonics in its metadata supply diffus
583
656
  comes from the roughest reflection level. Filament's KTX parser and upload abort on malformed
584
657
  input, so the wrapper checks the file structure first. The custom diffuse-transmission material
585
658
  needs an irradiance cubemap; it is filtered from a full-mip copy of the IBL's sharpest level.
586
- The filter runs only when a model with that material is in the scene, at the environment load or
587
- at the model load, whichever comes later: it cost about 60 ms per environment, plus about 35 ms
659
+ The filter runs only when a model with that material is in a scene that uses the environment,
660
+ when the scene gets the environment or the model, whichever comes later, and once per
661
+ environment: it cost about 60 ms, plus about 35 ms
588
662
  to build the filter once per renderer (256-pixel IBL, desktop). It performs GPU work and a
589
663
  completion wait during setup. Each prefilter object builds its material on first use. Shader
590
664
  compilation in the driver on first use, image decoding, and environment filtering belong outside
591
665
  a trial's frame loop.
592
666
  The renderer creates Filament's IBL prefilter objects once and reuses them. When they were
593
- destroyed after each call, models loaded after `set_environment()` could render black on
667
+ destroyed after each call, models loaded after an environment could render black on
594
668
  Intel and NVIDIA. The cause inside Filament is not isolated.
595
669
 
596
670
  glTF animation restores authored transforms and morph weights before evaluating a clip, which
@@ -616,7 +690,8 @@ the scan over a model's copies allocates nothing.
616
690
  Camera projection and UV transform tracks update grouped values, then rebuild each matrix once
617
691
  after channel evaluation. This prevents channel order from producing invalid intermediate clip
618
692
  planes or overwriting another UV channel. Imported camera handles borrow glTF-owned components.
619
- Closing an asset detaches its active cameras from every scene before destroying those components.
693
+ `Asset.close()` releases only the source data. Closing a model closes its imported cameras and
694
+ detaches them from every scene; the components stay until the asset goes.
620
695
 
621
696
  The build follows [nanobind's packaging interface](https://nanobind.readthedocs.io/en/latest/packaging.html)
622
697
  with CMake and scikit-build-core. Windows links the pinned release SDK. Linux builds the same
@@ -658,7 +733,7 @@ UBO fence reclamation while the driver waits for more commands. A wait without a
658
733
  (`FENCE_WAIT_FOR_EVER`, a `time_point::max()` deadline) uses an untimed wait. The converted
659
734
  deadline overflows, so `wait_until()` returns at once; Filament's frame-info thread then spins on
660
735
  the fence mutex and starves the driver thread that would signal the fence. The first
661
- `OffscreenTarget.read()` after a render hung this way. The corrections preserve asynchronous
736
+ `RenderTarget.read()` after a render hung this way. The corrections preserve asynchronous
662
737
  rendering and do not add GPU completion waits.
663
738
 
664
739
  Shared GLX contexts use the host's X display connection. Opening a separate connection can