PyParticles3 0.4.0__py3-none-any.whl
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.
- pyparticles/__init__.py +26 -0
- pyparticles/__main__.py +7 -0
- pyparticles/animation/__init__.py +21 -0
- pyparticles/animation/animated_cli.py +53 -0
- pyparticles/animation/animated_ogl.py +691 -0
- pyparticles/animation/animated_ogl_compat.py +243 -0
- pyparticles/animation/animated_scatter.py +94 -0
- pyparticles/animation/animation.py +251 -0
- pyparticles/animation/test_animation.py +310 -0
- pyparticles/demo/__init__.py +33 -0
- pyparticles/demo/bubble.py +106 -0
- pyparticles/demo/electromagnetic_demo.py +128 -0
- pyparticles/demo/electrostatic_demo.py +126 -0
- pyparticles/demo/fountain.py +478 -0
- pyparticles/demo/gas_lennard_jones.py +87 -0
- pyparticles/demo/gravity_clusters.py +73 -0
- pyparticles/demo/solar_system.py +222 -0
- pyparticles/demo/springs.py +141 -0
- pyparticles/demo/springs_constr.py +135 -0
- pyparticles/demo/test.py +34 -0
- pyparticles/forces/__init__.py +21 -0
- pyparticles/forces/const_force.py +140 -0
- pyparticles/forces/damping.py +174 -0
- pyparticles/forces/drag.py +167 -0
- pyparticles/forces/electromagnetic.py +105 -0
- pyparticles/forces/electromagnetic_field.py +95 -0
- pyparticles/forces/electrostatic.py +72 -0
- pyparticles/forces/force.py +75 -0
- pyparticles/forces/force_constrained.py +32 -0
- pyparticles/forces/fused_const_drag.py +445 -0
- pyparticles/forces/gravity.py +345 -0
- pyparticles/forces/lennard_jones.py +74 -0
- pyparticles/forces/linear_spring.py +86 -0
- pyparticles/forces/linear_spring_constrained.py +72 -0
- pyparticles/forces/multiple_force.py +134 -0
- pyparticles/forces/pseudo_bubble.py +226 -0
- pyparticles/forces/van_der_waals_force.py +60 -0
- pyparticles/forces/vector_field_force.py +52 -0
- pyparticles/geometry/__init__.py +21 -0
- pyparticles/geometry/dist.py +24 -0
- pyparticles/geometry/intersection.py +62 -0
- pyparticles/geometry/transformations.py +387 -0
- pyparticles/main/__init__.py +21 -0
- pyparticles/main/main.py +466 -0
- pyparticles/measures/__init__.py +21 -0
- pyparticles/measures/elastic_potential_energy.py +67 -0
- pyparticles/measures/gravitational_potential_energy.py +69 -0
- pyparticles/measures/kinetic_energy.py +68 -0
- pyparticles/measures/mass.py +59 -0
- pyparticles/measures/measure.py +156 -0
- pyparticles/measures/momentum.py +144 -0
- pyparticles/measures/total_energy.py +68 -0
- pyparticles/ode/__init__.py +21 -0
- pyparticles/ode/euler_solver.py +214 -0
- pyparticles/ode/euler_solver_constrained.py +49 -0
- pyparticles/ode/leapfrog_solver.py +37 -0
- pyparticles/ode/leapfrog_solver_constrained.py +53 -0
- pyparticles/ode/midpoint_solver.py +43 -0
- pyparticles/ode/midpoint_solver_constrained.py +60 -0
- pyparticles/ode/ode_solver.py +134 -0
- pyparticles/ode/ode_solver_constrained.py +43 -0
- pyparticles/ode/runge_kutta_solver.py +79 -0
- pyparticles/ode/runge_kutta_solver_constrained.py +98 -0
- pyparticles/ode/sim_time.py +56 -0
- pyparticles/ode/stormer_verlet_solver.py +50 -0
- pyparticles/ode/stormer_verlet_solver_constrained.py +73 -0
- pyparticles/ogl/__init__.py +21 -0
- pyparticles/ogl/axis_ogl.py +210 -0
- pyparticles/ogl/draw_particles_ogl.py +313 -0
- pyparticles/ogl/draw_particles_ogl_compat.py +509 -0
- pyparticles/ogl/draw_vector_field.py +221 -0
- pyparticles/ogl/opencl_gl_vbo.py +485 -0
- pyparticles/ogl/trackball.py +131 -0
- pyparticles/ogl/translate_scene.py +87 -0
- pyparticles/pset/__init__.py +21 -0
- pyparticles/pset/boundary.py +69 -0
- pyparticles/pset/cluster.py +28 -0
- pyparticles/pset/constrained_force_interactions.py +63 -0
- pyparticles/pset/constrained_x.py +158 -0
- pyparticles/pset/constraint.py +42 -0
- pyparticles/pset/default_boundary.py +43 -0
- pyparticles/pset/file_cluster.py +131 -0
- pyparticles/pset/logger.py +152 -0
- pyparticles/pset/octree.py +451 -0
- pyparticles/pset/opencl_context.py +374 -0
- pyparticles/pset/particles_set.py +499 -0
- pyparticles/pset/periodic_boundary.py +36 -0
- pyparticles/pset/rand_cluster.py +188 -0
- pyparticles/pset/rebound_boundary.py +68 -0
- pyparticles/utils/__init__.py +21 -0
- pyparticles/utils/parse_args.py +72 -0
- pyparticles/utils/problem_config.py +608 -0
- pyparticles/utils/pypart_global.py +122 -0
- pyparticles/utils/time_formatter.py +50 -0
- pyparticles3-0.4.0.dist-info/METADATA +395 -0
- pyparticles3-0.4.0.dist-info/RECORD +100 -0
- pyparticles3-0.4.0.dist-info/WHEEL +5 -0
- pyparticles3-0.4.0.dist-info/entry_points.txt +3 -0
- pyparticles3-0.4.0.dist-info/licenses/LICENSE-gpl-3.0.txt +674 -0
- pyparticles3-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
|
|
2
|
+
# PyParticles : Particles simulation in python
|
|
3
|
+
# Copyright (C) 2012 Simone Riva
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import random
|
|
21
|
+
|
|
22
|
+
import pyparticles.geometry.transformations as tr
|
|
23
|
+
|
|
24
|
+
from OpenGL.GL import *
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DrawVectorField( object ):
|
|
29
|
+
"""
|
|
30
|
+
Draw the given vector fields.
|
|
31
|
+
|
|
32
|
+
Constructor:
|
|
33
|
+
|
|
34
|
+
:param limits: size of the draw volume: (x_min,x_max,y_min,y_max,z_min,z_max)
|
|
35
|
+
:param density: distance between the plotted vectors
|
|
36
|
+
|
|
37
|
+
"""
|
|
38
|
+
def __init__( self , limits , density ):
|
|
39
|
+
|
|
40
|
+
self.__fields = dict()
|
|
41
|
+
self.__col_fun = dict()
|
|
42
|
+
|
|
43
|
+
if len(limits) not in ( 4 , 6 ):
|
|
44
|
+
raise ValueError("limits are allowed only for 2D or 3D")
|
|
45
|
+
|
|
46
|
+
self.__limits = limits
|
|
47
|
+
self.__density = density
|
|
48
|
+
|
|
49
|
+
self._build_coords()
|
|
50
|
+
|
|
51
|
+
def __del__(self):
|
|
52
|
+
for key in self.__fields.keys() :
|
|
53
|
+
if self.__fields[key]["display_list"] != None :
|
|
54
|
+
glDeleteLists( self.__fields[key]["display_list"] , 1 )
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _build_coords(self):
|
|
58
|
+
"""
|
|
59
|
+
private: build the coordinates of the vectors
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
li = self.__limits
|
|
63
|
+
de = self.__density
|
|
64
|
+
|
|
65
|
+
sz_x = int( ( li[1] - li[0] ) / de )
|
|
66
|
+
sz_y = int( ( li[3] - li[2] ) / de )
|
|
67
|
+
|
|
68
|
+
if len(li) == 6 :
|
|
69
|
+
sz_z = int( ( li[5] - li[4] ) / de )
|
|
70
|
+
else:
|
|
71
|
+
sz_z = 1.0
|
|
72
|
+
li[4] = 0.0
|
|
73
|
+
li[5] = 0.0
|
|
74
|
+
|
|
75
|
+
self.__X = np.zeros(( sz_x * sz_y * sz_z , 3 ))
|
|
76
|
+
self.__V = np.zeros(( sz_x * sz_y * sz_z , 3 ))
|
|
77
|
+
self.__Vs = np.zeros(( sz_x * sz_y * sz_z , 3 ))
|
|
78
|
+
|
|
79
|
+
x = np.float64( li[0] )
|
|
80
|
+
y = np.float64( li[2] )
|
|
81
|
+
z = np.float64( li[4] )
|
|
82
|
+
|
|
83
|
+
indx = 0
|
|
84
|
+
for i in range(sz_z):
|
|
85
|
+
x = li[0]
|
|
86
|
+
for j in range(sz_x):
|
|
87
|
+
y = li[2]
|
|
88
|
+
for l in range(sz_y):
|
|
89
|
+
self.__X[indx,:] = np.array([x,y,z])
|
|
90
|
+
indx+=1
|
|
91
|
+
y = y + de
|
|
92
|
+
x = x + de
|
|
93
|
+
z = z + de
|
|
94
|
+
|
|
95
|
+
def _default_color( self , RGBA , X ):
|
|
96
|
+
RGBA[:] = np.array([ 0.7 , 0.7 , 0.0 , 0.5 ])
|
|
97
|
+
|
|
98
|
+
def add_vector_fun( self , fun , unit_len=1.0 , color_fun=None , key=None , time_dep=False ):
|
|
99
|
+
r"""
|
|
100
|
+
Insert a new vector function,
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
fun : Vector filed function
|
|
105
|
+
color_fun : Colors function
|
|
106
|
+
key : [optional] a key used for distinguish the vector field
|
|
107
|
+
time_dep : [True or **False** ] if True Is a time dependent filed
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
where functions are defined:
|
|
111
|
+
fun( V , X )
|
|
112
|
+
color_fun( RGBA , X )
|
|
113
|
+
|
|
114
|
+
| X : (n by DIM) coordinates array
|
|
115
|
+
| V : (n by DIM) resulting vector field
|
|
116
|
+
| RGBA : (n by 4) colors array
|
|
117
|
+
"""
|
|
118
|
+
if key == None :
|
|
119
|
+
key = str( random.randint( 0 , 2**64 ) )
|
|
120
|
+
|
|
121
|
+
if color_fun == None :
|
|
122
|
+
color_fun = self._default_color
|
|
123
|
+
|
|
124
|
+
self.__fields[key] = { "fun": fun ,
|
|
125
|
+
"color_fun": color_fun ,
|
|
126
|
+
"time_dep" : time_dep ,
|
|
127
|
+
"unit" : unit_len ,
|
|
128
|
+
"display_list" : None }
|
|
129
|
+
|
|
130
|
+
return key
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def ogl_init(self):
|
|
134
|
+
for key in self.__fields.keys() :
|
|
135
|
+
if not self.__fields[key]["time_dep"] :
|
|
136
|
+
dl = glGenLists(1)
|
|
137
|
+
|
|
138
|
+
glNewList( dl , GL_COMPILE );
|
|
139
|
+
self._draw_field(key)
|
|
140
|
+
glEndList()
|
|
141
|
+
|
|
142
|
+
self.__fields[key]["display_list"] = dl
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _draw_field( self , key ):
|
|
146
|
+
|
|
147
|
+
sz = self.__X.shape[0]
|
|
148
|
+
|
|
149
|
+
transf = tr.Transformations()
|
|
150
|
+
transf.set_points_tuple_size(6)
|
|
151
|
+
|
|
152
|
+
self.__fields[key]["fun"]( self.__V , self.__X )
|
|
153
|
+
|
|
154
|
+
# Vector in spherical coordinates
|
|
155
|
+
self.__Vs[:,0] = np.sqrt( np.sum( self.__V**2 , 1 ) )
|
|
156
|
+
self.__Vs[:,1] = np.arccos( self.__V[:,2] / self.__Vs[:,0] )
|
|
157
|
+
self.__Vs[:,2] = np.arctan( np.divide( self.__V[:,1] , self.__V[:,0] ) )
|
|
158
|
+
|
|
159
|
+
err_nan = np.isnan( self.__Vs[:,2] )
|
|
160
|
+
self.__Vs[err_nan,2] = np.sign( self.__V[err_nan,2] ) * np.pi / 2.0
|
|
161
|
+
|
|
162
|
+
unit = self.__fields[key]["unit"]
|
|
163
|
+
|
|
164
|
+
for i in range(sz):
|
|
165
|
+
|
|
166
|
+
x = self.__X[i,0] / self.__density
|
|
167
|
+
y = self.__X[i,1] / self.__density
|
|
168
|
+
z = self.__X[i,2] / self.__density
|
|
169
|
+
|
|
170
|
+
transf.push_matrix()
|
|
171
|
+
transf.translation( x , y , z )
|
|
172
|
+
|
|
173
|
+
transf.rotZ( self.__Vs[i,2] )
|
|
174
|
+
transf.rotY( -( np.pi/2.0 - self.__Vs[i,1] ) )
|
|
175
|
+
|
|
176
|
+
le = self.__Vs[i,0] / unit
|
|
177
|
+
|
|
178
|
+
transf.append_point( [ 0 , 0 , 0 ] )
|
|
179
|
+
transf.append_point( [ le , 0 , 0 ] )
|
|
180
|
+
|
|
181
|
+
transf.append_point( [ le , 0 , 0 ] )
|
|
182
|
+
transf.append_point( [ 0.8*le , 0.1*le , 0 ] )
|
|
183
|
+
|
|
184
|
+
transf.append_point( [ le , 0 , 0 ] )
|
|
185
|
+
transf.append_point( [ 0.8*le , -0.1*le , 0 ] )
|
|
186
|
+
|
|
187
|
+
transf.pop_matrix()
|
|
188
|
+
|
|
189
|
+
color = np.zeros((4))
|
|
190
|
+
|
|
191
|
+
glBegin(GL_LINES)
|
|
192
|
+
|
|
193
|
+
for pts in transf :
|
|
194
|
+
self.__fields[key]["color_fun"]( color , pts[0] )
|
|
195
|
+
|
|
196
|
+
glColor4f( color[0] , color[1] , color[2] , color[3] )
|
|
197
|
+
|
|
198
|
+
glVertex3fv( pts[0] )
|
|
199
|
+
glVertex3fv( pts[1] )
|
|
200
|
+
|
|
201
|
+
glVertex3fv( pts[2] )
|
|
202
|
+
glVertex3fv( pts[3] )
|
|
203
|
+
|
|
204
|
+
glVertex3fv( pts[4] )
|
|
205
|
+
glVertex3fv( pts[5] )
|
|
206
|
+
|
|
207
|
+
glEnd()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def draw(self):
|
|
211
|
+
for key in self.__fields.keys() :
|
|
212
|
+
|
|
213
|
+
if self.__fields[key]["display_list"] != None :
|
|
214
|
+
glCallList( self.__fields[key]["display_list"] )
|
|
215
|
+
else :
|
|
216
|
+
self._draw_field(key)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# PyParticles : Particles simulation in python
|
|
2
|
+
# Copyright (C) 2012 Simone Riva
|
|
3
|
+
#
|
|
4
|
+
# OpenCL/OpenGL zero-host-copy rendering helpers.
|
|
5
|
+
|
|
6
|
+
"""Share particle positions with OpenGL through double-buffered VBOs.
|
|
7
|
+
|
|
8
|
+
The stable path keeps canonical X in an ordinary OpenCL buffer and copies it to
|
|
9
|
+
an alternating shared VBO after each simulation step. An experimental fused
|
|
10
|
+
path acquires the next VBO before the step so the fountain integration kernel
|
|
11
|
+
can mirror its final X values directly into that VBO. Both paths retain
|
|
12
|
+
per-buffer OpenGL fences and never require host position transfers.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import pyopencl as cl
|
|
21
|
+
except ImportError:
|
|
22
|
+
cl = None
|
|
23
|
+
|
|
24
|
+
from OpenGL.GL import (
|
|
25
|
+
GL_ALREADY_SIGNALED,
|
|
26
|
+
GL_ARRAY_BUFFER,
|
|
27
|
+
GL_CONDITION_SATISFIED,
|
|
28
|
+
GL_DYNAMIC_DRAW,
|
|
29
|
+
GL_SYNC_FLUSH_COMMANDS_BIT,
|
|
30
|
+
GL_SYNC_GPU_COMMANDS_COMPLETE,
|
|
31
|
+
glBindBuffer,
|
|
32
|
+
glBufferData,
|
|
33
|
+
glClientWaitSync,
|
|
34
|
+
glDeleteBuffers,
|
|
35
|
+
glDeleteSync,
|
|
36
|
+
glFenceSync,
|
|
37
|
+
glFinish,
|
|
38
|
+
glGenBuffers,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _event_profile_seconds(event):
|
|
43
|
+
"""Return valid timing phases for a completed OpenCL event.
|
|
44
|
+
|
|
45
|
+
NVIDIA's GL acquire/release events may expose ``start``/``end`` while
|
|
46
|
+
leaving ``queued`` or ``submit`` equal to zero. Treat unavailable phases
|
|
47
|
+
as NaN instead of subtracting zero from an absolute device timestamp.
|
|
48
|
+
"""
|
|
49
|
+
nan = float("nan")
|
|
50
|
+
result = {
|
|
51
|
+
"queued_to_submit_s": nan,
|
|
52
|
+
"submit_to_start_s": nan,
|
|
53
|
+
"queued_to_start_s": nan,
|
|
54
|
+
"execution_s": nan,
|
|
55
|
+
"queued_to_end_s": nan,
|
|
56
|
+
}
|
|
57
|
+
if event is None:
|
|
58
|
+
return result
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
queued = int(event.profile.queued)
|
|
62
|
+
submit = int(event.profile.submit)
|
|
63
|
+
start = int(event.profile.start)
|
|
64
|
+
end = int(event.profile.end)
|
|
65
|
+
except Exception:
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
scale = 1.0e-9
|
|
69
|
+
if queued > 0 and submit >= queued:
|
|
70
|
+
result["queued_to_submit_s"] = (submit - queued) * scale
|
|
71
|
+
if submit > 0 and start >= submit:
|
|
72
|
+
result["submit_to_start_s"] = (start - submit) * scale
|
|
73
|
+
if queued > 0 and start >= queued:
|
|
74
|
+
result["queued_to_start_s"] = (start - queued) * scale
|
|
75
|
+
if start > 0 and end >= start:
|
|
76
|
+
result["execution_s"] = (end - start) * scale
|
|
77
|
+
if queued > 0 and end >= queued:
|
|
78
|
+
result["queued_to_end_s"] = (end - queued) * scale
|
|
79
|
+
return result
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class OpenCLGLPositionBuffer(object):
|
|
83
|
+
"""Mirror an OpenCL X buffer into alternating GL VBOs without host copies."""
|
|
84
|
+
|
|
85
|
+
_BUFFER_COUNT = 2
|
|
86
|
+
_FENCE_TIMEOUT_NS = 100000000 # 100 ms; fallback to glFinish after this.
|
|
87
|
+
|
|
88
|
+
def __init__(self, ocl_context, pset, draw_particles=None):
|
|
89
|
+
if cl is None:
|
|
90
|
+
raise RuntimeError("PyOpenCL is required for CL/GL interoperability")
|
|
91
|
+
if not getattr(ocl_context, "gl_sharing", False):
|
|
92
|
+
raise RuntimeError("OpenCL context was not created with GL sharing")
|
|
93
|
+
|
|
94
|
+
self.__occ = ocl_context
|
|
95
|
+
self.__pset = pset
|
|
96
|
+
self.__draw_particles = draw_particles
|
|
97
|
+
self.__nbytes = int(pset.size * pset.dim * np.dtype(np.float32).itemsize)
|
|
98
|
+
self.__copy_calls = 0
|
|
99
|
+
self.__copy_bytes = 0
|
|
100
|
+
self.__mirror_calls = 0
|
|
101
|
+
self.__mirror_bytes = 0
|
|
102
|
+
self.__closed = False
|
|
103
|
+
self.__next_index = 0
|
|
104
|
+
self.__active_index = 0
|
|
105
|
+
self.__vbos = []
|
|
106
|
+
self.__gl_buffers = []
|
|
107
|
+
self.__fences = [None] * self._BUFFER_COUNT
|
|
108
|
+
self.__needs_gl_completion = [False] * self._BUFFER_COUNT
|
|
109
|
+
self.__vbo_to_index = {}
|
|
110
|
+
self.__fused_pending = None
|
|
111
|
+
self.__last_profile = self._empty_profile()
|
|
112
|
+
|
|
113
|
+
vertices = np.ascontiguousarray(pset.X, dtype=np.float32)
|
|
114
|
+
try:
|
|
115
|
+
for index in range(self._BUFFER_COUNT):
|
|
116
|
+
vbo = int(np.asarray(glGenBuffers(1)).reshape(-1)[0])
|
|
117
|
+
self.__vbos.append(vbo)
|
|
118
|
+
self.__vbo_to_index[vbo] = index
|
|
119
|
+
glBindBuffer(GL_ARRAY_BUFFER, vbo)
|
|
120
|
+
glBufferData(
|
|
121
|
+
GL_ARRAY_BUFFER,
|
|
122
|
+
self.__nbytes,
|
|
123
|
+
vertices,
|
|
124
|
+
GL_DYNAMIC_DRAW,
|
|
125
|
+
)
|
|
126
|
+
self.__gl_buffers.append(
|
|
127
|
+
self.__occ.create_gl_buffer(
|
|
128
|
+
vbo, flags=cl.mem_flags.READ_WRITE
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
finally:
|
|
132
|
+
glBindBuffer(GL_ARRAY_BUFFER, 0)
|
|
133
|
+
|
|
134
|
+
if len(self.__vbos) != self._BUFFER_COUNT:
|
|
135
|
+
self.close()
|
|
136
|
+
raise RuntimeError("Could not allocate both CL/GL position VBOs")
|
|
137
|
+
|
|
138
|
+
if draw_particles is not None:
|
|
139
|
+
draw_particles.set_shared_position_vbo(self.__vbos[0])
|
|
140
|
+
draw_particles.set_shared_position_draw_complete_callback(
|
|
141
|
+
self.mark_draw_complete
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _empty_profile():
|
|
146
|
+
return {
|
|
147
|
+
"gl_finish_wall_s": 0.0,
|
|
148
|
+
"gl_fence_wait_wall_s": 0.0,
|
|
149
|
+
"gl_finish_fallback_wall_s": 0.0,
|
|
150
|
+
"fence_immediate": 1.0,
|
|
151
|
+
"acquire_gpu_s": 0.0,
|
|
152
|
+
"acquire_queue_s": float("nan"),
|
|
153
|
+
"acquire_total_s": float("nan"),
|
|
154
|
+
"copy_gpu_s": 0.0,
|
|
155
|
+
"copy_queue_s": float("nan"),
|
|
156
|
+
"copy_total_s": float("nan"),
|
|
157
|
+
"release_gpu_s": 0.0,
|
|
158
|
+
"release_queue_s": float("nan"),
|
|
159
|
+
"release_total_s": float("nan"),
|
|
160
|
+
"release_wait_wall_s": 0.0,
|
|
161
|
+
"bridge_wall_s": 0.0,
|
|
162
|
+
"vbo_index": 0.0,
|
|
163
|
+
"fused_mirror": 0.0,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def mark_draw_complete(self, vbo):
|
|
167
|
+
"""Insert a fence immediately after OpenGL submits a draw using *vbo*."""
|
|
168
|
+
if self.__closed:
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
index = self.__vbo_to_index.get(int(vbo))
|
|
172
|
+
if index is None:
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
old_fence = self.__fences[index]
|
|
176
|
+
if old_fence is not None:
|
|
177
|
+
try:
|
|
178
|
+
glDeleteSync(old_fence)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
self.__fences[index] = None
|
|
182
|
+
|
|
183
|
+
self.__needs_gl_completion[index] = True
|
|
184
|
+
try:
|
|
185
|
+
fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0)
|
|
186
|
+
if fence:
|
|
187
|
+
self.__fences[index] = fence
|
|
188
|
+
except Exception:
|
|
189
|
+
self.__fences[index] = None
|
|
190
|
+
|
|
191
|
+
def _wait_for_gl_vbo(self, index):
|
|
192
|
+
"""Wait only for the GL draw that last consumed one VBO."""
|
|
193
|
+
if not self.__needs_gl_completion[index]:
|
|
194
|
+
return 0.0, 1.0, 0.0
|
|
195
|
+
|
|
196
|
+
start = time.perf_counter()
|
|
197
|
+
fallback_seconds = 0.0
|
|
198
|
+
immediate = 0.0
|
|
199
|
+
fence = self.__fences[index]
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
if fence is None:
|
|
203
|
+
fallback_start = time.perf_counter()
|
|
204
|
+
glFinish()
|
|
205
|
+
fallback_seconds = time.perf_counter() - fallback_start
|
|
206
|
+
else:
|
|
207
|
+
result = glClientWaitSync(fence, 0, 0)
|
|
208
|
+
if result in (GL_ALREADY_SIGNALED, GL_CONDITION_SATISFIED):
|
|
209
|
+
immediate = 1.0
|
|
210
|
+
else:
|
|
211
|
+
result = glClientWaitSync(
|
|
212
|
+
fence,
|
|
213
|
+
GL_SYNC_FLUSH_COMMANDS_BIT,
|
|
214
|
+
self._FENCE_TIMEOUT_NS,
|
|
215
|
+
)
|
|
216
|
+
if result not in (GL_ALREADY_SIGNALED, GL_CONDITION_SATISFIED):
|
|
217
|
+
fallback_start = time.perf_counter()
|
|
218
|
+
glFinish()
|
|
219
|
+
fallback_seconds = time.perf_counter() - fallback_start
|
|
220
|
+
except Exception:
|
|
221
|
+
fallback_start = time.perf_counter()
|
|
222
|
+
glFinish()
|
|
223
|
+
fallback_seconds = time.perf_counter() - fallback_start
|
|
224
|
+
finally:
|
|
225
|
+
if fence is not None:
|
|
226
|
+
try:
|
|
227
|
+
glDeleteSync(fence)
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
self.__fences[index] = None
|
|
231
|
+
self.__needs_gl_completion[index] = False
|
|
232
|
+
|
|
233
|
+
return time.perf_counter() - start, immediate, fallback_seconds
|
|
234
|
+
|
|
235
|
+
def _select_next_vbo(self):
|
|
236
|
+
index = self.__next_index
|
|
237
|
+
self.__next_index = (self.__next_index + 1) % self._BUFFER_COUNT
|
|
238
|
+
fence_wait, fence_immediate, finish_fallback = self._wait_for_gl_vbo(index)
|
|
239
|
+
return index, fence_wait, fence_immediate, finish_fallback
|
|
240
|
+
|
|
241
|
+
def update_from_device(self):
|
|
242
|
+
"""Stable path: copy current device X into the next VBO and draw it."""
|
|
243
|
+
if self.__closed:
|
|
244
|
+
raise RuntimeError("The shared OpenGL position buffer is closed")
|
|
245
|
+
if self.__fused_pending is not None:
|
|
246
|
+
raise RuntimeError("A fused CL/GL render target is already acquired")
|
|
247
|
+
|
|
248
|
+
bridge_start = time.perf_counter()
|
|
249
|
+
index, fence_wait, fence_immediate, finish_fallback = self._select_next_vbo()
|
|
250
|
+
|
|
251
|
+
gl_buffer = self.__gl_buffers[index]
|
|
252
|
+
acquire = self.__occ.acquire_gl_objects([gl_buffer])
|
|
253
|
+
copy = cl.enqueue_copy(
|
|
254
|
+
self.__occ.CL_queue,
|
|
255
|
+
gl_buffer,
|
|
256
|
+
self.__occ.X_cla.data,
|
|
257
|
+
byte_count=self.__nbytes,
|
|
258
|
+
wait_for=[acquire],
|
|
259
|
+
)
|
|
260
|
+
release = self.__occ.release_gl_objects(
|
|
261
|
+
[gl_buffer], wait_for=[copy]
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Without cl_khr_gl_event, OpenGL must not consume the VBO until the CL
|
|
265
|
+
# release is complete. Keep this wait so rendering remains correct.
|
|
266
|
+
wait_start = time.perf_counter()
|
|
267
|
+
release.wait()
|
|
268
|
+
wait_end = time.perf_counter()
|
|
269
|
+
|
|
270
|
+
acquire_profile = _event_profile_seconds(acquire)
|
|
271
|
+
copy_profile = _event_profile_seconds(copy)
|
|
272
|
+
release_profile = _event_profile_seconds(release)
|
|
273
|
+
|
|
274
|
+
self.__active_index = index
|
|
275
|
+
if self.__draw_particles is not None:
|
|
276
|
+
self.__draw_particles.set_shared_position_vbo(self.__vbos[index])
|
|
277
|
+
|
|
278
|
+
bridge_end = time.perf_counter()
|
|
279
|
+
self.__last_profile = {
|
|
280
|
+
"gl_finish_wall_s": 0.0,
|
|
281
|
+
"gl_fence_wait_wall_s": fence_wait,
|
|
282
|
+
"gl_finish_fallback_wall_s": finish_fallback,
|
|
283
|
+
"fence_immediate": fence_immediate,
|
|
284
|
+
"acquire_gpu_s": acquire_profile["execution_s"],
|
|
285
|
+
"acquire_queue_s": acquire_profile["queued_to_start_s"],
|
|
286
|
+
"acquire_total_s": acquire_profile["queued_to_end_s"],
|
|
287
|
+
"copy_gpu_s": copy_profile["execution_s"],
|
|
288
|
+
"copy_queue_s": copy_profile["queued_to_start_s"],
|
|
289
|
+
"copy_total_s": copy_profile["queued_to_end_s"],
|
|
290
|
+
"release_gpu_s": release_profile["execution_s"],
|
|
291
|
+
"release_queue_s": release_profile["queued_to_start_s"],
|
|
292
|
+
"release_total_s": release_profile["queued_to_end_s"],
|
|
293
|
+
"release_wait_wall_s": wait_end - wait_start,
|
|
294
|
+
"bridge_wall_s": bridge_end - bridge_start,
|
|
295
|
+
"vbo_index": float(index),
|
|
296
|
+
"fused_mirror": 0.0,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
self.__copy_calls += 1
|
|
300
|
+
self.__copy_bytes += self.__nbytes
|
|
301
|
+
return release
|
|
302
|
+
|
|
303
|
+
def prepare_fused_render(self):
|
|
304
|
+
"""Acquire the next VBO so a simulation kernel may write it directly.
|
|
305
|
+
|
|
306
|
+
Returns ``(cl_buffer, acquire_event)``. The caller must arrange for its
|
|
307
|
+
kernel to wait for the acquire event and then call
|
|
308
|
+
:meth:`finish_fused_render` with the kernel event.
|
|
309
|
+
"""
|
|
310
|
+
if self.__closed:
|
|
311
|
+
raise RuntimeError("The shared OpenGL position buffer is closed")
|
|
312
|
+
if self.__fused_pending is not None:
|
|
313
|
+
raise RuntimeError("A fused CL/GL render target is already acquired")
|
|
314
|
+
|
|
315
|
+
prepare_start = time.perf_counter()
|
|
316
|
+
index, fence_wait, fence_immediate, finish_fallback = self._select_next_vbo()
|
|
317
|
+
gl_buffer = self.__gl_buffers[index]
|
|
318
|
+
acquire = self.__occ.acquire_gl_objects([gl_buffer])
|
|
319
|
+
prepare_end = time.perf_counter()
|
|
320
|
+
|
|
321
|
+
self.__fused_pending = {
|
|
322
|
+
"index": index,
|
|
323
|
+
"gl_buffer": gl_buffer,
|
|
324
|
+
"acquire": acquire,
|
|
325
|
+
"fence_wait": fence_wait,
|
|
326
|
+
"fence_immediate": fence_immediate,
|
|
327
|
+
"finish_fallback": finish_fallback,
|
|
328
|
+
"prepare_wall_s": prepare_end - prepare_start,
|
|
329
|
+
}
|
|
330
|
+
return gl_buffer, acquire
|
|
331
|
+
|
|
332
|
+
def finish_fused_render(self, kernel_event):
|
|
333
|
+
"""Release a VBO written by the fused simulation kernel and draw it."""
|
|
334
|
+
if self.__closed:
|
|
335
|
+
raise RuntimeError("The shared OpenGL position buffer is closed")
|
|
336
|
+
pending = self.__fused_pending
|
|
337
|
+
if pending is None:
|
|
338
|
+
raise RuntimeError("No fused CL/GL render target is pending")
|
|
339
|
+
if kernel_event is None:
|
|
340
|
+
raise ValueError("kernel_event is required for fused CL/GL rendering")
|
|
341
|
+
|
|
342
|
+
finish_start = time.perf_counter()
|
|
343
|
+
release = self.__occ.release_gl_objects(
|
|
344
|
+
[pending["gl_buffer"]], wait_for=[kernel_event]
|
|
345
|
+
)
|
|
346
|
+
wait_start = time.perf_counter()
|
|
347
|
+
release.wait()
|
|
348
|
+
wait_end = time.perf_counter()
|
|
349
|
+
|
|
350
|
+
acquire_profile = _event_profile_seconds(pending["acquire"])
|
|
351
|
+
release_profile = _event_profile_seconds(release)
|
|
352
|
+
|
|
353
|
+
index = pending["index"]
|
|
354
|
+
self.__active_index = index
|
|
355
|
+
if self.__draw_particles is not None:
|
|
356
|
+
self.__draw_particles.set_shared_position_vbo(self.__vbos[index])
|
|
357
|
+
|
|
358
|
+
finish_end = time.perf_counter()
|
|
359
|
+
self.__last_profile = {
|
|
360
|
+
"gl_finish_wall_s": 0.0,
|
|
361
|
+
"gl_fence_wait_wall_s": pending["fence_wait"],
|
|
362
|
+
"gl_finish_fallback_wall_s": pending["finish_fallback"],
|
|
363
|
+
"fence_immediate": pending["fence_immediate"],
|
|
364
|
+
"acquire_gpu_s": acquire_profile["execution_s"],
|
|
365
|
+
"acquire_queue_s": acquire_profile["queued_to_start_s"],
|
|
366
|
+
"acquire_total_s": acquire_profile["queued_to_end_s"],
|
|
367
|
+
"copy_gpu_s": 0.0,
|
|
368
|
+
"copy_queue_s": 0.0,
|
|
369
|
+
"copy_total_s": 0.0,
|
|
370
|
+
"release_gpu_s": release_profile["execution_s"],
|
|
371
|
+
"release_queue_s": release_profile["queued_to_start_s"],
|
|
372
|
+
"release_total_s": release_profile["queued_to_end_s"],
|
|
373
|
+
"release_wait_wall_s": wait_end - wait_start,
|
|
374
|
+
"bridge_wall_s": pending["prepare_wall_s"] + (finish_end - finish_start),
|
|
375
|
+
"vbo_index": float(index),
|
|
376
|
+
"fused_mirror": 1.0,
|
|
377
|
+
}
|
|
378
|
+
self.__fused_pending = None
|
|
379
|
+
self.__mirror_calls += 1
|
|
380
|
+
self.__mirror_bytes += self.__nbytes
|
|
381
|
+
return release
|
|
382
|
+
|
|
383
|
+
def _release_pending_fused_vbo(self):
|
|
384
|
+
pending = self.__fused_pending
|
|
385
|
+
if pending is None or self.__occ is None:
|
|
386
|
+
self.__fused_pending = None
|
|
387
|
+
return
|
|
388
|
+
try:
|
|
389
|
+
release = self.__occ.release_gl_objects([pending["gl_buffer"]])
|
|
390
|
+
release.wait()
|
|
391
|
+
except Exception:
|
|
392
|
+
pass
|
|
393
|
+
self.__fused_pending = None
|
|
394
|
+
|
|
395
|
+
def close(self):
|
|
396
|
+
"""Release CL and GL views while the owning GL context is still current."""
|
|
397
|
+
if self.__closed:
|
|
398
|
+
return
|
|
399
|
+
self.__closed = True
|
|
400
|
+
|
|
401
|
+
occ = self.__occ
|
|
402
|
+
|
|
403
|
+
# Teardown is not a hot path. Fully drain both APIs before destroying
|
|
404
|
+
# fences and shared objects so the NVIDIA/GLX lifetime ordering remains
|
|
405
|
+
# deterministic.
|
|
406
|
+
try:
|
|
407
|
+
if occ is not None:
|
|
408
|
+
occ.CL_queue.finish()
|
|
409
|
+
except Exception:
|
|
410
|
+
pass
|
|
411
|
+
|
|
412
|
+
self._release_pending_fused_vbo()
|
|
413
|
+
|
|
414
|
+
try:
|
|
415
|
+
glFinish()
|
|
416
|
+
except Exception:
|
|
417
|
+
pass
|
|
418
|
+
|
|
419
|
+
if self.__draw_particles is not None:
|
|
420
|
+
try:
|
|
421
|
+
self.__draw_particles.set_shared_position_draw_complete_callback(None)
|
|
422
|
+
self.__draw_particles.set_gpu_timing_enabled(False)
|
|
423
|
+
self.__draw_particles.set_shared_position_vbo(None)
|
|
424
|
+
except Exception:
|
|
425
|
+
pass
|
|
426
|
+
|
|
427
|
+
for index, fence in enumerate(self.__fences):
|
|
428
|
+
if fence is not None:
|
|
429
|
+
try:
|
|
430
|
+
glDeleteSync(fence)
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
self.__fences[index] = None
|
|
434
|
+
|
|
435
|
+
for gl_buffer in self.__gl_buffers:
|
|
436
|
+
try:
|
|
437
|
+
release = getattr(gl_buffer, "release", None)
|
|
438
|
+
if release is not None:
|
|
439
|
+
release()
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
self.__gl_buffers = []
|
|
443
|
+
|
|
444
|
+
for vbo in self.__vbos:
|
|
445
|
+
try:
|
|
446
|
+
if vbo:
|
|
447
|
+
glDeleteBuffers(1, [vbo])
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
self.__vbos = []
|
|
451
|
+
self.__vbo_to_index = {}
|
|
452
|
+
|
|
453
|
+
self.__occ = None
|
|
454
|
+
self.__pset = None
|
|
455
|
+
self.__draw_particles = None
|
|
456
|
+
|
|
457
|
+
def get_vbo(self):
|
|
458
|
+
if not self.__vbos:
|
|
459
|
+
return 0
|
|
460
|
+
return self.__vbos[self.__active_index]
|
|
461
|
+
|
|
462
|
+
vbo = property(get_vbo)
|
|
463
|
+
|
|
464
|
+
def get_cl_buffer(self):
|
|
465
|
+
if not self.__gl_buffers:
|
|
466
|
+
return None
|
|
467
|
+
return self.__gl_buffers[self.__active_index]
|
|
468
|
+
|
|
469
|
+
cl_buffer = property(get_cl_buffer)
|
|
470
|
+
|
|
471
|
+
def get_copy_stats(self):
|
|
472
|
+
return {
|
|
473
|
+
"calls": self.__copy_calls,
|
|
474
|
+
"bytes": self.__copy_bytes,
|
|
475
|
+
"mirror_calls": self.__mirror_calls,
|
|
476
|
+
"mirror_bytes": self.__mirror_bytes,
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
copy_stats = property(get_copy_stats)
|
|
480
|
+
|
|
481
|
+
def get_last_profile(self):
|
|
482
|
+
"""Return timings for the most recent CL/GL bridge update."""
|
|
483
|
+
return dict(self.__last_profile)
|
|
484
|
+
|
|
485
|
+
last_profile = property(get_last_profile)
|