diff --git a/src/topsy/canvas.py b/src/topsy/canvas.py index 92954a5..7d230ca 100644 --- a/src/topsy/canvas.py +++ b/src/topsy/canvas.py @@ -12,9 +12,14 @@ class VisualizerCanvas(WgpuCanvas): def __init__(self, *args, **kwargs): self._visualizer : Visualizer = kwargs.pop("visualizer") - super().__init__(*args, **kwargs) + self._last_x = 0 self._last_y = 0 + # The below are dummy values that will be updated by the initial resize event + self.width_physical, self.height_physical = 640,480 + self.pixel_ratio = 1 + + super().__init__(*args, **kwargs) def handle_event(self, event): if event['event_type']=='pointer_move': @@ -54,4 +59,6 @@ def mouse_wheel(self, delta_x, delta_y): self._visualizer.scale*=np.exp(delta_y/1000) def resize(self, width, height, pixel_ratio=1): - pass \ No newline at end of file + self.width_physical = width*pixel_ratio + self.height_physical = height*pixel_ratio + self.pixel_ratio = pixel_ratio \ No newline at end of file diff --git a/src/topsy/colorbar.py b/src/topsy/colorbar.py index e1179f9..7eaed46 100644 --- a/src/topsy/colorbar.py +++ b/src/topsy/colorbar.py @@ -7,14 +7,16 @@ class ColorbarOverlay(Overlay): - def __init__(self, visualizer, vmin, vmax, colormap, label, *, dpi=200, **kwargs): - self.dpi = dpi + def __init__(self, visualizer, vmin, vmax, colormap, label, *, dpi_logical=72, **kwargs): + self.dpi_logical = dpi_logical self.kwargs = kwargs - self._aspect_ratio = 0.15 + self._aspect_ratio = 0.2 self.vmin = vmin self.vmax = vmax self.colormap = colormap self.label = label + self._last_width = None + self._last_height = None super().__init__(visualizer) @@ -23,9 +25,18 @@ def get_clipspace_coordinates(self, pixel_width, pixel_height): height = 2.0 width = 2.0*pixel_height*im.shape[1]/im.shape[0]/pixel_width x,y = 1.0-width,-1.0 + if self._last_width!=pixel_width or self._last_height!=pixel_height: + # contents is the wrong size + self.update() + self._last_width = pixel_width + self._last_height = pixel_height return x, y, width, height def render_contents(self): - fig = plt.figure(figsize=(10 * self._aspect_ratio, 10), dpi=200, + dpi_physical = self.dpi_logical*self._visualizer.canvas.pixel_ratio + + fig = plt.figure(figsize=(self._visualizer.canvas.height_physical * self._aspect_ratio/dpi_physical, + self._visualizer.canvas.height_physical/dpi_physical), + dpi=dpi_physical, facecolor=(1.0, 1.0, 1.0, 0.5)) cmap = matplotlib.colormaps[self.colormap] diff --git a/src/topsy/colormap.py b/src/topsy/colormap.py index 4e03b28..b37e757 100644 --- a/src/topsy/colormap.py +++ b/src/topsy/colormap.py @@ -81,7 +81,7 @@ def _setup_texture(self, num_points=config.COLORMAP_NUM_SAMPLES): ) def _setup_render_pipeline(self): - self._vmin_vmax_buffer = self._device.create_buffer(size =4 * 2, + self._parameter_buffer = self._device.create_buffer(size =4 * 3, usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST) self._bind_group_layout = \ @@ -112,7 +112,7 @@ def _setup_render_pipeline(self): }, { "binding": 4, - "visibility": wgpu.ShaderStage.FRAGMENT, + "visibility": wgpu.ShaderStage.FRAGMENT | wgpu.ShaderStage.VERTEX, "buffer": {"type": wgpu.BufferBindingType.uniform} } ] @@ -139,9 +139,9 @@ def _setup_render_pipeline(self): "resource": self._input_interpolation, }, {"binding": 4, - "resource": {"buffer": self._vmin_vmax_buffer, + "resource": {"buffer": self._parameter_buffer, "offset": 0, - "size": self._vmin_vmax_buffer.size} + "size": self._parameter_buffer.size} } ] ) @@ -192,6 +192,8 @@ def _setup_render_pipeline(self): def encode_render_pass(self, command_encoder): display_texture = self._visualizer.context.get_current_texture() + + self._update_parameter_buffer(display_texture.size[0], display_texture.size[1]) colormap_render_pass = command_encoder.begin_render_pass( color_attachments=[ { @@ -238,13 +240,17 @@ def set_vmin_vmax(self): logger.warning("Press 'r' in the window to try again") self.vmin, self.vmax = 0.0, 1.0 - self._update_vmin_vmax_buffer() + def _update_parameter_buffer(self, width, height): + parameter_dtype = [("vmin", np.float32, (1,)), + ("vmax", np.float32, (1,)), + ("window_aspect_ratio", np.float32, (1,))] + + parameters = np.zeros((), dtype=parameter_dtype) + parameters["vmin"] = self.vmin + parameters["vmax"] = self.vmax + + self._visualizer.context.get_current_texture() - def _update_vmin_vmax_buffer(self): - vmin_vmax_dtype = [("vmin", np.float32, (1,)), - ("vmax", np.float32, (1,))] - vmin_vmax = np.zeros((), dtype=vmin_vmax_dtype) - vmin_vmax["vmin"] = self.vmin - vmin_vmax["vmax"] = self.vmax - self._device.queue.write_buffer(self._vmin_vmax_buffer, 0, vmin_vmax) \ No newline at end of file + parameters["window_aspect_ratio"] = float(width)/height + self._device.queue.write_buffer(self._parameter_buffer, 0, parameters) \ No newline at end of file diff --git a/src/topsy/config.py b/src/topsy/config.py index 52726b9..ed00ed7 100644 --- a/src/topsy/config.py +++ b/src/topsy/config.py @@ -5,6 +5,7 @@ TARGET_FPS = 30 # will use downsampling to achieve this FULL_RESOLUTION_RENDER_AFTER = 0.3 # inactivity seconds to wait before rendering without downs +STATUS_LINE_UPDATE_INTERVAL = 0.2 # seconds COLORBAR_ASPECT_RATIO = 0.15 COLORMAP_NUM_SAMPLES = 1000 \ No newline at end of file diff --git a/src/topsy/overlay.py b/src/topsy/overlay.py index b3351e7..da66ca5 100644 --- a/src/topsy/overlay.py +++ b/src/topsy/overlay.py @@ -87,13 +87,18 @@ def _setup_texture(self): def _setup_params_buffer(self): self._overlay_params_buffer = self._device.create_buffer( label="overlay_params_buffer", - size=4*4, + size=4*8, usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST ) + def get_texturespace_coordinates(self, width, height) -> tuple[float,float,float,float]: + return (0.0,0.0,1.0,1.0) + def _update_params_buffer(self, width, height): x, y, w, h = self.get_clipspace_coordinates(width, height) - self._device.queue.write_buffer(self._overlay_params_buffer, 0, np.array([x,y,w,h], dtype=np.float32).tobytes()) + x_t, y_t, w_t, h_t = self.get_texturespace_coordinates(width, height) + self._device.queue.write_buffer(self._overlay_params_buffer, 0, + np.array([x,y,w,h,x_t,y_t,w_t,h_t], dtype=np.float32).tobytes()) def _setup_render_pipeline(self): @@ -138,7 +143,7 @@ def _setup_render_pipeline(self): "resource": { "buffer": self._overlay_params_buffer, "offset": 0, - "size": 4*4, + "size": 4*8, }, }, { @@ -222,6 +227,10 @@ def get_contents(self) -> np.ndarray: if self._contents is None: self._contents = self.render_contents() return self._contents + + def invalidate_contents(self): + """Mark the contents as invalid, needing a re-render""" + self._contents = None @abstractmethod def render_contents(self) -> np.ndarray: """Must return a 2D image with RGBA channels for display.""" diff --git a/src/topsy/scalebar.py b/src/topsy/scalebar.py index d8956ae..1e7ec97 100644 --- a/src/topsy/scalebar.py +++ b/src/topsy/scalebar.py @@ -42,6 +42,16 @@ def __init__(self, visualizer: Visualizer): def encode_render_pass(self, command_encoder: wgpu.GPUCommandEncoder): physical_scalebar_length = self._recommend_physical_scalebar_length() self._bar.length = physical_scalebar_length / self._visualizer.scale + # note that the visualizer scale refers to a square rendering target + # however only part of this is shown in the final window if the window + # aspect ratio isn't 1:1. So we now need to correct for this effect. + # The full x extent is shown if the width is greater than the height, so + # no correction is needed then. If the height is greater than the width, + # then the x extent is scaled by the ratio of the height to the width. + + if self._visualizer.canvas.width_physical < self._visualizer.canvas.height_physical: + self._bar.length *= self._visualizer.canvas.height_physical / self._visualizer.canvas.width_physical + self._update_scalebar_label(physical_scalebar_length) self._label.encode_render_pass(command_encoder) diff --git a/src/topsy/shaders/colormap.wgsl b/src/topsy/shaders/colormap.wgsl index 0d9e21b..68f886c 100644 --- a/src/topsy/shaders/colormap.wgsl +++ b/src/topsy/shaders/colormap.wgsl @@ -1,6 +1,7 @@ struct ColormapParams { vmin: f32, - vmax: f32 + vmax: f32, + window_aspect_ratio: f32 }; struct VertexOutput { @@ -13,6 +14,22 @@ struct FragmentOutput { } +@group(0) @binding(0) +var image_texture: texture_2d; + +@group(0) @binding(1) +var image_sampler: sampler; + +@group(0) @binding(2) +var colormap_texture: texture_1d; + +@group(0) @binding(3) +var colormap_sampler: sampler; + +@group(0) @binding(4) +var colormap_params: ColormapParams; + + @vertex fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { var pos = array, 4>( @@ -22,6 +39,16 @@ fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { vec2(1.0, 1.0) ); + if(colormap_params.window_aspect_ratio>1.0) { + for(var i = 0u; i<4u; i=i+1u) { + pos[i].y = pos[i].y*colormap_params.window_aspect_ratio; + } + } else { + for(var i = 0u; i<4u; i=i+1u) { + pos[i].x = pos[i].x/colormap_params.window_aspect_ratio; + } + } + var texc = array, 4>( vec2(0.0, 1.0), vec2(0.0, 0.0), @@ -37,21 +64,6 @@ fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { return output; } -@group(0) @binding(0) -var image_texture: texture_2d; - -@group(0) @binding(1) -var image_sampler: sampler; - -@group(0) @binding(2) -var colormap_texture: texture_1d; - -@group(0) @binding(3) -var colormap_sampler: sampler; - -@group(0) @binding(4) -var colormap_params: ColormapParams; - @fragment fn fragment_main(input: VertexOutput) -> FragmentOutput { diff --git a/src/topsy/shaders/overlay.wgsl b/src/topsy/shaders/overlay.wgsl index cfbeebc..9a3821d 100644 --- a/src/topsy/shaders/overlay.wgsl +++ b/src/topsy/shaders/overlay.wgsl @@ -1,6 +1,8 @@ struct OverlayParams { - origin: vec2, - extent: vec2 + clipspace_origin: vec2, + clipspace_extent: vec2, + texturespace_origin: vec2, + texturespace_extent: vec2, }; @group(0) @binding(0) @@ -14,7 +16,7 @@ struct VertexOutput { @vertex fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { - var texc = array, 4>( + var offsets = array, 4>( vec2(0.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 0.0), @@ -23,13 +25,12 @@ fn vertex_main(@builtin(vertex_index) vertexIndex : u32) -> VertexOutput { var output: VertexOutput; - output.texcoord = texc[vertexIndex]; - - var posOffset = output.texcoord; + var posOffset = offsets[vertexIndex]; posOffset.y = 1.0 - posOffset.y; - posOffset *= overlay_params.extent; + posOffset *= overlay_params.clipspace_extent; - output.pos = vec4(overlay_params.origin + posOffset, 0.0, 1.0); + output.pos = vec4(overlay_params.clipspace_origin + posOffset, 0.0, 1.0); + output.texcoord = overlay_params.texturespace_origin + offsets[vertexIndex]*overlay_params.texturespace_extent; return output; } diff --git a/src/topsy/visualizer.py b/src/topsy/visualizer.py index 3e4816a..aedd7d7 100644 --- a/src/topsy/visualizer.py +++ b/src/topsy/visualizer.py @@ -185,7 +185,7 @@ def draw(self): def _update_and_display_status(self, command_encoder): now = time.time() - if now - self._last_status_update > 0.2: + if now - self._last_status_update > config.STATUS_LINE_UPDATE_INTERVAL: self._last_status_update = now self._status.text = f"${1.0 / self._render_timer.running_mean_duration:.0f}$ fps" if self._sph.downsample_factor > 1: