webgpu 0.0.1__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.
webgpu/jupyter.py ADDED
@@ -0,0 +1,159 @@
1
+ import base64
2
+ import itertools
3
+ import os
4
+ import pickle
5
+ import time
6
+
7
+ from . import platform, utils
8
+ from .canvas import Canvas
9
+ from .lilgui import LilGUI
10
+ from .link import js_code as _link_js_code
11
+ from .render_object import *
12
+ from .scene import Scene
13
+ from .triangles import *
14
+ from .utils import init_device_sync
15
+ from .webgpu_api import *
16
+
17
+
18
+ def create_package_zip(module_name="webgpu"):
19
+ """
20
+ Creates a zip file containing all files in the specified Python package.
21
+ """
22
+ import importlib.util
23
+ import os
24
+ import tempfile
25
+ import zipfile
26
+
27
+ spec = importlib.util.find_spec(module_name)
28
+ if spec is None or spec.origin is None:
29
+ raise ValueError(f"Package {module_name} not found.")
30
+
31
+ package_dir = os.path.dirname(spec.origin)
32
+
33
+ with tempfile.TemporaryDirectory() as temp_dir:
34
+ output_filename = os.path.join(temp_dir, f"{module_name}.zip")
35
+ with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
36
+ for root, _, files in os.walk(package_dir):
37
+ for file in files:
38
+ file_path = os.path.join(root, file)
39
+ arcname = os.path.relpath(file_path, start=os.path.dirname(package_dir))
40
+ zipf.write(file_path, arcname)
41
+
42
+ return open(output_filename, "rb").read()
43
+
44
+
45
+ _id_counter = itertools.count()
46
+
47
+
48
+ def _init_html(scene, width, height):
49
+ from IPython.display import HTML, display
50
+
51
+ if isinstance(scene, RenderObject):
52
+ scene = [scene]
53
+ if isinstance(scene, list):
54
+ scene = Scene(scene)
55
+
56
+ id_ = f"__webgpu_{next(_id_counter)}_"
57
+
58
+ display(
59
+ HTML(
60
+ f"""
61
+ <div id='{id_}root'
62
+ style="display: flex; justify-content: space-between;"
63
+ >
64
+ <canvas
65
+ id='{id_}canvas'
66
+ style='background-color: #d0d0d0; flex: 3; width: {width}px; height: {height}px;'
67
+ >
68
+ </canvas>
69
+ <div id='{id_}lilgui'
70
+ style='flex: 1;'
71
+
72
+ ></div>
73
+ </div>
74
+ """
75
+ )
76
+ )
77
+
78
+ return scene, id_
79
+
80
+
81
+ def _draw_scene(scene: Scene, width, height, id_):
82
+ html_canvas = platform.js.document.getElementById(f"{id_}canvas")
83
+
84
+ while html_canvas is None:
85
+ html_canvas = platform.js.document.getElementById(f"{id_}canvas")
86
+ html_canvas.width = width
87
+ html_canvas.height = height
88
+ gui_element = platform.js.document.getElementById(f"{id_}lilgui")
89
+
90
+ canvas = Canvas(utils.get_device(), html_canvas)
91
+ scene.gui = LilGUI(gui_element, scene)
92
+ scene.init(canvas)
93
+ scene.render()
94
+
95
+
96
+ def _DrawPyodide(b64_data: str):
97
+ data = base64.b64decode(b64_data.encode("utf-8"))
98
+ id_, scene, width, height = pickle.loads(data)
99
+
100
+ _draw_scene(scene, width, height, id_)
101
+ return scene
102
+
103
+
104
+ def _DrawHTML(
105
+ scene: Scene | list[RenderObject] | RenderObject,
106
+ width=640,
107
+ height=640,
108
+ ):
109
+ """Draw a scene using display(Javascrip()) with all information in the HTML
110
+ This way, data is kept in the converted html when running nbconvert
111
+ The scene object is unpickled and drawn within a pyodide instance in the browser when the html is opened
112
+ """
113
+ from IPython.display import Javascript, display
114
+
115
+ scene, id_ = _init_html(scene, width, height)
116
+
117
+ data = pickle.dumps((id_, scene, width, height))
118
+ b64_data = base64.b64encode(data).decode("utf-8")
119
+
120
+ display(Javascript(f"window.draw_scene('{b64_data}');"))
121
+ return scene
122
+
123
+
124
+ def Draw(
125
+ scene: Scene | list[RenderObject] | RenderObject,
126
+ width=640,
127
+ height=640,
128
+ ):
129
+ scene, id_ = _init_html(scene, width, height)
130
+ _draw_scene(scene, width, height, id_)
131
+ return scene
132
+
133
+
134
+ if not platform.is_pyodide:
135
+ from IPython.display import Javascript, display
136
+
137
+ is_exporting = "WEBGPU_EXPORTING" in os.environ
138
+
139
+ if is_exporting:
140
+ Draw = _DrawHTML
141
+ webgpu_module = create_package_zip("webgpu")
142
+ webgpu_module_b64 = base64.b64encode(webgpu_module).decode("utf-8")
143
+ js_code = _link_js_code
144
+ js_code += f"\nconst _webgpu_code = '{webgpu_module_b64}';"
145
+ js_code += f"\nwindow.pyodide_ready = init_pyodide(_webgpu_code);"
146
+ display(Javascript(js_code))
147
+ else:
148
+ # Not exporting and not running in pyodide -> Start a websocket server and wait for the client to connect
149
+ while not platform.websocket_server:
150
+ time.sleep(0.1)
151
+ port = platform.websocket_server.port
152
+ host = f"ws://localhost:{port}"
153
+ js_code = _link_js_code + f"WebsocketLink('{host}');"
154
+
155
+ display(Javascript(js_code))
156
+
157
+ platform.init()
158
+
159
+ device = init_device_sync()
@@ -0,0 +1,363 @@
1
+ import base64
2
+ import pickle
3
+
4
+ from .draw import Draw as DrawPyodide
5
+ from .lilgui import LilGUI
6
+ from .render_object import RenderObject, _render_objects
7
+ from .scene import Scene
8
+ from .utils import _is_pyodide, reload_package
9
+
10
+
11
+ def create_package_zip(module_name="webgpu"):
12
+ """
13
+ Creates a zip file containing all files in the specified Python package.
14
+ """
15
+ import importlib.util
16
+ import os
17
+ import tempfile
18
+ import zipfile
19
+
20
+ spec = importlib.util.find_spec(module_name)
21
+ if spec is None or spec.origin is None:
22
+ raise ValueError(f"Package {module_name} not found.")
23
+
24
+ package_dir = os.path.dirname(spec.origin)
25
+
26
+ with tempfile.TemporaryDirectory() as temp_dir:
27
+ output_filename = os.path.join(temp_dir, f"{module_name}.zip")
28
+ with zipfile.ZipFile(output_filename, "w", zipfile.ZIP_DEFLATED) as zipf:
29
+ for root, _, files in os.walk(package_dir):
30
+ for file in files:
31
+ file_path = os.path.join(root, file)
32
+ arcname = os.path.relpath(file_path, start=os.path.dirname(package_dir))
33
+ zipf.write(file_path, arcname)
34
+
35
+ return open(output_filename, "rb").read()
36
+
37
+
38
+ _package_b64 = base64.b64encode(create_package_zip()).decode("utf-8")
39
+
40
+ _init_js_code = (
41
+ r"""
42
+ const SNAPSHOT_URL = 'https://cdn.jsdelivr.net/gh/mhochsteger/ngsolve_pyodide@webgpu1/snapshot.bin.gz';
43
+
44
+ function decodeB64(base64String) {
45
+ const binaryString = atob(base64String);
46
+ const len = binaryString.length;
47
+ const bytes = new Uint8Array(len);
48
+ for (let i = 0; i < len; i++) {
49
+ bytes[i] = binaryString.charCodeAt(i);
50
+ }
51
+ return bytes;
52
+ }
53
+
54
+ async function fetchSnapshot() {
55
+ const blob = await (await fetch(SNAPSHOT_URL)).blob();
56
+ const decompressor = new DecompressionStream('gzip');
57
+ const stream = blob.stream().pipeThrough(decompressor);
58
+ const response = new Response(stream);
59
+ return await response.arrayBuffer();
60
+ };
61
+
62
+ function initLilGUI() {
63
+ // In generated html files, requirejs is imported before lil-gui is loaded.
64
+ // Thus, we must load lil-gui using require, use import otherwise.
65
+ const lil_url = "https://cdn.jsdelivr.net/npm/lil-gui@0.20";
66
+ if(window.define === undefined){
67
+ import(lil_url);
68
+ } else {
69
+ require([lil_url], (module) => {
70
+ window.lil = module;
71
+ });
72
+ }
73
+ }
74
+
75
+ async function main() {
76
+ if(window.webgpu_ready === undefined) {
77
+ initLilGUI();
78
+ const pyodide_module = await import("https://cdn.jsdelivr.net/pyodide/v0.27.2/full/pyodide.mjs");
79
+ window.pyodide = await pyodide_module.loadPyodide( {
80
+ // _loadSnapshot: await fetchSnapshot(),
81
+ // lockFileURL: 'https://cdn.jsdelivr.net/gh/mhochsteger/ngsolve_pyodide@webgpu2/pyodide-lock.json',
82
+ // indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/",
83
+ }
84
+ );
85
+ pyodide.setDebug(true);
86
+ window.pyodide_ready = pyodide.loadPackage(['micropip', 'numpy', 'packaging']);
87
+ await window.pyodide_ready;
88
+ }
89
+ else {
90
+ await webgpu_ready;
91
+ }
92
+ const webgpu_b64 = `"""
93
+ + _package_b64
94
+ + r"""`;
95
+ const webpgu_zip = decodeB64(webgpu_b64);
96
+ await pyodide.unpackArchive(webpgu_zip, 'zip');
97
+ await pyodide.runPythonAsync('import webgpu.utils');
98
+ await pyodide.runPythonAsync('await webgpu.utils.init_device()');
99
+ }
100
+ window.webgpu_ready = main();
101
+
102
+ """
103
+ )
104
+
105
+
106
+ def _encode_data(data):
107
+ binary_chunk = pickle.dumps(data)
108
+ return base64.b64encode(binary_chunk).decode("utf-8")
109
+
110
+
111
+ def _decode_data(data):
112
+ binary_chunk = base64.b64decode(data.encode("utf-8"))
113
+ return pickle.loads(binary_chunk)
114
+
115
+
116
+ def _encode_function(func):
117
+ import inspect
118
+
119
+ return [func.__name__, inspect.getsource(func)]
120
+
121
+
122
+ def _decode_function(encoded_func):
123
+ import __main__
124
+
125
+ func_name, func_str = encoded_func
126
+ symbols = __main__.__dict__
127
+ exec(func_str, symbols, symbols)
128
+ return symbols[func_name]
129
+
130
+
131
+ def _init(canvas_id="canvas"):
132
+ import js
133
+
134
+ from webgpu.canvas import init_webgpu
135
+
136
+ return init_webgpu(js.document.getElementById(canvas_id))
137
+
138
+
139
+ def get_render_canvas(canvas_id):
140
+ return _render_canvases[canvas_id]
141
+
142
+
143
+ def _draw_client(canvas_id, scene, assets, globs):
144
+ from pathlib import Path
145
+
146
+ import js
147
+ import pyodide.ffi
148
+
149
+ from webgpu.jupyter import _decode_data, _decode_function
150
+
151
+ assets = _decode_data(assets)
152
+
153
+ for module_data in assets.get("modules", {}).values():
154
+ # extract zipfile from binary chunk
155
+ import io
156
+ import zipfile
157
+
158
+ zipf = zipfile.ZipFile(io.BytesIO(module_data))
159
+ zipf.extractall()
160
+
161
+ for file_name, file_data in assets.get("files", {}).items():
162
+ with open(file_name, "wb") as f:
163
+ f.write(file_data)
164
+
165
+ for module_name in assets.get("modules", {}):
166
+ reload_package(module_name)
167
+
168
+ canvas = _init(canvas_id)
169
+ scene = _decode_data(scene)
170
+
171
+ if "init_function" in assets:
172
+ func = _decode_function(assets["init_function"])
173
+ func(canvas, **scene)
174
+ elif "init_function_name" in assets:
175
+ func = globs[assets["init_function_name"]]
176
+ func(canvas, **scene)
177
+ else:
178
+ scene.init(canvas)
179
+ DrawPyodide(scene, canvas)
180
+
181
+
182
+ _draw_js_code_template = r"""
183
+ async function draw() {{
184
+ var canvas = document.createElement('canvas');
185
+ var canvas_id = "{canvas_id}";
186
+ canvas.id = canvas_id;
187
+ canvas.width = {width};
188
+ canvas.height = {height};
189
+ canvas.style = "background-color: #d0d0d0";
190
+ element.appendChild(canvas);
191
+ await window.webgpu_ready;
192
+ await window.pyodide.runPythonAsync('import webgpu.jupyter; webgpu.jupyter._draw_client("{canvas_id}", "{data}", "{assets}", globals())');
193
+ }}
194
+ draw();
195
+ """
196
+
197
+ if not _is_pyodide:
198
+ from IPython.core.magic import register_cell_magic
199
+ from IPython.display import HTML, Javascript, display
200
+
201
+ display(Javascript(_init_js_code))
202
+
203
+ _call_counter = 0
204
+
205
+ def _get_canvas_id():
206
+ global _call_counter
207
+ _call_counter += 1
208
+ return f"canvas_{_call_counter}"
209
+
210
+ def _run_js_code(data, assets, width, height):
211
+ display(
212
+ Javascript(
213
+ _draw_js_code_template.format(
214
+ canvas_id=_get_canvas_id(),
215
+ data=_encode_data(data),
216
+ assets=_encode_data(assets),
217
+ width=width,
218
+ height=height,
219
+ )
220
+ )
221
+ )
222
+
223
+ html_code = r"""
224
+ <div id="{canvas_id}_row" style="display: flex; justify-content: space-between;">
225
+ <canvas id="{canvas_id}" style="flex: 3; margin-right: 10px; padding: 10px; height: {height}px; width: {width}px; background-color: #d0d0d0;"></canvas>
226
+ <div id="{canvas_id}_gui" style="flex: 1; margin-left: 10px; padding: 10px;"></div>
227
+ </div>
228
+ """
229
+ js_code = r"""
230
+ async function draw() {{
231
+ await window.webgpu_ready;
232
+ var gui_element = document.getElementById('{canvas_id}' + '_gui');
233
+ console.log('gui_element =', gui_element);
234
+ if(window.lil_guis === undefined) {{
235
+ window.lil_guis = new Object();
236
+ }}
237
+ window.lil_guis['{canvas_id}'] = new lil.GUI({{container: gui_element}});
238
+ // var canvas2 = document.createElement('canvas');
239
+ // console.log("canvas2 =", canvas2);
240
+ var canvas = document.getElementById("{canvas_id}");
241
+ console.log('canvas size', canvas.clientWidth, canvas.clientHeight);
242
+ console.log(canvas);
243
+ canvas.width = Math.floor(canvas.clientWidth/32)*32;
244
+ canvas.height = Math.floor(canvas.clientHeight/32)*32;
245
+ canvas.style = "background-color: #d0d0d0; max-width: {width}px; max-height: {height}px;";
246
+ await window.pyodide.runPythonAsync('import webgpu.jupyter; webgpu.jupyter._draw_client("{canvas_id}", "{scene}", "{assets}", globals())');
247
+ }}
248
+ draw();
249
+ """
250
+
251
+ def Draw(
252
+ scene: Scene | list[RenderObject] | RenderObject,
253
+ width=608,
254
+ height=608,
255
+ modules=[],
256
+ ):
257
+ if isinstance(scene, RenderObject):
258
+ scene = [scene]
259
+ if isinstance(scene, list):
260
+ scene = Scene(scene)
261
+ canvas_id = _get_canvas_id()
262
+ scene.gui = LilGUI(canvas_id, scene._id)
263
+ assets = {"modules": {module: create_package_zip(module) for module in modules}}
264
+ display(
265
+ HTML(html_code.format(canvas_id=canvas_id, width=width, height=height)),
266
+ Javascript(
267
+ js_code.format(
268
+ canvas_id=canvas_id,
269
+ scene=_encode_data(scene),
270
+ assets=_encode_data(assets),
271
+ width=width,
272
+ height=height,
273
+ )
274
+ ),
275
+ )
276
+
277
+ return scene
278
+
279
+ def DrawCustom(
280
+ client_function,
281
+ kwargs={},
282
+ modules: list[str] = [],
283
+ files: list[str] = [],
284
+ width=608,
285
+ height=608,
286
+ ):
287
+ assets = {
288
+ "modules": {module: create_package_zip(module) for module in modules},
289
+ "files": {f: open(f, "rb").read() for f in files},
290
+ }
291
+ if isinstance(client_function, str):
292
+ assets["init_function_name"] = client_function
293
+ else:
294
+ assets["init_function"] = _encode_function(client_function)
295
+ canvas_id = _get_canvas_id()
296
+ display(
297
+ HTML(html_code.format(canvas_id=canvas_id, height=height, width=width)),
298
+ Javascript(
299
+ js_code.format(
300
+ canvas_id=canvas_id,
301
+ scene=_encode_data(kwargs),
302
+ assets=_encode_data(assets),
303
+ width=width,
304
+ height=height,
305
+ )
306
+ ),
307
+ )
308
+
309
+ def run_code_in_pyodide(code: str):
310
+ display(
311
+ Javascript(
312
+ f"window.webgpu_ready.then(() => {{ window.pyodide.runPythonAsync(`{code}`) }});"
313
+ )
314
+ )
315
+
316
+ @register_cell_magic
317
+ def pyodide(line, cell):
318
+ run_code_in_pyodide(str(cell))
319
+
320
+ @register_cell_magic
321
+ def pyodide_and_kernel(line, cell):
322
+ run_code_in_pyodide(str(cell))
323
+ ip = get_ipython()
324
+ exec(cell, ip.user_global_ns)
325
+
326
+ del pyodide
327
+
328
+ class Pyodide:
329
+ def __setattr__(self, key, value):
330
+ data = _encode_data(value)
331
+ display(
332
+ Javascript(
333
+ f"window.webgpu_ready.then(() => {{ window.pyodide.runPythonAsync(`import webgpu.jupyter; {key} = webgpu.jupyter._decode_data('{data}')`) }});"
334
+ )
335
+ )
336
+
337
+ pyodide = Pyodide()
338
+
339
+
340
+ def pyodide_install_packages(packages):
341
+ if not _is_pyodide:
342
+ display(
343
+ Javascript(
344
+ f"window.webgpu_ready = window.webgpu_ready.then(() => {{ return window.pyodide.loadPackage({packages}); }})"
345
+ )
346
+ )
347
+
348
+
349
+ def update_render_object(id, **kwargs):
350
+ if _is_pyodide:
351
+ obj = _render_objects[id]
352
+ obj.update(**kwargs)
353
+ else:
354
+ kwargs = _encode_data(kwargs)
355
+ run_code_in_pyodide(
356
+ f"import webgpu.jupyter; webgpu.jupyter.update_render_object('{id}', **webgpu.jupyter._decode_data('{kwargs}'));"
357
+ )
358
+
359
+
360
+ def redraw_canvas(canvas_id: str):
361
+ run_code_in_pyodide(
362
+ f"import webgpu.draw, js; js.requestAnimationFrame(webgpu.draw._canvas_id_to_gpu['{canvas_id}'].input_handler.render_function)"
363
+ )
webgpu/labels.py ADDED
@@ -0,0 +1,132 @@
1
+ import numpy as np
2
+
3
+ from .font import Font
4
+ from .render_object import RenderObject
5
+ from .uniforms import Binding
6
+ from .utils import BufferBinding, read_shader_file
7
+ from .webgpu_api import *
8
+
9
+
10
+ class Labels(RenderObject):
11
+ vertex_entry_point: str = "vertexText"
12
+ fragment_entry_point: str = "fragmentFont"
13
+ n_vertices: int = 6
14
+
15
+ """Render a list of strings on screen
16
+ @param labels: list of strings to render
17
+ @param positions: list of positions to render the labels at
18
+ @param apply_camera: whether to apply the camera transformation to the labels
19
+ @param h_align: horizontal alignment of the labels. Can be one of: left, l, center, c, right, r
20
+ @param v_align: horizontal alignment of the labels. Can be one of: bottom, b, center, c, top, t
21
+ @param font_size: font size
22
+
23
+ If any of apply_camera, h_align, or v_align is a list, it must have the same length as labels.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ labels: list[str],
29
+ positions: list[tuple],
30
+ apply_camera: bool | list[bool] = False,
31
+ h_align: str | list[str] = "left",
32
+ v_align: str | list[str] = "bottom",
33
+ font_size=20,
34
+ ):
35
+ self.labels = labels
36
+ self.positions = positions
37
+ self.font_size = font_size
38
+ self.apply_camera = apply_camera
39
+ self.h_align = h_align
40
+ self.v_align = v_align
41
+
42
+ def update(self, timestamp):
43
+ if timestamp == self._timestamp:
44
+ return
45
+ self._timestamp = timestamp
46
+ n_chars = sum(len(label) for label in self.labels)
47
+ n_labels = len(self.labels)
48
+ self.n_vertices = 6
49
+ self.n_instances = n_chars
50
+ char_t = np.dtype(
51
+ [
52
+ ("itext", np.uint32),
53
+ ("ichar", np.uint16),
54
+ ("char", np.uint8),
55
+ ("padding", np.uint8),
56
+ ]
57
+ )
58
+ char_data = np.zeros(n_chars, dtype=char_t)
59
+ text_t = np.dtype(
60
+ [
61
+ ("pos", np.float32, 3),
62
+ ("length", np.uint16),
63
+ ("apply_camera", np.uint8),
64
+ ("alignment", np.uint8),
65
+ ]
66
+ )
67
+ text_data = np.zeros(n_labels, dtype=text_t)
68
+
69
+ align_map = {
70
+ "c": 1,
71
+ "center": 1,
72
+ "r": 2,
73
+ "right": 2,
74
+ "t": 2,
75
+ "top": 2,
76
+ "b": 0,
77
+ "bottom": 0,
78
+ "l": 0,
79
+ "left": 0,
80
+ }
81
+
82
+ ichar = 0
83
+ for i, label, pos in zip(range(len(self.labels)), self.labels, self.positions):
84
+ h_align = self.h_align if isinstance(self.h_align, str) else self.h_align[i]
85
+ v_align = self.v_align if isinstance(self.v_align, str) else self.v_align[i]
86
+ align = align_map[h_align] + 4 * align_map[v_align]
87
+ apply_camera = (
88
+ self.apply_camera if isinstance(self.apply_camera, bool) else self.apply_camera[i]
89
+ )
90
+
91
+ if len(pos) == 2:
92
+ pos = (*pos, 0)
93
+
94
+ text_data[i]["pos"] = pos
95
+ text_data[i]["length"] = len(label)
96
+ text_data[i]["apply_camera"] = apply_camera
97
+ text_data[i]["alignment"] = align
98
+
99
+ i0 = ichar
100
+ for c in label:
101
+ char_data[ichar]["itext"] = i
102
+ char_data[ichar]["ichar"] = ichar - i0
103
+ char_data[ichar]["char"] = ord(c)
104
+ ichar += 1
105
+
106
+ self.font = Font(self.canvas, self.font_size)
107
+
108
+ data = (
109
+ np.array([len(self.labels)], dtype=np.uint32).tobytes()
110
+ + text_data.tobytes()
111
+ + char_data.tobytes()
112
+ )
113
+
114
+ self.buffer = self.device.createBuffer(
115
+ len(data),
116
+ usage=BufferUsage.STORAGE | BufferUsage.COPY_DST,
117
+ )
118
+ self.device.queue.writeBuffer(self.buffer, 0, data)
119
+ self.create_render_pipeline()
120
+
121
+ def get_shader_code(self):
122
+ shader_code = read_shader_file("text.wgsl", __file__)
123
+ shader_code += self.font.get_shader_code()
124
+ shader_code += self.options.camera.get_shader_code()
125
+ return shader_code
126
+
127
+ def get_bindings(self):
128
+ return [
129
+ *self.font.get_bindings(),
130
+ *self.options.camera.get_bindings(),
131
+ BufferBinding(Binding.TEXT, self.buffer),
132
+ ]
webgpu/light.py ADDED
@@ -0,0 +1,12 @@
1
+ from .utils import read_shader_file
2
+
3
+
4
+ class Light:
5
+ def __init__(self, device):
6
+ self.device = device
7
+
8
+ def get_bindings(self):
9
+ return []
10
+
11
+ def get_shader_code(self):
12
+ return read_shader_file("light.wgsl", __file__)