diff --git a/examples/example_imgui.py b/examples/example_imgui.py new file mode 100644 index 0000000..ee28ea9 --- /dev/null +++ b/examples/example_imgui.py @@ -0,0 +1,106 @@ +from wgpu_shadertoy import Shadertoy + +# shadertoy source: https://www.shadertoy.com/view/Wf3SWn by Xor CC-BY-NC-SA 3.0 +# modified in Line73 to disassemble the for loop due to: https://github.com/gfx-rs/wgpu/issues/6208 + +shader_code = """//glsl +/* + "Sunset" by @XorDev + + Expanded and clarified version of my Sunset shader: + https://www.shadertoy.com/view/wXjSRt + + Based on my tweet shader: + https://x.com/XorDev/status/1918764164153049480 +*/ + +//Output image brightness +#define BRIGHTNESS 1.0 + +//Base brightness (higher = brighter, less saturated) +#define COLOR_BASE 1.5 +//Color cycle speed (radians per second) +#define COLOR_SPEED 0.5 +//RGB color phase shift (in radians) +#define RGB vec3(0.0, 1.0, 2.0) +//Color translucency strength +#define COLOR_WAVE 14.0 +//Color direction and (magnitude = frequency) +#define COLOR_DOT vec3(1,-1,0) + +//Wave iterations (higher = slower) +#define WAVE_STEPS 8.0 +//Starting frequency +#define WAVE_FREQ 5.0 +//Wave amplitude +#define WAVE_AMP 0.6 +//Scaling exponent factor +#define WAVE_EXP 1.8 +//Movement direction +#define WAVE_VELOCITY vec3(0.2) + + +//Cloud thickness (lower = denser) +#define PASSTHROUGH 0.2 + +//Cloud softness +#define SOFTNESS 0.005 +//Raymarch step +#define STEPS 100.0 +//Sky brightness factor (finicky) +#define SKY 10.0 +//Camera fov ratio (tan(fov_y/2)) +#define FOV 1.0 + +void mainImage(out vec4 fragColor, in vec2 fragCoord) +{ + //Raymarch depth + float z = 0.0; + + //Step distance + float d = 0.0; + //Signed distance + float s = 0.0; + + //Ray direction + vec3 dir = normalize( vec3(2.0*fragCoord - iResolution.xy, - FOV * iResolution.y)); + + //Output color + vec3 col = vec3(0); + + //Clear fragcolor and raymarch with 100 iterations + for(float i = 0.0; i=0.21.1", + "wgpu[imgui]>=0.21.1", "rendercanvas", "requests", "numpy", diff --git a/tests/testutils.py b/tests/testutils.py index 7c4aea2..75170bf 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -117,7 +117,7 @@ def get_default_adapter_summary(): def find_examples(query=None, negative_query=None, return_stems=False): result = [] for example_path in examples_dir.glob("*.py"): - example_code = example_path.read_text() + example_code = example_path.read_text(encoding="utf-8") query_match = query is None or query in example_code negative_query_match = ( negative_query is None or negative_query not in example_code diff --git a/wgpu_shadertoy/cli.py b/wgpu_shadertoy/cli.py index fed471d..b9de9ae 100644 --- a/wgpu_shadertoy/cli.py +++ b/wgpu_shadertoy/cli.py @@ -17,12 +17,20 @@ default=(800, 450), ) +argument_parser.add_argument( + "--imgui", + help="automatically turn constants into imgui sliders", + action="store_true", +) + def main_cli(): args = argument_parser.parse_args() shader_id = args.shader_id resolution = args.resolution - shader = Shadertoy.from_id(shader_id, resolution=resolution) + imgui = args.imgui + # TODO maybe **args? + shader = Shadertoy.from_id(shader_id, resolution=resolution, imgui=imgui) shader.show() diff --git a/wgpu_shadertoy/imgui.py b/wgpu_shadertoy/imgui.py new file mode 100644 index 0000000..de2256e --- /dev/null +++ b/wgpu_shadertoy/imgui.py @@ -0,0 +1,227 @@ +import re +from imgui_bundle import imgui as ig + +from .utils import UniformArray +from wgpu.utils.imgui import ImguiWgpuBackend +# from wgpu_shadertoy.passes import RenderPass #circular import-.- +from dataclasses import dataclass +from math import log + + +# could imgui become just another RenderPass after Image? I got to understand backend vs renderer first. +# make become part of .passes?? +# todo: raise error if imgui isn't installed (only if this module is required?) + + +@dataclass +class ShaderConstant: + # renderpass_pass: str #maybe this is a RenderPass pointer? likely redundant + line_number: int + original_line: str + name: str + value: int | float + shader_dtype: str # float, int, vec2, vec3, bool etc. + + def c_type_format(self) -> str: + # based on these for the memoryview cast: + # https://docs.python.org/3/library/struct.html#format-characters + if self.shader_dtype == "float": + return "f" + elif self.shader_dtype == "int": + return "i" + elif self.shader_dtype == "uint": + return "I" + # add more types as needed + return "?" + +def parse_constants(code:str) -> list[ShaderConstant]: + # todo: + # WGSL variants?? + # re/tree-sitter/loops and functions? + # parse and collect constants from shadercode (including common pass?) + # get information about the line, the type and it's initial value + # make up a range (maybe just the order of magnitude + 1 as max and 0 as min (what about negative values?)) + # what is the return type? (line(int), type(str), value(float/tuple/int?)) maybe proper dataclasss for once + + # for multipass shader this might need to be per pass (rpass.value) ? + # mataches the macro: #define NAME VALUE + # TODO there can be characters in numerical literals, such as x and o for hex and octal representation or e for scientific notation + # technically the macros can also be an expression that is evaluated to be a number... such as # define DOF 10..0/30.0 - so how do we deal with that? + define_pattern = re.compile(r"#\s*define\s+(\w+)\s+(-?[\d.]+)") #for numerical literals right now. + if_def_template = r"#(el)?if\s+" #preprocessor ifdef blocks can't become uniforms. replacing these dynamically will be difficult. + + constants = [] + for li, line in enumerate(code.splitlines()): + match = define_pattern.match(line.strip()) + if match: + name, value = match.groups() + if_def_pattern = re.compile(if_def_template + name) + if if_def_pattern.findall(code): + #.findall over .match because because not only the beginning matters here + print(f"skipping constant {name}, it needs to stay a macro") + continue + + if "." in value: #value.isdecimal? + # TODO: wgsl needs to be more specific (f32 for example?) - but there is no preprocessor anyways... + dtype = "float" #default float (32bit) + value = float(value) + elif value.isdecimal(): # value.isnumeric? + dtype = "int" # "big I (32bit)" + value = int(value) + else: + # TODO complexer types? + print(f"can't parse type for constant {name} with value {value}, skipping") + continue + + constant = ShaderConstant( + # renderpass_pass="image", # TODO: shouldn't be names. + line_number=li, + original_line=line.strip(), + name=name, + value=value, + shader_dtype=dtype + ) + # todo: remove lines here? (comment out better) + constants.append(constant) + print(f"In line {li} found constant: {name} with value: {value} of dtype {dtype}") # maybe name renderpass too? + + # maybe just named tuple instead of dataclass? + return constants + +def make_uniform(constants) -> UniformArray: + arr_data = [] + for constant in constants: + arr_data.append(tuple([constant.name, constant.c_type_format(), 1])) + data = UniformArray(*arr_data) + + # init data + for constant in constants: + data[constant.name] = constant.value + + # TODO: + # is there issues with padding? (maybe solve in the class) + # figure out order due to padding/alignment: https://www.w3.org/TR/WGSL/#alignment-and-size + # return a UniformArray object too (cycling import?) also needs device handed down. + # (does this need to be a class to update the values?) + return data + +# TODO mark private? +def construct_imports(constants: list[ShaderConstant], constant_binding_idx: int) -> str: + # codegen the import block for this uniform (including binding? - which number?) + # could be part of the UniformArray class maybe? + # to be pasted near the top of the fragment shader code. + # alternatively: insert these in the ShadertoyInputs uniform? + # better yet: use push constants + # TODO: can you even import a uniform struct and then have these available as global? + # maybe I got to add them back in as #define name = constant.name or something + + if not constants: + return "" + + var_init_lines = [] + var_mapping_lines = [] + for const in constants: + var_init_lines.append(f"{const.shader_dtype} {const.name};") + var_mapping_lines.append(f"# define {const.name} const_input{constant_binding_idx}.{const.name}") + + new_line = "\n" # pytest was complaining about having blackslash in an f-string + code_construct = f""" + uniform struct ConstantInput{constant_binding_idx} {{ + {new_line.join(var_init_lines)} + }}; + layout(binding = {constant_binding_idx}) uniform ConstantInput{constant_binding_idx} const_input{constant_binding_idx}; + {new_line.join(var_mapping_lines)} + """ + # the identifier name includes the digit so common doesn't cause redefinition! + # TODO messed up indentation... textwrap.dedent? + return code_construct + +def replace_constants(code: str, constants: list[ShaderConstant], constant_binding_idx: int) -> str: + """ + comment out existing constants and redefine them with uniform struct + """ + code_lines = code.splitlines() + for const in constants: + # comment out existing constants + code_lines[const.line_number] = f"// {code_lines[const.line_number]}" + + constant_headers = construct_imports(constants, constant_binding_idx) + code_lines.insert(0, constant_headers) + + return "\n".join(code_lines) + + +# imgui stuff +def update_gui(): + # todo: look at exmaples nad largely copy nad paste, will be called in the draw_frame function I think. + + pass + + +def gui(renderpasses: list["RenderPass"]): + ig.new_frame() + ig.set_next_window_pos((0, 0), ig.Cond_.appearing) + ig.set_next_window_size((0, 0), ig.Cond_.appearing) # auto size not wide enough with text :/ + ig.begin("Shader constants", None) + ig.text('in-dev imgui overlay\n') + + if ig.is_item_hovered(): + ig.set_tooltip("TODO") + + # maybe we should have a global main or utils.get_main()? + main = renderpasses[0].main + + # TODO: avoid duplication, maybe common should be a renderpass instance (at least a little bit) - or we iterate through constants lists + if main._common_constants: + if ig.collapsing_header("Common Constants", flags=ig.TreeNodeFlags_.default_open): + for const in main._common_constants: + if const.shader_dtype == "float": + _, main._common_constants_data[const.name] = ig.drag_float(f"{const.name}", main._common_constants_data[const.name], v_speed=abs(const.value)*0.01) + elif const.shader_dtype == "int": + _, main._common_constants_data[const.name] = ig.drag_int(f"{const.name}", main._common_constants_data[const.name], v_speed=abs(const.value)*0.01) + if ig.is_item_hovered() and ig.is_mouse_clicked(ig.MouseButton_.right): + main._common_constants_data[const.name] = const.value + + for rp in renderpasses: # TODO: most likely add common here? + constants = rp._constants + constants_data = rp._constants_data + if ig.collapsing_header(f"{rp} Constants", flags=ig.TreeNodeFlags_.default_open): + if hasattr(rp, "texture_front"): # isinstance(rp, BufferRenderPass) + # make this another toggle? or a whole 2nd UI? + front_view = rp.texture_front.create_view() + front_ref = rp.main._imgui_backend.register_texture(front_view) + scale = 0.25 # TODO dynamic zoom via width? + # TODO: can we force a background? do we need to request additional view formats? -> ig.image_with_bg? + buf_img = ig.image(front_ref, (front_view.size[0]*scale, front_view.size[1]*scale), uv0=(0,1), uv1=(1,0)) + + # create the sliders? -> drag widget! + for const in constants: + if const.shader_dtype == "float": + _, constants_data[const.name] = ig.drag_float(f"{const.name}", constants_data[const.name], v_speed=abs(const.value)*0.01) + elif const.shader_dtype == "int": + _, constants_data[const.name] = ig.drag_int(f"{const.name}", constants_data[const.name], v_speed=abs(const.value)*0.01) + if ig.is_item_hovered() and ig.is_mouse_clicked(ig.MouseButton_.right): + constants_data[const.name] = const.value + + ig.text("Right sliders/drag to reset") + + # TODO: control the size of these headers to make the window as small as possible after they are collapsed! + ig.end() + ig.end_frame() + ig.render() + return ig.get_draw_data() + +def get_backend(device, canvas, render_texture_format): + """ + copied from backend example, held here to avoid clutter in the main class + """ + + # init imgui backend + ig.create_context() + imgui_backend = ImguiWgpuBackend(device, render_texture_format) + imgui_backend.io.display_size = canvas.get_logical_size() + imgui_backend.io.display_framebuffer_scale = ( + canvas.get_pixel_ratio(), + canvas.get_pixel_ratio(), + ) + return imgui_backend diff --git a/wgpu_shadertoy/passes.py b/wgpu_shadertoy/passes.py index 09be6b9..362430f 100644 --- a/wgpu_shadertoy/passes.py +++ b/wgpu_shadertoy/passes.py @@ -4,6 +4,7 @@ import wgpu from .inputs import ShadertoyChannel, ShadertoyChannelBuffer, ShadertoyChannelTexture +from .imgui import replace_constants, gui, parse_constants, make_uniform builtin_variables_glsl = """#version 450 core @@ -56,6 +57,7 @@ def __init__( self._inputs = inputs self._input_headers = "" + def get_current_texture(self) -> wgpu.GPUTexture: """ The current (next) texture to draw to @@ -162,6 +164,19 @@ def _prepare_render(self): It attaches inputs, assembles the shadercode and creates the render pipeline. """ + # need to be after we got main class? + if self.main._imgui: + # maybe a self._imgui for the case where there are no local or common constants? + self._constants = parse_constants(self.shader_code) + self._constants_data = make_uniform(self._constants) + self._constants_buffer = self._device.create_buffer( + label=f"{self} constant buffer for imgui overlay", + size=self._constants_data.nbytes, + usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST + ) + # 0 is uniform buffer, 2 per input (texture + sampler), 9 for common constants and finally 10 for pass constants + # TODO can there be gaps like this? then we don't need a constant anyway. + self._constants_binding_idx = 10 # inputs can only be attached once the main class is set, so calling it here should do it. self.channels = self._attach_inputs(self._inputs) vertex_shader_code, frag_shader_code = self.construct_code() @@ -178,6 +193,9 @@ def _prepare_render(self): size=self.main._uniform_data.nbytes, usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST, ) + + + self._setup_renderpipeline() # split in half so the next part can be reused. def _setup_renderpipeline(self): @@ -203,6 +221,45 @@ def _setup_renderpipeline(self): }, ] + if self.main._imgui and self._constants: + bind_groups_layout_entries.append( + { + "binding": self._constants_binding_idx, + "resource": { + "buffer": self._constants_buffer, + "offset": 0, + "size": self._constants_buffer.size, + }, + }, + ) + binding_layout.append( + { + "binding": self._constants_binding_idx, + "visibility": wgpu.ShaderStage.FRAGMENT, + "buffer": {"type": wgpu.BufferBindingType.uniform}, + }, + ) + # TODO: could we make a little iterable at the top? (also doesn't need to be nested?) + if self.main._imgui and self.main._common_constants: + # # maybe we can one fewer buffers but use the offsets instead? + bind_groups_layout_entries.append( + { + "binding": 9, + "resource": { + "buffer": self.main._common_constants_buffer, + "offset": 0, + "size": self.main._common_constants_buffer.size, + }, + } + ) + binding_layout.append( + { + "binding": 9, + "visibility": wgpu.ShaderStage.FRAGMENT, + "buffer": {"type": wgpu.BufferBindingType.uniform}, + } + ) + # setup bind groups for the channels channel_res = [] for channel in self.channels: @@ -272,6 +329,23 @@ def draw(self) -> wgpu.GPUCommandBuffer: size=self.main._uniform_data.nbytes, ) + if self.main._imgui and self._constants: + self._device.queue.write_buffer( + buffer = self._constants_buffer, + buffer_offset = 0, + data = self._constants_data.mem, + data_offset = 0, + size = self._constants_buffer.size, + ) + if self.main._imgui and self.main._common_constants: + self._device.queue.write_buffer( + buffer=self.main._common_constants_buffer, + buffer_offset=0, + data=self.main._common_constants_data.mem, + data_offset=0, + size=self.main._common_constants_buffer.size, + ) + command_encoder: wgpu.GPUCommandEncoder = self._device.create_command_encoder() current_texture: wgpu.GPUTexture = self.get_current_texture() @@ -294,8 +368,15 @@ def draw(self) -> wgpu.GPUCommandBuffer: # self._bind_group might get generalized out for buffer render_pass.set_bind_group(0, self._bind_group, [], 0, 99) render_pass.draw(3, 1, 0, 0) - render_pass.end() + # instead of reimplementing the same thing twice? + if isinstance(self, ImageRenderPass) and self.main._imgui: + # render imgui overlay only on the image pass. + if self.main._imgui_backend is not None: + imgui_data = gui(self.main.renderpasses) + self.main._imgui_backend.render(imgui_data, render_pass) + + render_pass.end() return command_encoder.finish() def construct_code(self) -> tuple[str, str]: @@ -348,6 +429,10 @@ def construct_code(self) -> tuple[str, str]: mainImage(FragColor, fragcoord); }} """ + + if self.main._imgui: + self._shader_code = replace_constants(self._shader_code, self._constants, self._constants_binding_idx) + frag_shader_code = ( builtin_variables_glsl + self._input_headers diff --git a/wgpu_shadertoy/shadertoy.py b/wgpu_shadertoy/shadertoy.py index 3459f83..62e7afc 100644 --- a/wgpu_shadertoy/shadertoy.py +++ b/wgpu_shadertoy/shadertoy.py @@ -1,5 +1,4 @@ import collections -import ctypes import os import time @@ -9,57 +8,9 @@ from rendercanvas.offscreen import loop as run_offscreen from .api import shader_args_from_json, shadertoy_from_id +from .imgui import parse_constants, make_uniform, replace_constants, get_backend from .passes import BufferRenderPass, ImageRenderPass, RenderPass - - -class UniformArray: - """Convenience class to create a uniform array. - - Ensure that the order matches structs in the shader code. - See https://www.w3.org/TR/WGSL/#alignment-and-size for reference on alignment. - """ - - def __init__(self, *args): - # Analyse incoming fields - fields = [] - byte_offset = 0 - for name, format, n in args: - assert format in ("f", "i", "I") - field = name, format, byte_offset, byte_offset + n * 4 - fields.append(field) - byte_offset += n * 4 - # Get padding - nbytes = byte_offset - while nbytes % 16: - nbytes += 1 - # Construct memoryview object and a view for each field - self._mem = memoryview((ctypes.c_uint8 * nbytes)()).cast("B") - self._views = {} - for name, format, i1, i2 in fields: - self._views[name] = self._mem[i1:i2].cast(format) - - @property - def mem(self): - return self._mem - - @property - def nbytes(self): - return self._mem.nbytes - - def __getitem__(self, key): - v = self._views[key].tolist() - return v[0] if len(v) == 1 else v - - def __setitem__(self, key, val): - m = self._views[key] - n = m.shape[0] - if n == 1: - assert isinstance(val, (float, int)) - m[0] = val - else: - assert isinstance(val, (tuple, list)) - for i in range(n): - m[i] = val[i] +from .utils import UniformArray class Shadertoy: @@ -77,6 +28,7 @@ class Shadertoy: title (str): The title of the window. Defaults to "Shadertoy". complete (bool): Whether the shader is complete. Unsupported renderpasses or inputs will set this to False. Default is True. canvas (RenderCanvas): Optionally provide the canvas the image pass will render too. Defaults to None (means auto?) + imgui (bool): Automatiicaly parse constants and provide a imgui interface to change them. Default is False. The shader code must contain a entry point function: @@ -114,6 +66,7 @@ def __init__( title: str = "Shadertoy", complete: bool = True, canvas=None, + imgui: bool = False, ) -> None: self._uniform_data = UniformArray( ("mouse", "f", 4), @@ -155,6 +108,21 @@ def __init__( device_features.append(wgpu.FeatureName.float32_filterable) self._device = self._request_device(device_features) + + self._imgui = imgui + if self._imgui: + self._common_constants = parse_constants(self.common) + if self._common_constants: + self._common_constants_data = make_uniform(self._common_constants) + self._common_constants_buffer = self._device.create_buffer( + label="Common constants buffer", + size=self._common_constants_data.nbytes, + usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST, + ) + self.common = replace_constants(self.common, self._common_constants, 9) + # else not have this at all? + + self._prepare_canvas(canvas=canvas) self._bind_events() @@ -259,6 +227,9 @@ def _prepare_canvas(self, canvas=None): self._present_context.configure(device=self._device, format=self._format) + if self._imgui: + self._imgui_backend = get_backend(self._device, self._canvas, self._format) + def _bind_events(self): # event spec: https://jupyter-rfb.readthedocs.io/en/stable/events.html # events returns logical size, so we can multiply by the pixel ratio to get physical size! @@ -269,16 +240,20 @@ def on_resize(event): # TODO: do we want to call this every single time or only when the resize is done? # render loop is suspended during any window interaction anyway - will be fixed with rendercanvas: https://github.com/pygfx/rendercanvas/issues/69 buf.resize_buffer() + if self._imgui: + self._imgui_backend.io.display_size = (w,h) def on_mouse_move(event): - if event["button"] == 1 or 1 in event["buttons"]: + if (event["button"] == 1 or 1 in event["buttons"]) and not (self._imgui and self._imgui_backend.io.want_capture_mouse): _, _, x2, y2 = self._uniform_data["mouse"] ratio = self._uniform_data["resolution"][2] x1, y1 = event["x"] * ratio, self.resolution[1] - event["y"] * ratio self._uniform_data["mouse"] = x1, y1, abs(x2), -abs(y2) + if self._imgui: + self._imgui_backend.io.add_mouse_pos_event(event["x"], event["y"]) def on_mouse_down(event): - if event["button"] == 1 or 1 in event["buttons"]: + if (event["button"] == 1 or 1 in event["buttons"]) and not (self._imgui and self._imgui_backend.io.want_capture_mouse): ratio = self._uniform_data["resolution"][2] x, y = event["x"] * ratio, self.resolution[1] - event["y"] * ratio self._uniform_data["mouse"] = (x, y, abs(x), abs(y)) @@ -288,10 +263,17 @@ def on_mouse_up(event): x1, y1, x2, y2 = self._uniform_data["mouse"] self._uniform_data["mouse"] = x1, y1, -abs(x2), -abs(y2) + def on_mouse(event): + event_type = event["event_type"] + down = event_type == "pointer_down" + if self._imgui: + self._imgui_backend.io.add_mouse_button_event(event["button"] - 1, down) + self._canvas.add_event_handler(on_resize, "resize") self._canvas.add_event_handler(on_mouse_move, "pointer_move") self._canvas.add_event_handler(on_mouse_down, "pointer_down") self._canvas.add_event_handler(on_mouse_up, "pointer_up") + self._canvas.add_event_handler(on_mouse, "pointer_up", "pointer_down") def _update(self): now = time.perf_counter() @@ -352,6 +334,14 @@ def show(self): run_offscreen.run() else: loop.run() + if self._imgui: + for rp in self.renderpasses: + for constant in rp._constants: + # TODO: this check messes up due to precision sometimes! + if constant.value != rp._constants_data[constant.name]: + print( + f"{rp} Constant {constant.shader_dtype} {constant.name}: {constant.value} -> {rp._constants_data[constant.name]}" + ) def snapshot( self, diff --git a/wgpu_shadertoy/utils.py b/wgpu_shadertoy/utils.py new file mode 100644 index 0000000..e73e511 --- /dev/null +++ b/wgpu_shadertoy/utils.py @@ -0,0 +1,55 @@ +import ctypes + +# Helper classes to be used throughout the project + +class UniformArray: + """Convenience class to create a uniform array. + + Ensure that the order matches structs in the shader code. + See https://www.w3.org/TR/WGSL/#alignment-and-size for reference on alignment. + """ + + def __init__(self, *args): + """ + *args is an iterable with (name, format, dim) + """ + # Analyse incoming fields + fields = [] + byte_offset = 0 + for name, format, n in args: + assert format in ("f", "i", "I") + field = name, format, byte_offset, byte_offset + n * 4 + fields.append(field) + byte_offset += n * 4 + # Get padding + nbytes = byte_offset + while nbytes % 16: + nbytes += 1 + # Construct memoryview object and a view for each field + self._mem = memoryview((ctypes.c_uint8 * nbytes)()).cast("B") + self._views = {} + for name, format, i1, i2 in fields: + self._views[name] = self._mem[i1:i2].cast(format) + + @property + def mem(self): + return self._mem + + @property + def nbytes(self): + return self._mem.nbytes + + def __getitem__(self, key): + v = self._views[key].tolist() + return v[0] if len(v) == 1 else v + + def __setitem__(self, key, val): + m = self._views[key] + n = m.shape[0] + if n == 1: + assert isinstance(val, (float, int)) + m[0] = val + else: + assert isinstance(val, (tuple, list)) + for i in range(n): + m[i] = val[i]