From 495fe09181a8121405598b01fac7ef60a9dea48c Mon Sep 17 00:00:00 2001 From: Tushar <30565750+tushar5526@users.noreply.github.com> Date: Fri, 17 Nov 2023 15:12:06 +0530 Subject: [PATCH] fix jitter behaviour of skia due to double buffers swapping (#446) * fix jitter behaviour of skia due to double buffers swapping * fix: fix pylint errors * fix: add exit method and fix size calls in setup * Update requirements.txt * :art: Python code fromated with psf/black (#447) Co-authored-by: tushar5526 * update mouse pos logic * :art: Python code fromated with psf/black (#448) Co-authored-by: tushar5526 --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: tushar5526 --- p5/sketch/Skia2DRenderer/base.py | 69 ++++++++++++++------------ p5/sketch/Skia2DRenderer/handlers.py | 19 +++++-- p5/sketch/Skia2DRenderer/renderer2d.py | 9 ++++ p5/sketch/userspace.py | 3 +- requirements.txt | 2 +- 5 files changed, 63 insertions(+), 39 deletions(-) diff --git a/p5/sketch/Skia2DRenderer/base.py b/p5/sketch/Skia2DRenderer/base.py index 77f6835b..4483903c 100644 --- a/p5/sketch/Skia2DRenderer/base.py +++ b/p5/sketch/Skia2DRenderer/base.py @@ -1,9 +1,7 @@ -import builtins from p5.core import p5 -import contextlib, glfw, skia +import skia from OpenGL import GL -from time import time import copy from ..events import handler_names @@ -76,20 +74,14 @@ def clean_up(self): def glfw_window(self): if not glfw.init(): raise RuntimeError("glfw.init() failed") + window = glfw.create_window(*self._size, "p5py", None, None) glfw.make_context_current(window) return window - def skia_surface(self, window=None, size=None): + def skia_surface(self): self.context = skia.GrDirectContext.MakeGL() - if size: - width, height = size - elif window: - width, height = glfw.get_framebuffer_size(window) - else: - raise ValueError( - "Both window and size can't be None, This is probably an error within p5 instead of the sketch" - ) + width, height = glfw.get_framebuffer_size(self.window) backend_render_target = skia.GrBackendRenderTarget( width, height, @@ -108,12 +100,10 @@ def skia_surface(self, window=None, size=None): return surface # create a new surface everytime - def create_surface(self, size=None): - if not size: - size = self._size - self._size = size - builtins.width, builtins.height = size - self.surface = self.skia_surface(self.window, size) + def create_surface(self): + self._size = glfw.get_framebuffer_size(self.window) + builtins.width, builtins.height = self._size + self.surface = self.skia_surface() self.canvas = self.surface.getCanvas() p5.renderer.initialize_renderer(self.canvas, self.paint, self.path) @@ -133,8 +123,11 @@ def main_loop(self): builtins.frame_count += 1 with self.surface as self.canvas: self.draw_method() + + p5.renderer._store_surface_state() self.surface.flushAndSubmit() glfw.swap_buffers(self.window) + p5.renderer._restore_surface_state() last_render_call_time = time() # If redraw == True, we have rendered the frame once @@ -157,27 +150,39 @@ def start(self): self.window = self.glfw_window() self.create_surface() self.assign_callbacks() - p5.renderer.initialize_renderer(self.canvas, self.paint, self.path) + + # We don't draw the buffer from scratch each time, instead store the current state of surface + # and restore it after swapping the buffer self.setup_method() - self.poll_events() p5.renderer.render() + + # Get snapshot of surface + p5.renderer._store_surface_state() + + # Write to secondary buffer self.surface.flushAndSubmit() glfw.swap_buffers(self.window) + p5.renderer._restore_surface_state() + self.surface.flushAndSubmit() + + # Buffers are swapped twice so that both buffers have the same initial surface state + glfw.swap_buffers(self.window) + self.main_loop() self.clean_up() def resize(self): + # when glfw changes the framebuffer size, we will be resized completely + # until then hold the rendering calls + self.resized = False + # call change the window size(), this will not be done instantly # but after some time and a frame_buffer_changed callback will occur on # on a different thread glfw.set_window_size(self.window, *self.size) - # when glfw changes the framebuffer size, we will be resized completely - # until then hold the rendering calls - self.resized = False - def poll_events(self): glfw.poll_events() if glfw.get_key( @@ -216,20 +221,16 @@ def frame_buffer_resize_callback_handler(self, window, width, height): # Creates an Image of current surface and a copy of current style configurations # For the purpose of handling setup_method() re-call # Ref: Issue #419 - old_image = self.surface.makeImageSnapshot() - old_image = old_image.resize(old_image.width(), old_image.height()) old_style = copy.deepcopy(p5.renderer.style) GL.glViewport(0, 0, width, height) - self.create_surface(size=(width, height)) + self.create_surface() self.setup_method() + p5.renderer._store_surface_state() + self.surface.flushAndSubmit() + glfw.swap_buffers(self.window) - # Redraws Image on the canvas/ new frame buffer - # Previously stored style configurations are restored for - # discarding setup_method() style changes p5.renderer.style = old_style - with self.surface as self.canvas: - self.canvas.drawImage(old_image, 0, 0) # Tell the program, we have resized the frame buffer # and do not rewind/clear the path @@ -238,3 +239,7 @@ def frame_buffer_resize_callback_handler(self, window, width, height): def _enqueue_event(self, handler_name, event): self.handler_queue.append((self.handlers[handler_name], event)) + + def exit(self): + self.clean_up() + exit() diff --git a/p5/sketch/Skia2DRenderer/handlers.py b/p5/sketch/Skia2DRenderer/handlers.py index a7af66d3..fee9cc15 100644 --- a/p5/sketch/Skia2DRenderer/handlers.py +++ b/p5/sketch/Skia2DRenderer/handlers.py @@ -4,6 +4,7 @@ import builtins +import platform import glfw from p5.core import p5 from p5.sketch.events import KeyEvent, MouseEvent @@ -130,8 +131,7 @@ def __init__( def on_mouse_button(window, button, action, mod): - pos = glfw.get_cursor_pos(window) - + pos = _adjust_mouse_pos(window, glfw.get_cursor_pos(window)) if button < 3: button = BUTTONMAP.get(button, 0) @@ -152,7 +152,7 @@ def on_mouse_button(window, button, action, mod): def on_mouse_scroll(window, x_off, y_off): - pos = glfw.get_cursor_pos(window) + pos = _adjust_mouse_pos(window, glfw.get_cursor_pos(window)) delta = (float(x_off), float(y_off)) event = PseudoMouseEvent(pos=pos, delta=delta, modifiers=input_state.modifiers) mev = MouseEvent(event, active=builtins.mouse_is_pressed) @@ -161,7 +161,9 @@ def on_mouse_scroll(window, x_off, y_off): def on_mouse_motion(window, x, y): - event = PseudoMouseEvent(pos=(x, y), modifiers=input_state.modifiers) + event = PseudoMouseEvent( + _adjust_mouse_pos(window, (x, y)), modifiers=input_state.modifiers + ) mev = MouseEvent(event, active=builtins.mouse_is_pressed) # Queue a 'mouse_dragged` or `mouse_moved` event, not both similar to p5.js @@ -220,3 +222,12 @@ def on_window_focus(window, focused): def on_close(window): pass + + +def _adjust_mouse_pos(window, pos): + if platform.system() != "Darwin": + return pos + glfw.get_window_content_scale(window) + pos_x, pos_y = pos + mul_x, mul_y = glfw.get_window_content_scale(window) + return pos_x * mul_x, pos_y * mul_y diff --git a/p5/sketch/Skia2DRenderer/renderer2d.py b/p5/sketch/Skia2DRenderer/renderer2d.py index b5e45e71..9d2e94ae 100644 --- a/p5/sketch/Skia2DRenderer/renderer2d.py +++ b/p5/sketch/Skia2DRenderer/renderer2d.py @@ -72,6 +72,15 @@ def __init__(self): self.path = None self.curve_tightness = 0 self.pimage = None + # used to store the surface state before swapping with secondary buffer + self._surface_image = None + + # TODO: Optimise it using bitmap or pixmap + def _store_surface_state(self): + self._surface_image = self.canvas.getSurface().makeImageSnapshot() + + def _restore_surface_state(self): + self.canvas.drawImage(self._surface_image, 0, 0) # Transforms functions def push_matrix(self): diff --git a/p5/sketch/userspace.py b/p5/sketch/userspace.py index 6e97de09..c2789c85 100644 --- a/p5/sketch/userspace.py +++ b/p5/sketch/userspace.py @@ -309,8 +309,7 @@ def exit(): before exiting the sketch. """ - if p5.sketch is not None and builtins.current_renderer == "vispy": - p5.sketch.exit() + p5.sketch.exit() def no_cursor(): diff --git a/requirements.txt b/requirements.txt index c362368e..71a54e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ glfw>=2.5.9 numpy Pillow==9.0.1 vispy==0.10.0 -PyOpenGL-accelerate==3.1.6 +PyOpenGL-accelerate==3.1.7 PyOpenGL==3.1.6 requests>=2.25.0 dataclasses;python_version=="3.6"