From 2aa7a26161e609696631ddce810ac6fd603a7465 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Jun 2024 22:45:26 +0200 Subject: [PATCH 01/31] initial Makie setup --- src/interaction/events.jl | 7 +++++-- src/types.jl | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/interaction/events.jl b/src/interaction/events.jl index 1e0db419b6b..c20300cc4cf 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -10,6 +10,7 @@ unicode_input(scene, native_window) = not_implemented_for(native_window) dropped_files(scene, native_window) = not_implemented_for(native_window) hasfocus(scene, native_window) = not_implemented_for(native_window) entered_window(scene, native_window) = not_implemented_for(native_window) +frame_tick(scene, native_window) = not_implemented_for(native_window) function connect_screen(scene::Scene, screen) @@ -27,6 +28,7 @@ function connect_screen(scene::Scene, screen) dropped_files(scene, screen) hasfocus(scene, screen) entered_window(scene, screen) + frame_tick(scene, screen) return end @@ -49,12 +51,13 @@ function disconnect_screen(scene::Scene, screen) disconnect!(screen, dropped_files) disconnect!(screen, hasfocus) disconnect!(screen, entered_window) + disconnect!(screen, frame_tick) end return end """ -Picks a mouse position. Implemented by the backend. +Picks a mouse position. Implemented by the backend. """ function pick end @@ -65,7 +68,7 @@ end """ onpick(func, plot) -Calls `func` if one clicks on `plot`. Implemented by the backend. +Calls `func` if one clicks on `plot`. Implemented by the backend. """ function onpick end diff --git a/src/types.jl b/src/types.jl index a619d0510bb..9fcf4c24a17 100644 --- a/src/types.jl +++ b/src/types.jl @@ -16,6 +16,12 @@ end include("interaction/iodevices.jl") +@enum FrameState begin + FrameStateUnknown + FirstFrame + LastFrameSkipped + LastFrameRendered +end """ This struct provides accessible `Observable`s to monitor the events @@ -103,6 +109,18 @@ struct Events Whether the mouse is inside the window or not. """ entered_window::Observable{Bool} + + """ + Triggers once per frame. + + The details differ somewhat by backend: + - GLMakie: Triggers after other events have been processed and before a new + frame is submitted to drawing. This means that the other event Observables + are up to date when this triggers. + - CairoMakie: Triggers once before drawing takes place, i.e. once per `display` or `save`. + - WGLMakie: TODO + """ + tick::Observable{FrameState} end function Base.show(io::IO, events::Events) @@ -132,6 +150,7 @@ function Events() Observable(String[]), Observable(false), Observable(false), + Observable(FrameStateUnknown) ) connect_states!(events) From b16bc8934f22e9a454f55a671a55f69fabc6a997 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 7 Jun 2024 22:45:43 +0200 Subject: [PATCH 02/31] update CairoMakie --- CairoMakie/src/infrastructure.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 46e0e3bcc2d..81738a41b4c 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -8,6 +8,8 @@ # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) + scene.events.tick[] = Makie.FirstFrame + Cairo.save(screen.context) draw_background(screen, scene) From 8f5a02172e1c7cd0d72d32bd8874542079f03dba Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 8 Jun 2024 14:15:57 +0200 Subject: [PATCH 03/31] track delta time and adjust states --- CairoMakie/src/infrastructure.jl | 3 ++- src/types.jl | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 81738a41b4c..7bc87cdbc21 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -8,7 +8,8 @@ # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) - scene.events.tick[] = Makie.FirstFrame + # technically we haven't rendered yet, but we know we will + scene.events.tick[] = Makie.Tick(Makie.OneTimeRenderTick, 0.0, 0.0) Cairo.save(screen.context) draw_background(screen, scene) diff --git a/src/types.jl b/src/types.jl index 9fcf4c24a17..55767503218 100644 --- a/src/types.jl +++ b/src/types.jl @@ -16,13 +16,22 @@ end include("interaction/iodevices.jl") -@enum FrameState begin - FrameStateUnknown - FirstFrame - LastFrameSkipped - LastFrameRendered +@enum TickState begin + UtilityTick # outside of rendering, e.g. plot insertions + UnknownTickState # everything else + OneTimeRenderTick # render outside of renderloop, e.g. colorbuffer() + PausedRenderTick # render loop paused + SkippedRenderTick # render skipped due to frame reuse + RegularRenderTick # normal render end +struct Tick + state::TickState + event_delta_time::Float64 # always set + frame_delta_time::Float64 # 0.0 outside of render loop ticks +end +Tick() = Tick(UnknownTickState, 0.0, 0.0) + """ This struct provides accessible `Observable`s to monitor the events associated with a Scene. @@ -120,7 +129,7 @@ struct Events - CairoMakie: Triggers once before drawing takes place, i.e. once per `display` or `save`. - WGLMakie: TODO """ - tick::Observable{FrameState} + tick::Observable{Tick} end function Base.show(io::IO, events::Events) @@ -150,7 +159,7 @@ function Events() Observable(String[]), Observable(false), Observable(false), - Observable(FrameStateUnknown) + Observable(Tick()) ) connect_states!(events) From 7e3265902d806a2f2d8b69e2f46d5fc49133c9ad Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 8 Jun 2024 14:17:18 +0200 Subject: [PATCH 04/31] implement tick events in GLMakie --- GLMakie/src/display.jl | 2 +- GLMakie/src/drawing_primitives.jl | 8 ++++---- GLMakie/src/events.jl | 34 ++++++++++++++++++++++++++++++- GLMakie/src/screen.jl | 27 +++++++++++++----------- 4 files changed, 53 insertions(+), 18 deletions(-) diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index c8cb5a85064..6d1269f2024 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -10,7 +10,7 @@ function Base.display(screen::Screen, scene::Scene; connect=true) else @assert screen.root_scene === scene "internal error. Scene already displayed by screen but not as root scene" end - pollevents(screen) + pollevents(screen, Makie.UtilityTick) return screen end diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index ca667c5ca97..8ba0ab042c5 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -24,7 +24,7 @@ function handle_lights(attr::Dict, screen::Screen, lights::Vector{Makie.Abstract attr[:light_colors] = Observable(sizehint!(RGBf[], MAX_LIGHTS)) attr[:light_parameters] = Observable(sizehint!(Float32[], MAX_PARAMS)) - on(screen.render_tick, priority = typemin(Int)) do _ + on(screen.render_tick, priority = -1000) do _ # derive number of lights from available lights. Both MAX_LIGHTS and # MAX_PARAMS are considered for this. n_lights = 0 @@ -231,7 +231,7 @@ const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.UtilityTick) robj = get!(screen.cache, objectid(plot)) do filtered = filter(plot.attributes) do (k, v) @@ -318,13 +318,13 @@ Base.insert!(::GLMakie.Screen, ::Scene, ::Makie.PlotList) = nothing function Base.insert!(screen::Screen, scene::Scene, @nospecialize(x::Plot)) ShaderAbstractions.switch_context!(screen.glscreen) # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.UtilityTick) if isempty(x.plots) # if no plots inserted, this truly is an atomic draw_atomic(screen, scene, x) else foreach(x.plots) do x # poll inside functions to make wait on compile less prominent - pollevents(screen) + pollevents(screen, Makie.UtilityTick) insert!(screen, scene, x) end end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 52f631095ce..a3a93a3b490 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -185,7 +185,7 @@ struct MousePositionUpdater hasfocus::Observable{Bool} end -function (p::MousePositionUpdater)(::Nothing) +function (p::MousePositionUpdater)(::Makie.TickState) !p.hasfocus[] && return nw = to_native(p.screen) x, y = GLFW.GetCursorPos(nw) @@ -294,3 +294,35 @@ end function Makie.disconnect!(window::GLFW.Window, ::typeof(entered_window)) GLFW.SetCursorEnterCallback(window, nothing) end + +# Just for finding the relevant listener +mutable struct TickCallback + last_event_time::UInt64 + last_frame_time::UInt64 + TickCallback() = new(time_ns(), time_ns()) +end + +function (cb::TickCallback)(x::Makie.TickState) + t = time_ns() + event_delta_time = 1e-9 * (t - cb.last_event_time) + frame_delta_time = 0.0 + + if x > Makie.OneTimeRenderTick # Paused, Skipped or rendered frame tick + frame_delta_time = 1e-9 * (t - cb.last_frame_time) + cb.last_frame_time = t + end + cb.last_event_time = t + + return Makie.Tick(x, event_delta_time, frame_delta_time) +end + +function Makie.frame_tick(scene::Scene, screen::Screen) + # Separating screen ticks from event ticks allows us to sanitize: + # Internal on-tick event updates happen first (mouseposition), no blocking + # listeners, set order + map!(TickCallback(), scene, scene.events.tick, screen.render_tick, priority = typemin(Int)) +end +function Makie.disconnect!(screen::Screen, ::typeof(Makie.frame_tick)) + connections = filter(x -> x[2] isa TickCallback, screen.render_tick.listeners) + foreach(x -> off(screen.render_tick, x[2]), connections) +end \ No newline at end of file diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 0bab3fd248e..32308f78b4c 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -172,7 +172,7 @@ mutable struct Screen{GLWindow} <: MakieScreen cache::Dict{UInt64, RenderObject} cache2plot::Dict{UInt32, AbstractPlot} framecache::Matrix{RGB{N0f8}} - render_tick::Observable{Nothing} # listeners must not Consume(true) + render_tick::Observable{Makie.TickState} # listeners must not Consume(true) window_open::Observable{Bool} scalefactor::Observable{Float32} @@ -205,7 +205,7 @@ mutable struct Screen{GLWindow} <: MakieScreen config, stop_renderloop, rendertask, Observable(0f0), screen2scene, screens, renderlist, postprocessors, cache, cache2plot, - Matrix{RGB{N0f8}}(undef, s), Observable(nothing), + Matrix{RGB{N0f8}}(undef, s), Observable(Makie.UnknownTickState), Observable(true), Observable(0f0), nothing, reuse, true, false ) push!(ALL_SCREENS, screen) # track all created screens @@ -444,10 +444,10 @@ function Screen(scene::Scene, config::ScreenConfig, ::Makie.ImageStorageFormat; return screen end -function pollevents(screen::Screen) +function pollevents(screen::Screen, frame_state::Makie.TickState) ShaderAbstractions.switch_context!(screen.glscreen) GLFW.PollEvents() - notify(screen.render_tick) + screen.render_tick[] = frame_state return end @@ -727,7 +727,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - pollevents(screen) + pollevents(screen, Makie.OneTimeRenderTick) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible @@ -860,7 +860,7 @@ function set_framerate!(screen::Screen, fps=30) end function refreshwindowcb(screen, window) - screen.render_tick[] = nothing + screen.render_tick[] = Makie.UtilityTick render_frame(screen) GLFW.SwapBuffers(window) return @@ -886,14 +886,13 @@ end scalechangeobs(screen) = scalefactor -> scalechangeobs(screen, scalefactor) -# TODO add render_tick event to scene events function vsynced_renderloop(screen) while isopen(screen) && !screen.stop_renderloop if screen.config.pause_renderloop - pollevents(screen); sleep(0.1) + pollevents(screen, Makie.PausedRenderTick); sleep(0.1) continue end - pollevents(screen) # GLFW poll + pollevents(screen, Makie.RegularRenderTick) # GLFW poll render_frame(screen) GLFW.SwapBuffers(to_native(screen)) yield() @@ -903,12 +902,12 @@ end function fps_renderloop(screen::Screen) while isopen(screen) && !screen.stop_renderloop if screen.config.pause_renderloop - pollevents(screen); sleep(0.1) + pollevents(screen, Makie.PausedRenderTick); sleep(0.1) continue end time_per_frame = 1.0 / screen.config.framerate t = time_ns() - pollevents(screen) # GLFW poll + pollevents(screen, Makie.RegularRenderTick) # GLFW poll render_frame(screen) GLFW.SwapBuffers(to_native(screen)) t_elapsed = (time_ns() - t) / 1e9 @@ -931,14 +930,18 @@ function requires_update(screen::Screen) end function on_demand_renderloop(screen::Screen) + tick_state = Makie.UnknownTickState while isopen(screen) && !screen.stop_renderloop t = time_ns() time_per_frame = 1.0 / screen.config.framerate - pollevents(screen) # GLFW poll + pollevents(screen, tick_state) # GLFW poll if !screen.config.pause_renderloop && requires_update(screen) + tick_state = Makie.RegularRenderTick render_frame(screen) GLFW.SwapBuffers(to_native(screen)) + else + tick_state = ifelse(screen.config.pause_renderloop, Makie.PausedRenderTick, Makie.SkippedRenderTick) end t_elapsed = (time_ns() - t) / 1e9 From 8a0de3a97fa39f5c0d77b074cbb8550e53dab96c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 8 Jun 2024 15:07:20 +0200 Subject: [PATCH 05/31] implement ticks in WGLMakie --- WGLMakie/src/events.jl | 8 ++++++++ WGLMakie/src/wglmakie.bundled.js | 11 ++++++++++- WGLMakie/src/wglmakie.js | 10 +++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index bd129e83d53..55b3b44ea14 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -50,6 +50,7 @@ end function connect_scene_events!(scene::Scene, comm::Observable) e = events(scene) + last_time = Base.RefValue(time_ns()) on(comm) do msg @async try @handle msg.mouseposition begin @@ -105,10 +106,17 @@ function connect_scene_events!(scene::Scene, comm::Observable) @handle msg.resize begin resize!(scene, tuple(resize...)) end + @handle msg.tick begin + t = time_ns() + delta_time = 1e-9 * (t - last_time[]) + e.tick[] = Makie.Tick(Makie.RegularRenderTick, delta_time, delta_time) + last_time[] = t + end catch err @warn "Error in window event callback" exception=(err, Base.catch_backtrace()) end return end + return end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index ae5a10200b0..43330d98005 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -22757,11 +22757,13 @@ function render_scene(scene, picking = false) { return scene.scene_children.every((x)=>render_scene(x, picking)); } function start_renderloop(three_scene) { - const { fps } = three_scene.screen; + const { fps , renderer } = three_scene.screen; const time_per_frame = 1 / fps * 1000; let last_time_stamp = performance.now(); function renderloop(timestamp) { if (timestamp - last_time_stamp > time_per_frame) { + const canvas = renderer.domElement; + canvas.dispatchEvent(new Event('render')); const all_rendered = render_scene(three_scene); if (!all_rendered) { return; @@ -22942,6 +22944,13 @@ function add_canvas_events(screen, comm, resize_to) { window.addEventListener("resize", (event)=>resize_callback_throttled()); resize_callback_throttled(); } + function tick(event) { + comm.notify({ + tick: true + }); + return false; + } + canvas.addEventListener("render", tick); } function threejs_module(canvas) { let context = canvas.getContext("webgl2", { diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index d023b735243..a4836207fa1 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -63,12 +63,14 @@ export function render_scene(scene, picking = false) { function start_renderloop(three_scene) { // extract the first scene for screen, which should be shared by all scenes! - const { fps } = three_scene.screen; + const { fps, renderer } = three_scene.screen; const time_per_frame = (1 / fps) * 1000; // default is 30 fps // make sure we immediately render the first frame and dont wait 30ms let last_time_stamp = performance.now(); function renderloop(timestamp) { if (timestamp - last_time_stamp > time_per_frame) { + const canvas = renderer.domElement; + canvas.dispatchEvent( new Event('render') ); const all_rendered = render_scene(three_scene); if (!all_rendered) { // if scenes don't render it means they're not displayed anymore @@ -328,6 +330,12 @@ function add_canvas_events(screen, comm, resize_to) { // Fire the resize event once at the start to auto-size our window resize_callback_throttled(); } + + function tick(event) { + comm.notify({ tick: true, }); + return false; + } + canvas.addEventListener("render", tick); } function threejs_module(canvas) { From 5f6d59b6a572bb500e07a766e65f96338ffef890 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 8 Jun 2024 16:39:04 +0200 Subject: [PATCH 06/31] track frame_delta_time in record --- CairoMakie/src/infrastructure.jl | 7 ++++++- GLMakie/src/events.jl | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 7bc87cdbc21..823595c1ea4 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -6,10 +6,15 @@ # Drawing pipeline # ######################################## +const LAST_RENDER_TIME = Base.RefValue(time_ns()) + # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) # technically we haven't rendered yet, but we know we will - scene.events.tick[] = Makie.Tick(Makie.OneTimeRenderTick, 0.0, 0.0) + t = time_ns() + delta_time = 1e-9 * (t - LAST_RENDER_TIME[]) + scene.events.tick[] = Makie.Tick(Makie.OneTimeRenderTick, delta_time, delta_time) + LAST_RENDER_TIME[] = t Cairo.save(screen.context) draw_background(screen, scene) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index a3a93a3b490..5371226df42 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -307,7 +307,7 @@ function (cb::TickCallback)(x::Makie.TickState) event_delta_time = 1e-9 * (t - cb.last_event_time) frame_delta_time = 0.0 - if x > Makie.OneTimeRenderTick # Paused, Skipped or rendered frame tick + if x >= Makie.OneTimeRenderTick # Paused, Skipped or rendered frame tick, including colorbuffer() calls frame_delta_time = 1e-9 * (t - cb.last_frame_time) cb.last_frame_time = t end From 362ab8ae2d7c2e47f54f6dea4d2a526aeb645383 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 8 Jun 2024 16:46:39 +0200 Subject: [PATCH 07/31] update some comments --- src/types.jl | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/types.jl b/src/types.jl index 55767503218..a7b731630b2 100644 --- a/src/types.jl +++ b/src/types.jl @@ -19,16 +19,16 @@ include("interaction/iodevices.jl") @enum TickState begin UtilityTick # outside of rendering, e.g. plot insertions UnknownTickState # everything else - OneTimeRenderTick # render outside of renderloop, e.g. colorbuffer() + OneTimeRenderTick # render outside of renderloop, e.g. colorbuffer() (save, record) PausedRenderTick # render loop paused SkippedRenderTick # render skipped due to frame reuse RegularRenderTick # normal render end struct Tick - state::TickState - event_delta_time::Float64 # always set - frame_delta_time::Float64 # 0.0 outside of render loop ticks + state::TickState # flag for the type of tick event + event_delta_time::Float64 # updated on any tick event + frame_delta_time::Float64 # time between frames, 0.0 for other tick events end Tick() = Tick(UnknownTickState, 0.0, 0.0) @@ -120,14 +120,7 @@ struct Events entered_window::Observable{Bool} """ - Triggers once per frame. - - The details differ somewhat by backend: - - GLMakie: Triggers after other events have been processed and before a new - frame is submitted to drawing. This means that the other event Observables - are up to date when this triggers. - - CairoMakie: Triggers once before drawing takes place, i.e. once per `display` or `save`. - - WGLMakie: TODO + TODO """ tick::Observable{Tick} end From ac5d092e84fe6eb936493a8739571131169c3cc1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 9 Jun 2024 11:37:10 +0200 Subject: [PATCH 08/31] cleanup CairoMakie --- CairoMakie/src/infrastructure.jl | 7 ++----- CairoMakie/src/screen.jl | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 823595c1ea4..307a16c3ffc 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -6,15 +6,12 @@ # Drawing pipeline # ######################################## -const LAST_RENDER_TIME = Base.RefValue(time_ns()) - # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) - # technically we haven't rendered yet, but we know we will t = time_ns() - delta_time = 1e-9 * (t - LAST_RENDER_TIME[]) + delta_time = 1e-9 * (t - screen.last_render_time) scene.events.tick[] = Makie.Tick(Makie.OneTimeRenderTick, delta_time, delta_time) - LAST_RENDER_TIME[] = t + screen.last_render_time = t Cairo.save(screen.context) draw_background(screen, scene) diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index af5b7042a26..0ecab3b43b5 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -171,6 +171,7 @@ mutable struct Screen{SurfaceRenderType} <: Makie.MakieScreen antialias::Int # cairo_antialias_t visible::Bool config::ScreenConfig + last_render_time::UInt64 end function Base.empty!(screen::Screen) @@ -313,7 +314,7 @@ function Screen(scene::Scene, config::ScreenConfig, surface::Cairo.CairoSurface) restrict_pdf_version!(surface, Int(config.pdf_version)) end - return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config) + return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config, time_ns()) end ######################################## From d7bc6069e6fddf37bafac517cc9dac45b4fd7383 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 9 Jun 2024 12:01:05 +0200 Subject: [PATCH 09/31] cleanup WGLMakie --- WGLMakie/src/display.jl | 7 ++++++- WGLMakie/src/events.jl | 7 +++---- WGLMakie/src/three_plot.jl | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 01c7cf1629a..7e5d33f4ca7 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -53,8 +53,9 @@ mutable struct Screen <: Makie.MakieScreen displayed_scenes::Set{String} config::ScreenConfig canvas::Union{Nothing,Bonito.HTMLElement} + last_frame_time::UInt64 function Screen(scene::Union{Nothing,Scene}, config::ScreenConfig) - return new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing) + return new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, time_ns()) end end @@ -277,6 +278,10 @@ function Makie.colorbuffer(screen::Screen) Base.display(screen, screen.scene) end session = get_screen_session(screen; error="Not able to show scene in a browser") + t = time_ns() + delta_time = 1e-9 * (t - screen.last_frame_time) + screen.scene.events.tick[] = Makie.Tick(Makie.RegularRenderTick, delta_time, delta_time) + screen.last_frame_time = t return session2image(session, screen.scene) end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index 55b3b44ea14..03310457810 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -48,9 +48,8 @@ function code_to_keyboard(code::String) end end -function connect_scene_events!(scene::Scene, comm::Observable) +function connect_scene_events!(screen::Screen, scene::Scene, comm::Observable) e = events(scene) - last_time = Base.RefValue(time_ns()) on(comm) do msg @async try @handle msg.mouseposition begin @@ -108,9 +107,9 @@ function connect_scene_events!(scene::Scene, comm::Observable) end @handle msg.tick begin t = time_ns() - delta_time = 1e-9 * (t - last_time[]) + delta_time = 1e-9 * (t - screen.last_frame_time) e.tick[] = Makie.Tick(Makie.RegularRenderTick, delta_time, delta_time) - last_time[] = t + screen.last_frame_time = t end catch err @warn "Error in window event callback" exception=(err, Base.catch_backtrace()) diff --git a/WGLMakie/src/three_plot.jl b/WGLMakie/src/three_plot.jl index aee3f71f7b8..c630d4711f9 100644 --- a/WGLMakie/src/three_plot.jl +++ b/WGLMakie/src/three_plot.jl @@ -62,6 +62,6 @@ function three_display(screen::Screen, session::Session, scene::Scene) on(session, done_init) do val window_open[] = true end - connect_scene_events!(scene, comm) + connect_scene_events!(screen, scene, comm) return wrapper, done_init end From 9243f0e839ec98c5c1a0191db7574a58114a0214 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 9 Jun 2024 12:01:19 +0200 Subject: [PATCH 10/31] add refimg test for record --- ReferenceTests/src/tests/updating.jl | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index 3c362fbea7f..c2fe6c6a4dd 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -173,3 +173,26 @@ end ax.title = "identity" Makie.step!(st) end + + +@reference_test "event ticks in record" begin + # Checks whether record calculates and triggers event.tick by drawing a + # Point at y = 1 for each frame where it does. The animation is irrelevant + # here, so we can jsut check the final image. + # The first point maybe at 0 depending on when the backend sets up it's + # reference time + ps = Observable(Point2f[]) + ps2 = Observable(Point2f[]) + f, a, p = scatter(ps) + scatter!(ps2, color = :red, marker = '+', markersize = 20) + xlims!(a, 0, 61) + ylims!(a, -0.1, 1.1) + Record(f, 1:60, framerate = 30) do i + push!(ps.val, Point2f(i, f.scene.events.tick[].frame_delta_time > 1e-6)) + push!(ps2.val, Point2f(i, f.scene.events.tick[].event_delta_time > 1e-6)) + notify(ps) + notify(ps2) + f.scene.events.tick[] = Makie.Tick(Makie.UnknownTickState, 0.0, 0.0) + end + f +end \ No newline at end of file From c3af54a38f0ba0971f7a72a7c5a21adaed0dc0fc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 15:52:17 +0200 Subject: [PATCH 11/31] add frame count, time since start, skip event ticks --- CairoMakie/src/infrastructure.jl | 6 ++---- CairoMakie/src/screen.jl | 5 +++-- GLMakie/src/display.jl | 2 +- GLMakie/src/drawing_primitives.jl | 6 +++--- GLMakie/src/events.jl | 22 ++++++++-------------- GLMakie/src/screen.jl | 2 +- WGLMakie/src/display.jl | 11 +++++------ WGLMakie/src/events.jl | 6 ++---- src/types.jl | 18 +++++++++++++----- 9 files changed, 38 insertions(+), 40 deletions(-) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index 307a16c3ffc..a99138aba38 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -8,10 +8,8 @@ # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) - t = time_ns() - delta_time = 1e-9 * (t - screen.last_render_time) - scene.events.tick[] = Makie.Tick(Makie.OneTimeRenderTick, delta_time, delta_time) - screen.last_render_time = t + screen.last_time = Makie.next_tick!( + scene.events.tick, Makie.OneTimeRenderTick, screen.start_time, screen.last_time) Cairo.save(screen.context) draw_background(screen, scene) diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 0ecab3b43b5..22d7dd8fed6 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -171,7 +171,8 @@ mutable struct Screen{SurfaceRenderType} <: Makie.MakieScreen antialias::Int # cairo_antialias_t visible::Bool config::ScreenConfig - last_render_time::UInt64 + start_time::UInt64 + last_time::UInt64 end function Base.empty!(screen::Screen) @@ -314,7 +315,7 @@ function Screen(scene::Scene, config::ScreenConfig, surface::Cairo.CairoSurface) restrict_pdf_version!(surface, Int(config.pdf_version)) end - return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config, time_ns()) + return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config, time_ns(), time_ns()) end ######################################## diff --git a/GLMakie/src/display.jl b/GLMakie/src/display.jl index 6d1269f2024..0763cf6023b 100644 --- a/GLMakie/src/display.jl +++ b/GLMakie/src/display.jl @@ -10,7 +10,7 @@ function Base.display(screen::Screen, scene::Scene; connect=true) else @assert screen.root_scene === scene "internal error. Scene already displayed by screen but not as root scene" end - pollevents(screen, Makie.UtilityTick) + pollevents(screen, Makie.BackendTick) return screen end diff --git a/GLMakie/src/drawing_primitives.jl b/GLMakie/src/drawing_primitives.jl index 8ba0ab042c5..14160d51588 100644 --- a/GLMakie/src/drawing_primitives.jl +++ b/GLMakie/src/drawing_primitives.jl @@ -231,7 +231,7 @@ const EXCLUDE_KEYS = Set([:transformation, :tickranges, :ticklabels, :raw, :SSAO function cached_robj!(robj_func, screen, scene, plot::AbstractPlot) # poll inside functions to make wait on compile less prominent - pollevents(screen, Makie.UtilityTick) + pollevents(screen, Makie.BackendTick) robj = get!(screen.cache, objectid(plot)) do filtered = filter(plot.attributes) do (k, v) @@ -318,13 +318,13 @@ Base.insert!(::GLMakie.Screen, ::Scene, ::Makie.PlotList) = nothing function Base.insert!(screen::Screen, scene::Scene, @nospecialize(x::Plot)) ShaderAbstractions.switch_context!(screen.glscreen) # poll inside functions to make wait on compile less prominent - pollevents(screen, Makie.UtilityTick) + pollevents(screen, Makie.BackendTick) if isempty(x.plots) # if no plots inserted, this truly is an atomic draw_atomic(screen, scene, x) else foreach(x.plots) do x # poll inside functions to make wait on compile less prominent - pollevents(screen, Makie.UtilityTick) + pollevents(screen, Makie.BackendTick) insert!(screen, scene, x) end end diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 5371226df42..6c77b790c80 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -297,30 +297,24 @@ end # Just for finding the relevant listener mutable struct TickCallback - last_event_time::UInt64 - last_frame_time::UInt64 - TickCallback() = new(time_ns(), time_ns()) + event::Observable{Makie.Tick} + start_time::UInt64 + last_time::UInt64 + TickCallback(tick::Observable{Makie.Tick}) = new(tick, time_ns(), time_ns()) end function (cb::TickCallback)(x::Makie.TickState) - t = time_ns() - event_delta_time = 1e-9 * (t - cb.last_event_time) - frame_delta_time = 0.0 - - if x >= Makie.OneTimeRenderTick # Paused, Skipped or rendered frame tick, including colorbuffer() calls - frame_delta_time = 1e-9 * (t - cb.last_frame_time) - cb.last_frame_time = t + if x > Makie.UnknownTickState # not backend or Unknown + cb.last_time = Makie.next_tick!(cb.event, x, cb.start_time, cb.last_time) end - cb.last_event_time = t - - return Makie.Tick(x, event_delta_time, frame_delta_time) + return nothing end function Makie.frame_tick(scene::Scene, screen::Screen) # Separating screen ticks from event ticks allows us to sanitize: # Internal on-tick event updates happen first (mouseposition), no blocking # listeners, set order - map!(TickCallback(), scene, scene.events.tick, screen.render_tick, priority = typemin(Int)) + on(TickCallback(scene.events.tick), scene, screen.render_tick, priority = typemin(Int)) end function Makie.disconnect!(screen::Screen, ::typeof(Makie.frame_tick)) connections = filter(x -> x[2] isa TickCallback, screen.render_tick.listeners) diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 32308f78b4c..8a67e555952 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -860,7 +860,7 @@ function set_framerate!(screen::Screen, fps=30) end function refreshwindowcb(screen, window) - screen.render_tick[] = Makie.UtilityTick + screen.render_tick[] = Makie.BackendTick render_frame(screen) GLFW.SwapBuffers(window) return diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 7e5d33f4ca7..ec96ea39a27 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -53,9 +53,10 @@ mutable struct Screen <: Makie.MakieScreen displayed_scenes::Set{String} config::ScreenConfig canvas::Union{Nothing,Bonito.HTMLElement} - last_frame_time::UInt64 + start_time::UInt64 + last_time::UInt64 function Screen(scene::Union{Nothing,Scene}, config::ScreenConfig) - return new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, time_ns()) + return new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, time_ns(), time_ns()) end end @@ -278,10 +279,8 @@ function Makie.colorbuffer(screen::Screen) Base.display(screen, screen.scene) end session = get_screen_session(screen; error="Not able to show scene in a browser") - t = time_ns() - delta_time = 1e-9 * (t - screen.last_frame_time) - screen.scene.events.tick[] = Makie.Tick(Makie.RegularRenderTick, delta_time, delta_time) - screen.last_frame_time = t + screen.last_time = Makie.next_tick!( + screen.scene.events.tick, Makie.RegularRenderTick, screen.start_time, screen.last_time) return session2image(session, screen.scene) end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index 03310457810..4639684dac3 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -106,10 +106,8 @@ function connect_scene_events!(screen::Screen, scene::Scene, comm::Observable) resize!(scene, tuple(resize...)) end @handle msg.tick begin - t = time_ns() - delta_time = 1e-9 * (t - screen.last_frame_time) - e.tick[] = Makie.Tick(Makie.RegularRenderTick, delta_time, delta_time) - screen.last_frame_time = t + screen.last_time = Makie.next_tick!( + e.tick, Makie.RegularRenderTick, screen.start_time, screen.last_time) end catch err @warn "Error in window event callback" exception=(err, Base.catch_backtrace()) diff --git a/src/types.jl b/src/types.jl index a7b731630b2..fb58fd1834f 100644 --- a/src/types.jl +++ b/src/types.jl @@ -17,7 +17,7 @@ end include("interaction/iodevices.jl") @enum TickState begin - UtilityTick # outside of rendering, e.g. plot insertions + BackendTick # for ticks only present in backends, e.g. GLMakie plot insertions UnknownTickState # everything else OneTimeRenderTick # render outside of renderloop, e.g. colorbuffer() (save, record) PausedRenderTick # render loop paused @@ -26,11 +26,19 @@ include("interaction/iodevices.jl") end struct Tick - state::TickState # flag for the type of tick event - event_delta_time::Float64 # updated on any tick event - frame_delta_time::Float64 # time between frames, 0.0 for other tick events + state::TickState # flag for the type of tick event + count::UInt64 # number of ticks since start + time::Float64 # time since scene initialization + delta_time::Float64 # time since last tick +end +Tick() = Tick(UnknownTickState, 0, 0.0, 0.0) +function next_tick!(tick::Observable{Tick}, state, start_time, last_time) + t = time_ns() + since_start = 1e-9 * (t - start_time) + delta_time = 1e-9 * (t - last_time) + tick[] = Tick(state, tick[].count + 1, since_start, delta_time) + return t end -Tick() = Tick(UnknownTickState, 0.0, 0.0) """ This struct provides accessible `Observable`s to monitor the events From 27aa39bec47c4d0ac253495842533482179c19dd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 15:53:44 +0200 Subject: [PATCH 12/31] update test --- ReferenceTests/src/tests/updating.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ReferenceTests/src/tests/updating.jl b/ReferenceTests/src/tests/updating.jl index c2fe6c6a4dd..bf177009373 100644 --- a/ReferenceTests/src/tests/updating.jl +++ b/ReferenceTests/src/tests/updating.jl @@ -178,21 +178,17 @@ end @reference_test "event ticks in record" begin # Checks whether record calculates and triggers event.tick by drawing a # Point at y = 1 for each frame where it does. The animation is irrelevant - # here, so we can jsut check the final image. + # here, so we can just check the final image. # The first point maybe at 0 depending on when the backend sets up it's # reference time ps = Observable(Point2f[]) - ps2 = Observable(Point2f[]) f, a, p = scatter(ps) - scatter!(ps2, color = :red, marker = '+', markersize = 20) xlims!(a, 0, 61) ylims!(a, -0.1, 1.1) Record(f, 1:60, framerate = 30) do i - push!(ps.val, Point2f(i, f.scene.events.tick[].frame_delta_time > 1e-6)) - push!(ps2.val, Point2f(i, f.scene.events.tick[].event_delta_time > 1e-6)) + push!(ps.val, Point2f(i, f.scene.events.tick[].delta_time > 1e-6)) notify(ps) - notify(ps2) - f.scene.events.tick[] = Makie.Tick(Makie.UnknownTickState, 0.0, 0.0) + f.scene.events.tick[] = Makie.Tick(Makie.UnknownTickState, 0, 0.0, 0.0) end f end \ No newline at end of file From d150377a9b3cccb5cc3fe4c2ccb2d4fa649cbeba Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 20:37:41 +0200 Subject: [PATCH 13/31] update docs --- src/types.jl | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/types.jl b/src/types.jl index fb58fd1834f..ed5f3fb4c41 100644 --- a/src/types.jl +++ b/src/types.jl @@ -16,18 +16,29 @@ end include("interaction/iodevices.jl") +""" + enum TickState + +Identifies the source of a tick: +- `BackendTick`: A tick used for backend purposes which is not present in `event.tick`. +- `UnknownTickState`: A tick from an uncategorized source (e.g. intialization of Events). +- `PausedRenderTick`: A tick from a paused renderloop. +- `SkippedRenderTick`: A tick from a running renderloop where the previous image was reused. +- `RegularRenderTick`: A tick from a running renderloop where a new image was produced. +- `OneTimeRenderTick`: A tick from a call to `colorbuffer`, i.e. an image request from `save` or `record`. +""" @enum TickState begin - BackendTick # for ticks only present in backends, e.g. GLMakie plot insertions - UnknownTickState # everything else - OneTimeRenderTick # render outside of renderloop, e.g. colorbuffer() (save, record) - PausedRenderTick # render loop paused - SkippedRenderTick # render skipped due to frame reuse - RegularRenderTick # normal render + BackendTick + UnknownTickState # GLMakie only allows states > UnknownTickState + PausedRenderTick + SkippedRenderTick + RegularRenderTick + OneTimeRenderTick end struct Tick state::TickState # flag for the type of tick event - count::UInt64 # number of ticks since start + count::Int64 # number of ticks since start time::Float64 # time since scene initialization delta_time::Float64 # time since last tick end @@ -128,7 +139,13 @@ struct Events entered_window::Observable{Bool} """ - TODO + A `tick` is triggered whenever a new frame is requested, i.e. during normal + rendering (even if the renderloop is paused) or when an image is produced + for `save` or `record`. A Tick contains: + - `state` which identifies what caused the tick (see [`TickState`](@ref)) + - `count` which increments with every tick + - `time` which is the total time since the screen has been created + - `delta_time` which is the time since the last frame """ tick::Observable{Tick} end From b384eb8ad5c249cbdaf9562b7fa1fe0de7403c24 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 20:40:22 +0200 Subject: [PATCH 14/31] update changelog --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cf3986ce6c..f63f40b8656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ # Changelog ## [Unreleased] -- CairoMakie: Add argument `pdf_version` to restrict the PDF version when saving a figure as a PDF [#3845](https://github.com/MakieOrg/Makie.jl/pull/3845). + +- CairoMakie: Added argument `pdf_version` to restrict the PDF version when saving a figure as a PDF [#3845](https://github.com/MakieOrg/Makie.jl/pull/3845). +- Added `events.tick` to allow linking actions like animations to the renderloop. [#3948](https://github.com/MakieOrg/Makie.jl/pull/3948) ## [0.21.2] - 2024-05-22 From 3782e387af5a2fff9375ee51b74829ba5db39e2e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 21:33:25 +0200 Subject: [PATCH 15/31] add more tests --- CairoMakie/test/runtests.jl | 17 ++++++++++++++ GLMakie/test/runtests.jl | 41 ++++++++++++++++++++++++++++++++++ WGLMakie/src/display.jl | 2 +- WGLMakie/test/runtests.jl | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index dd0c200495a..dad68916489 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -232,3 +232,20 @@ end @test_throws ArgumentError save(filename, Figure(), pdf_version="foo") end + +@testset "Tick Events" begin + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + + filename = "$(tempname()).pdf" + try + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 1 + @test tick.time > 1e-9 + @test tick.delta_time > 1e-9 + finally + rm(filename) + end +end \ No newline at end of file diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 98e30bc505c..10b98d63764 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -37,3 +37,44 @@ include("unit_tests.jl") GLMakie.closeall() GC.gc(true) # make sure no finalizers act up! end + +@testset "Tick Events" begin + function check_tick(tick, state, count) + @test tick.state == state + @test tick.count == count + @test tick.time > 1e-9 + @test tick.delta_time > 1e-9 + end + + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + + filename = "$(tempname()).png" + try + Makie.save(filename, f) + tick = events(f).tick[] + check_tick(tick, Makie.OneTimeRenderTick, 1) + finally + rm(filename) + end + + f, a, p = scatter(rand(10)); + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) + display(screen, f.scene) + sleep(0.15) + GLMakie.pause_renderloop!(screen) + sleep(0.1) + GLMakie.closeall() + + # Why does it start with a skipped tick? + check_tick(tick_record[1], Makie.SkippedRenderTick, 1) + check_tick(tick_record[2], Makie.RegularRenderTick, 2) + i = 3 + while (tick_record[i].state == Makie.SkippedRenderTick) + check_tick(tick_record[i], Makie.SkippedRenderTick, i) + i += 1 + end + check_tick(tick_record[i], Makie.PausedRenderTick, i) +end \ No newline at end of file diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index ec96ea39a27..551aef2012a 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -280,7 +280,7 @@ function Makie.colorbuffer(screen::Screen) end session = get_screen_session(screen; error="Not able to show scene in a browser") screen.last_time = Makie.next_tick!( - screen.scene.events.tick, Makie.RegularRenderTick, screen.start_time, screen.last_time) + screen.scene.events.tick, Makie.OneTimeRenderTick, screen.start_time, screen.last_time) return session2image(session, screen.scene) end diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index f7e4bd786b6..747e5fc1f9d 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -70,3 +70,47 @@ end # we used Retain for global_obs, so it should stay as long as root session is open @test keys(js_objects) == Set([WGLMakie.TEXTURE_ATLAS.id]) end + +@testset "Tick Events" begin + function check_tick(tick, state, count) + @test tick.state == state + @test tick.count == count + @test tick.time > 1e-9 + @test tick.delta_time > 1e-9 + end + + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + + filename = "$(tempname()).png" + try + save(filename, f) + # WGLMakie produces a running renderloop when calling colorbuffer so + # we have multiple ticks to deal with + idx = findfirst(tick -> tick.state == Makie.OneTimeRenderTick, tick_record) + @test idx !== nothing + check_tick(tick_record[idx], Makie.OneTimeRenderTick, idx) + finally + rm(filename) + end + + # This produces a lot of pre-render ticks claiming to be normal render ticks + f, a, p = scatter(rand(10)); + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + screen = display(f) + sleep(0.1) + close(screen) + + # May have preceeding ticks from previous renderloop + start = 1 + while tick_record[start].count > 1 + start += 1 + end + + for i in start:length(tick_record) + check_tick(tick_record[i], Makie.RegularRenderTick, i-start+1) + end +end \ No newline at end of file From 214e28ec79c78ac0e073fe9a62895db01c7f6a75 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 10 Jun 2024 22:40:47 +0200 Subject: [PATCH 16/31] try more time --- WGLMakie/test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 747e5fc1f9d..2d0c34386bb 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -101,7 +101,7 @@ end tick_record = Makie.Tick[] on(t -> push!(tick_record, t), events(f).tick) screen = display(f) - sleep(0.1) + sleep(0.2) close(screen) # May have preceeding ticks from previous renderloop From 1f45d6626c4541f50205f6f2131d2fe0ccf34dae Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 11 Jun 2024 14:35:57 +0200 Subject: [PATCH 17/31] make record ticks match set framerate --- CairoMakie/src/infrastructure.jl | 3 -- CairoMakie/src/screen.jl | 4 +-- CairoMakie/test/runtests.jl | 26 +++++++++++++--- GLMakie/src/screen.jl | 2 +- GLMakie/test/runtests.jl | 26 ++++++++++++++-- WGLMakie/src/display.jl | 2 -- WGLMakie/test/runtests.jl | 53 +++++++++++++++++++------------- src/display.jl | 1 + src/ffmpeg-util.jl | 7 ++++- 9 files changed, 84 insertions(+), 40 deletions(-) diff --git a/CairoMakie/src/infrastructure.jl b/CairoMakie/src/infrastructure.jl index a99138aba38..46e0e3bcc2d 100644 --- a/CairoMakie/src/infrastructure.jl +++ b/CairoMakie/src/infrastructure.jl @@ -8,9 +8,6 @@ # The main entry point into the drawing pipeline function cairo_draw(screen::Screen, scene::Scene) - screen.last_time = Makie.next_tick!( - scene.events.tick, Makie.OneTimeRenderTick, screen.start_time, screen.last_time) - Cairo.save(screen.context) draw_background(screen, scene) diff --git a/CairoMakie/src/screen.jl b/CairoMakie/src/screen.jl index 22d7dd8fed6..af5b7042a26 100644 --- a/CairoMakie/src/screen.jl +++ b/CairoMakie/src/screen.jl @@ -171,8 +171,6 @@ mutable struct Screen{SurfaceRenderType} <: Makie.MakieScreen antialias::Int # cairo_antialias_t visible::Bool config::ScreenConfig - start_time::UInt64 - last_time::UInt64 end function Base.empty!(screen::Screen) @@ -315,7 +313,7 @@ function Screen(scene::Scene, config::ScreenConfig, surface::Cairo.CairoSurface) restrict_pdf_version!(surface, Int(config.pdf_version)) end - return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config, time_ns(), time_ns()) + return Screen{get_render_type(surface)}(scene, surface, ctx, dsf, aa, config.visible, config) end ######################################## diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index dad68916489..6501e51df60 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -237,14 +237,30 @@ end f, a, p = scatter(rand(10)); @test events(f).tick[] == Makie.Tick() - filename = "$(tempname()).pdf" + filename = "$(tempname()).png" try save(filename, f) - tick = events(f).tick[] + tick = events(f).tick[] @test tick.state == Makie.OneTimeRenderTick - @test tick.count == 1 - @test tick.time > 1e-9 - @test tick.delta_time > 1e-9 + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 + finally + rm(filename) + end + + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) + record(_ -> nothing, f, filename, 1:10, framerate = 30) + dt = 1.0 / 30.0 + for (i, tick) in enumerate(tick_record) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i + @test tick.time ≈ dt * i + @test tick.delta_time ≈ dt + end finally rm(filename) end diff --git a/GLMakie/src/screen.jl b/GLMakie/src/screen.jl index 8a67e555952..f9e0fcf03d6 100644 --- a/GLMakie/src/screen.jl +++ b/GLMakie/src/screen.jl @@ -727,7 +727,7 @@ function Makie.colorbuffer(screen::Screen, format::Makie.ImageStorageFormat = Ma ctex = screen.framebuffer.buffers[:color] # polling may change window size, when its bigger than monitor! # we still need to poll though, to get all the newest events! - pollevents(screen, Makie.OneTimeRenderTick) + pollevents(screen, Makie.BackendTick) # keep current buffer size to allows larger-than-window renders render_frame(screen, resize_buffers=false) # let it render if screen.config.visible diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 10b98d63764..eb7eb091bcc 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -51,13 +51,33 @@ end filename = "$(tempname()).png" try - Makie.save(filename, f) - tick = events(f).tick[] - check_tick(tick, Makie.OneTimeRenderTick, 1) + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 finally rm(filename) end + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) + record(_ -> nothing, f, filename, 1:10, framerate = 30) + dt = 1.0 / 30.0 + for (i, tick) in enumerate(tick_record) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i + @test tick.time ≈ dt * i + @test tick.delta_time ≈ dt + end + finally + rm(filename) + end + + GLMakie.closeall() f, a, p = scatter(rand(10)); tick_record = Makie.Tick[] on(t -> push!(tick_record, t), events(f).tick) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 551aef2012a..c30cf2eb6db 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -279,8 +279,6 @@ function Makie.colorbuffer(screen::Screen) Base.display(screen, screen.scene) end session = get_screen_session(screen; error="Not able to show scene in a browser") - screen.last_time = Makie.next_tick!( - screen.scene.events.tick, Makie.OneTimeRenderTick, screen.start_time, screen.last_time) return session2image(session, screen.scene) end diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index 2d0c34386bb..a27fb1d33a7 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -81,36 +81,45 @@ end f, a, p = scatter(rand(10)); @test events(f).tick[] == Makie.Tick() - tick_record = Makie.Tick[] - on(t -> push!(tick_record, t), events(f).tick) filename = "$(tempname()).png" try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) save(filename, f) - # WGLMakie produces a running renderloop when calling colorbuffer so - # we have multiple ticks to deal with - idx = findfirst(tick -> tick.state == Makie.OneTimeRenderTick, tick_record) - @test idx !== nothing - check_tick(tick_record[idx], Makie.OneTimeRenderTick, idx) + idx = findfirst(tick -> tick.state == Makie.OneTimeRenderTick, tick_record) + tick = tick_record[idx] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 finally + close(f.scene.current_screens[1]) rm(filename) end - # This produces a lot of pre-render ticks claiming to be normal render ticks + f, a, p = scatter(rand(10)); - tick_record = Makie.Tick[] - on(t -> push!(tick_record, t), events(f).tick) - screen = display(f) - sleep(0.2) - close(screen) - - # May have preceeding ticks from previous renderloop - start = 1 - while tick_record[start].count > 1 - start += 1 - end - - for i in start:length(tick_record) - check_tick(tick_record[i], Makie.RegularRenderTick, i-start+1) + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) + record(_ -> nothing, f, filename, 1:10, framerate = 30) + dt = 1/30 + # normal and record ticks both show up and they stumble over each other + # at the start... + previous_count = 0 + previous_time = -1.0 + for (i, tick) in enumerate(filter(tick -> tick.state == Makie.OneTimeRenderTick, tick_record)) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i + @test tick.time ≈ dt * i + @test tick.delta_time ≈ dt + previous_count = tick.count + previous_time = tick.time + end + finally + close(f.scene.current_screens[1]) + rm(filename) end end \ No newline at end of file diff --git a/src/display.jl b/src/display.jl index c7cf1b3f5b7..fd1d2a715e8 100644 --- a/src/display.jl +++ b/src/display.jl @@ -341,6 +341,7 @@ function FileIO.save( config = Dict{Symbol, Any}(screen_config) get!(config, :visible, visible) screen = getscreen(backend, scene, config, io, mime) + events(fig).tick[] = Tick(OneTimeRenderTick, 0, 0.0, 0.0) backend_show(screen, io, mime, scene) end catch e diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index ec280481b85..a0403faf524 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -182,6 +182,8 @@ struct VideoStream io::Base.PipeEndpoint process::Base.Process screen::MakieScreen + tick::Observable{Tick} + frame_counter::RefValue{Int} buffer::Matrix{RGB{N0f8}} path::String options::VideoStreamOptions @@ -229,7 +231,7 @@ function VideoStream(fig::FigureLike; vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loop, loglevel, "pipe:0", true) cmd = to_ffmpeg_cmd(vso, xdim, ydim) process = open(`$(FFMPEG_jll.ffmpeg()) $cmd $path`, "w") - return VideoStream(process.in, process, screen, buffer, abspath(path), vso) + return VideoStream(process.in, process, screen, events(fig).tick, RefValue(0), buffer, abspath(path), vso) end """ @@ -238,6 +240,9 @@ end Adds a video frame to the VideoStream `io`. """ function recordframe!(io::VideoStream) + dt = 1.0 / io.options.framerate + io.frame_counter[] = io.frame_counter[] + 1 + io.tick[] = Tick(OneTimeRenderTick, io.frame_counter[], io.frame_counter[] * dt, dt) glnative = colorbuffer(io.screen, GLNative) # Make no copy if already Matrix{RGB{N0f8}} # There may be a 1px padding for odd dimensions From 9d43f930283fe626160a0450cbc66b163378bec3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 18 Jun 2024 16:41:56 +0200 Subject: [PATCH 18/31] add safeguard --- GLMakie/test/runtests.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index eb7eb091bcc..63737f7e8c8 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -46,6 +46,8 @@ end @test tick.delta_time > 1e-9 end + GLMakie.closeall() + f, a, p = scatter(rand(10)); @test events(f).tick[] == Makie.Tick() @@ -67,6 +69,12 @@ end on(tick -> push!(tick_record, tick), events(f).tick) record(_ -> nothing, f, filename, 1:10, framerate = 30) dt = 1.0 / 30.0 + + if first(tick_record).state != Makie.OneTimeRenderTick + popfirst!(tick_record) + end + @assert length(tick_record) == 10 "tick record too long: $(length(tick_record)) > 10" + for (i, tick) in enumerate(tick_record) @test tick.state == Makie.OneTimeRenderTick @test tick.count == i @@ -78,6 +86,7 @@ end end GLMakie.closeall() + f, a, p = scatter(rand(10)); tick_record = Makie.Tick[] on(t -> push!(tick_record, t), events(f).tick) From f787ef2cb911207f01915463bbf414ddc15ec209 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 18 Jun 2024 16:48:58 +0200 Subject: [PATCH 19/31] try stabilize normals of cat --- ReferenceTests/src/tests/examples3d.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ReferenceTests/src/tests/examples3d.jl b/ReferenceTests/src/tests/examples3d.jl index 5d7a08a5e90..6773b8345e2 100644 --- a/ReferenceTests/src/tests/examples3d.jl +++ b/ReferenceTests/src/tests/examples3d.jl @@ -424,12 +424,13 @@ end @reference_test "Normals of a Cat" begin x = loadasset("cat.obj") - mesh(x, color=:black) + f, a, p = mesh(x, color=:black) pos = map(decompose(Point3f, x), GeometryBasics.normals(x)) do p, n p => p .+ Point(normalize(n) .* 0.05f0) end linesegments!(pos, color=:blue) - current_figure() + Makie.update_state_before_display!(f) + f end @reference_test "Sphere Mesh" begin From efa3a98706d22b56fc1599b0acd06e75bf82bd32 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 18 Jun 2024 17:51:04 +0200 Subject: [PATCH 20/31] try fix tests --- GLMakie/test/runtests.jl | 99 +++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 47 deletions(-) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 63737f7e8c8..48a46512e0e 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -48,62 +48,67 @@ end GLMakie.closeall() - f, a, p = scatter(rand(10)); - @test events(f).tick[] == Makie.Tick() + let + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() - filename = "$(tempname()).png" - try - save(filename, f) - tick = events(f).tick[] - @test tick.state == Makie.OneTimeRenderTick - @test tick.count == 0 - @test tick.time == 0.0 - @test tick.delta_time == 0.0 - finally - rm(filename) - end + filename = "$(tempname()).png" + try + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 + finally + rm(filename) + end - filename = "$(tempname()).mp4" - try - tick_record = Makie.Tick[] - on(tick -> push!(tick_record, tick), events(f).tick) - record(_ -> nothing, f, filename, 1:10, framerate = 30) - dt = 1.0 / 30.0 + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + on(tick -> push!(tick_record, tick), events(f).tick) + record(_ -> nothing, f, filename, 1:10, framerate = 30) + GLMakie.closeall() + dt = 1.0 / 30.0 - if first(tick_record).state != Makie.OneTimeRenderTick - popfirst!(tick_record) - end - @assert length(tick_record) == 10 "tick record too long: $(length(tick_record)) > 10" + if first(tick_record).state != Makie.OneTimeRenderTick + popfirst!(tick_record) + end + @assert length(tick_record) == 10 "tick record too long: $(length(tick_record)) > 10" - for (i, tick) in enumerate(tick_record) - @test tick.state == Makie.OneTimeRenderTick - @test tick.count == i - @test tick.time ≈ dt * i - @test tick.delta_time ≈ dt + for (i, tick) in enumerate(tick_record) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i + @test tick.time ≈ dt * i + @test tick.delta_time ≈ dt + end + finally + rm(filename) end - finally - rm(filename) end GLMakie.closeall() - f, a, p = scatter(rand(10)); - tick_record = Makie.Tick[] - on(t -> push!(tick_record, t), events(f).tick) - screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) - display(screen, f.scene) - sleep(0.15) - GLMakie.pause_renderloop!(screen) - sleep(0.1) - GLMakie.closeall() + let + f, a, p = scatter(rand(10)); + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) + display(screen, f.scene) + sleep(0.15) + GLMakie.pause_renderloop!(screen) + sleep(0.1) + GLMakie.closeall() - # Why does it start with a skipped tick? - check_tick(tick_record[1], Makie.SkippedRenderTick, 1) - check_tick(tick_record[2], Makie.RegularRenderTick, 2) - i = 3 - while (tick_record[i].state == Makie.SkippedRenderTick) - check_tick(tick_record[i], Makie.SkippedRenderTick, i) - i += 1 + # Why does it start with a skipped tick? + check_tick(tick_record[1], Makie.SkippedRenderTick, 1) + check_tick(tick_record[2], Makie.RegularRenderTick, 2) + i = 3 + while (tick_record[i].state == Makie.SkippedRenderTick) + check_tick(tick_record[i], Makie.SkippedRenderTick, i) + i += 1 + end + check_tick(tick_record[i], Makie.PausedRenderTick, i) end - check_tick(tick_record[i], Makie.PausedRenderTick, i) end \ No newline at end of file From 5fc9c730b8c189a902afe87695b3cd28ff171aa6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 15:12:56 +0200 Subject: [PATCH 21/31] try fix tests --- GLMakie/test/runtests.jl | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index 48a46512e0e..c8db4857685 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -46,9 +46,6 @@ end @test tick.delta_time > 1e-9 end - GLMakie.closeall() - - let f, a, p = scatter(rand(10)); @test events(f).tick[] == Makie.Tick() @@ -69,28 +66,23 @@ end tick_record = Makie.Tick[] on(tick -> push!(tick_record, tick), events(f).tick) record(_ -> nothing, f, filename, 1:10, framerate = 30) - GLMakie.closeall() dt = 1.0 / 30.0 - if first(tick_record).state != Makie.OneTimeRenderTick - popfirst!(tick_record) - end - @assert length(tick_record) == 10 "tick record too long: $(length(tick_record)) > 10" - - for (i, tick) in enumerate(tick_record) - @test tick.state == Makie.OneTimeRenderTick + i = 1 + for tick in tick_record + if tick.state == Makie.OneTimeRenderTick @test tick.count == i @test tick.time ≈ dt * i @test tick.delta_time ≈ dt + i += 1 end - finally - rm(filename) end + finally + rm(filename) end GLMakie.closeall() - let f, a, p = scatter(rand(10)); tick_record = Makie.Tick[] on(t -> push!(tick_record, t), events(f).tick) @@ -102,13 +94,24 @@ end GLMakie.closeall() # Why does it start with a skipped tick? - check_tick(tick_record[1], Makie.SkippedRenderTick, 1) - check_tick(tick_record[2], Makie.RegularRenderTick, 2) - i = 3 - while (tick_record[i].state == Makie.SkippedRenderTick) + i = 1 + while tick_record[i].state == Makie.SkippedRenderTick + check_tick(tick_record[1], Makie.SkippedRenderTick, i) + i += 1 + end + + check_tick(tick_record[i], Makie.RegularRenderTick, i) + i += 1 + + while tick_record[i].state == Makie.SkippedRenderTick check_tick(tick_record[i], Makie.SkippedRenderTick, i) i += 1 end + + while (i <= length(tick_record)) && (tick_record[i].state == Makie.PausedRenderTick) check_tick(tick_record[i], Makie.PausedRenderTick, i) + i += 1 end + + @test i == length(tick_record)+1 end \ No newline at end of file From 6adafd5ffe90b54c9b6f587b00d5462e6ce9b532 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 14:11:29 +0200 Subject: [PATCH 22/31] drop docs ref to enum to avoid docs failure --- src/types.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/types.jl b/src/types.jl index ed5f3fb4c41..7b0e2642944 100644 --- a/src/types.jl +++ b/src/types.jl @@ -36,6 +36,15 @@ Identifies the source of a tick: OneTimeRenderTick end +""" + struct TickState + +Contains information for tick events: +- `state::TickState`: identifies what caused the tick (see Makie.TickState) +- `count::Int64`: number of ticks produced since the start of rendering (display or record) +- `time::Float64`: time that has passed since the first tick +- `delta_time`: time that has passed since the last tick +""" struct Tick state::TickState # flag for the type of tick event count::Int64 # number of ticks since start @@ -142,7 +151,7 @@ struct Events A `tick` is triggered whenever a new frame is requested, i.e. during normal rendering (even if the renderloop is paused) or when an image is produced for `save` or `record`. A Tick contains: - - `state` which identifies what caused the tick (see [`TickState`](@ref)) + - `state` which identifies what caused the tick (see Makie.TickState) - `count` which increments with every tick - `time` which is the total time since the screen has been created - `delta_time` which is the time since the last frame From 8bd331b9a94e1cf99ad6dd3651af247ce543ccfc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 15:22:26 +0200 Subject: [PATCH 23/31] add more documentation --- docs/src/explanations/events.md | 35 +++++++++++++++++++++++++++++++++ src/types.jl | 4 ++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/docs/src/explanations/events.md b/docs/src/explanations/events.md index b58da505e39..74eccdd7f8a 100644 --- a/docs/src/explanations/events.md +++ b/docs/src/explanations/events.md @@ -58,6 +58,7 @@ Events from the backend are stored in Observables within the `Events` struct. Yo - `keyboardstate::Observable{Keyboard.Button}`: Contains all currently pressed keys. - `unicode_input::Observable{Char}`: Contains the most recently typed character. - `dropped_files::Observable{Vector{String}}`: Contains a list of filepaths to a collection files dragged into the window. +- `tick::Observable{Makie.Tick}`: Contains the most recent `Makie.Tick`. A `tick` is produced for every frame rendered, i.e. at regular intervals for interactive figures, when a image is saved or when `record()` is used. ## Mouse Interaction @@ -397,3 +398,37 @@ record(scene, "test.mp4"; framerate = fps) do io end end ``` + +## Tick Events + +Tick events are produced by the renderloop in GLMakie and WGLMakie, as well as `Makie.save` and `Makie.record` for all backends. They allow you to synchronize tasks such as animations with rendering. A Tick contains the following information: + +- `state::Makie.TickState`: Describes the situation in which the tick was produced. These include: + - `Makie.UnknownTickState`: A catch-all for uncategorized ticks. Currently only used for initialization of the tick event. + - `Makie.PausedRenderTick`: A tick originating from a paused renderloop in GLMakie. (This refers to the last attempt to draw a frame.) + - `Makie.SkippedRenderTick`: A tick originating from a `render_on_demand = true` renderloop in GLMakie where the last frame has been reused. (This happens when nothing about the displayed frame has changed. For animation you may consider this a regular render tick.) + - `Makie.RegularRenderTick`: A tick from a renderloop where the last frame has been redrawn. + - `Makie.OneTimeRenderTick`: A tick produced before generating an image for `Makie.save` and `Makie.record`. +- `count::Int64`: Number of ticks produced since the first. During `record` this is relative to the first recorded frame. +- `time::Float64`: The time that has passed since the first tick in seconds. During `record` this is relative to the first recorded frame and increments based on the `framerate` set in record. +- `delta_time::Float64`: The time that has passed since the last tick in seconds. During `record` this is `1 / framerate`. + +For an animation you will generally not need to worry about tick state. You can simply update the relevant data as needed. +```julia +on(events(fig).tick) do tick + # For a simulation you may want to use delta times for updates: + position[] = position[] + tick.delta_time * velocity + + # For a solved system you may want to use the total time to compute the current state: + position[] = trajectory(tick.time) + + # For a data record you may want to use count to move to the next data point(s) + position[] = position_record[mod1(tick.count, end)] +end +``` + +For an interactive figure this will produce an animation synchronized with real time. Within `record` the tick times match up the set `framerate` such that the animation in the produced video matches up with real time. + +The only exception here is WGLMakie which currently runs the render loop during recording. Because of that `RegularRenderTick` and `OneTimeRenderTick` will mix during `record`, with the former being based on real time and the latter based on video time. If you do not restrict to `OneTimeRenderTick` here the resulting video will run faster than real time with `tick.delta_time` and may jump with `tick.time` and `tick.count`. + +For reference, a `tick` generally happens after other events have been processed and before the next frame will be drawn. In WGLMakie the order is unpredictable due to asynchronous event handling in javascript and the time it takes to move data between Julia and javascript. \ No newline at end of file diff --git a/src/types.jl b/src/types.jl index 7b0e2642944..ce4b8c6e0a5 100644 --- a/src/types.jl +++ b/src/types.jl @@ -42,8 +42,8 @@ end Contains information for tick events: - `state::TickState`: identifies what caused the tick (see Makie.TickState) - `count::Int64`: number of ticks produced since the start of rendering (display or record) -- `time::Float64`: time that has passed since the first tick -- `delta_time`: time that has passed since the last tick +- `time::Float64`: time that has passed since the first tick in seconds +- `delta_time`: time that has passed since the last tick in seconds """ struct Tick state::TickState # flag for the type of tick event From fefaac7956657abeb6d7f169971febc805a40cc1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 15:53:26 +0200 Subject: [PATCH 24/31] add tick safeguards to record() --- src/recording.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/recording.jl b/src/recording.jl index 7f01de597db..827101c38f0 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -145,13 +145,18 @@ end """ function record(func, figlike::FigureLike, path::AbstractString; kw_args...) format = lstrip(splitext(path)[2], '.') + # safeguard against other tick sources messing with recordings + cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) io = Record(func, figlike; format=format, visible=true, kw_args...) + off(cb) save(path, io) end function record(func, figlike::FigureLike, path::AbstractString, iter; kw_args...) format = lstrip(splitext(path)[2], '.') + cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) io = Record(func, figlike, iter; format=format, kw_args...) + off(cb) save(path, io) end From 649195ce6d3564109be0f74beaf23c52f9e77603 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 16:03:47 +0200 Subject: [PATCH 25/31] improve WGLMakie record suggestion & general formating --- docs/src/explanations/events.md | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/src/explanations/events.md b/docs/src/explanations/events.md index 74eccdd7f8a..f270d1fae2c 100644 --- a/docs/src/explanations/events.md +++ b/docs/src/explanations/events.md @@ -401,7 +401,9 @@ end ## Tick Events -Tick events are produced by the renderloop in GLMakie and WGLMakie, as well as `Makie.save` and `Makie.record` for all backends. They allow you to synchronize tasks such as animations with rendering. A Tick contains the following information: +Tick events are produced by the renderloop in GLMakie and WGLMakie, as well as `Makie.save` and `Makie.record` for all backends. +They allow you to synchronize tasks such as animations with rendering. +A Tick contains the following information: - `state::Makie.TickState`: Describes the situation in which the tick was produced. These include: - `Makie.UnknownTickState`: A catch-all for uncategorized ticks. Currently only used for initialization of the tick event. @@ -413,7 +415,8 @@ Tick events are produced by the renderloop in GLMakie and WGLMakie, as well as ` - `time::Float64`: The time that has passed since the first tick in seconds. During `record` this is relative to the first recorded frame and increments based on the `framerate` set in record. - `delta_time::Float64`: The time that has passed since the last tick in seconds. During `record` this is `1 / framerate`. -For an animation you will generally not need to worry about tick state. You can simply update the relevant data as needed. +For an animation you will generally not need to worry about tick state. +You can simply update the relevant data as needed. ```julia on(events(fig).tick) do tick # For a simulation you may want to use delta times for updates: @@ -427,8 +430,18 @@ on(events(fig).tick) do tick end ``` -For an interactive figure this will produce an animation synchronized with real time. Within `record` the tick times match up the set `framerate` such that the animation in the produced video matches up with real time. +For an interactive figure this will produce an animation synchronized with real time. +Within `record` the tick times match up the set `framerate` such that the animation in the produced video matches up with real time. -The only exception here is WGLMakie which currently runs the render loop during recording. Because of that `RegularRenderTick` and `OneTimeRenderTick` will mix during `record`, with the former being based on real time and the latter based on video time. If you do not restrict to `OneTimeRenderTick` here the resulting video will run faster than real time with `tick.delta_time` and may jump with `tick.time` and `tick.count`. +The only exception here is WGLMakie with the lower level recording functions (anything not wrapped by `record()`). +WGLMakie still runs a regular render loop while recording, which produces `RegularRenderTick` alongside `OneTimeRenderTick`. +Both being present may cause the animation to jump around (if based on time or count) or run faster (based on delta_time). +To avoid this `record()` actively filters out `RegularRenderTick` with +```julia +cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) +``` +but the lower level functions do not. +If you want to use these functions with WGLMakie you will likely need to add that line before you start recording frames and `off(cb)` afterwards. -For reference, a `tick` generally happens after other events have been processed and before the next frame will be drawn. In WGLMakie the order is unpredictable due to asynchronous event handling in javascript and the time it takes to move data between Julia and javascript. \ No newline at end of file +For reference, a `tick` generally happens after other events have been processed and before the next frame will be drawn. +In WGLMakie the order is unpredictable due to asynchronous event handling in javascript and the time it takes to move data between Julia and javascript. \ No newline at end of file From 6a917ddb639b827ad10301790d5b43ae2f220033 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 17:44:25 +0200 Subject: [PATCH 26/31] move tick filtering to VideoStream --- CairoMakie/test/runtests.jl | 19 +++++++-- GLMakie/test/runtests.jl | 82 ++++++++++++++++++++----------------- WGLMakie/test/runtests.jl | 32 +++++++++------ src/ffmpeg-util.jl | 54 ++++++++++++++++++++---- src/recording.jl | 5 --- 5 files changed, 123 insertions(+), 69 deletions(-) diff --git a/CairoMakie/test/runtests.jl b/CairoMakie/test/runtests.jl index 6501e51df60..eed4a503d71 100644 --- a/CairoMakie/test/runtests.jl +++ b/CairoMakie/test/runtests.jl @@ -252,16 +252,27 @@ end filename = "$(tempname()).mp4" try tick_record = Makie.Tick[] - on(tick -> push!(tick_record, tick), events(f).tick) - record(_ -> nothing, f, filename, 1:10, framerate = 30) + record(_ -> push!(tick_record, events(f).tick[]), f, filename, 1:10, framerate = 30) dt = 1.0 / 30.0 + for (i, tick) in enumerate(tick_record) @test tick.state == Makie.OneTimeRenderTick - @test tick.count == i - @test tick.time ≈ dt * i + @test tick.count == i-1 + @test tick.time ≈ dt * (i-1) @test tick.delta_time ≈ dt end finally rm(filename) end + + # test destruction of tick overwrite + f, a, p = scatter(rand(10)); + let + io = VideoStream(f) + @test events(f).tick[] == Makie.Tick(Makie.OneTimeRenderTick, 0, 0.0, 1.0 / io.options.framerate) + nothing + end + tick = Makie.Tick(Makie.UnknownTickState, 1, 1.0, 1.0) + events(f).tick[] = tick + @test events(f).tick[] == tick end \ No newline at end of file diff --git a/GLMakie/test/runtests.jl b/GLMakie/test/runtests.jl index c8db4857685..4b20757772d 100644 --- a/GLMakie/test/runtests.jl +++ b/GLMakie/test/runtests.jl @@ -46,52 +46,58 @@ end @test tick.delta_time > 1e-9 end - f, a, p = scatter(rand(10)); - @test events(f).tick[] == Makie.Tick() + f, a, p = scatter(rand(10)); + @test events(f).tick[] == Makie.Tick() + + filename = "$(tempname()).png" + try + save(filename, f) + tick = events(f).tick[] + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == 0 + @test tick.time == 0.0 + @test tick.delta_time == 0.0 + finally + rm(filename) + end - filename = "$(tempname()).png" - try - save(filename, f) - tick = events(f).tick[] - @test tick.state == Makie.OneTimeRenderTick - @test tick.count == 0 - @test tick.time == 0.0 - @test tick.delta_time == 0.0 - finally - rm(filename) - end + filename = "$(tempname()).mp4" + try + tick_record = Makie.Tick[] + record(_ -> push!(tick_record, events(f).tick[]), f, filename, 1:10, framerate = 30) + dt = 1.0 / 30.0 - filename = "$(tempname()).mp4" - try - tick_record = Makie.Tick[] - on(tick -> push!(tick_record, tick), events(f).tick) - record(_ -> nothing, f, filename, 1:10, framerate = 30) - dt = 1.0 / 30.0 - - i = 1 - for tick in tick_record - if tick.state == Makie.OneTimeRenderTick - @test tick.count == i - @test tick.time ≈ dt * i - @test tick.delta_time ≈ dt - i += 1 - end + for (i, tick) in enumerate(tick_record) + @test tick.state == Makie.OneTimeRenderTick + @test tick.count == i-1 + @test tick.time ≈ dt * (i-1) + @test tick.delta_time ≈ dt end finally rm(filename) end - GLMakie.closeall() + # test destruction of tick overwrite + f, a, p = scatter(rand(10)); + let + io = VideoStream(f) + @test events(f).tick[] == Makie.Tick(Makie.OneTimeRenderTick, 0, 0.0, 1.0 / io.options.framerate) + nothing + end + tick = Makie.Tick(Makie.UnknownTickState, 1, 1.0, 1.0) + events(f).tick[] = tick + @test events(f).tick[] == tick + - f, a, p = scatter(rand(10)); - tick_record = Makie.Tick[] - on(t -> push!(tick_record, t), events(f).tick) - screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) - display(screen, f.scene) - sleep(0.15) - GLMakie.pause_renderloop!(screen) - sleep(0.1) - GLMakie.closeall() + f, a, p = scatter(rand(10)); + tick_record = Makie.Tick[] + on(t -> push!(tick_record, t), events(f).tick) + screen = GLMakie.Screen(render_on_demand = true, framerate = 30.0, pause_rendering = false, visible = false) + display(screen, f.scene) + sleep(0.15) + GLMakie.pause_renderloop!(screen) + sleep(0.1) + GLMakie.closeall() # Why does it start with a skipped tick? i = 1 diff --git a/WGLMakie/test/runtests.jl b/WGLMakie/test/runtests.jl index a27fb1d33a7..f6630c10dcb 100644 --- a/WGLMakie/test/runtests.jl +++ b/WGLMakie/test/runtests.jl @@ -103,23 +103,29 @@ end filename = "$(tempname()).mp4" try tick_record = Makie.Tick[] - on(tick -> push!(tick_record, tick), events(f).tick) - record(_ -> nothing, f, filename, 1:10, framerate = 30) - dt = 1/30 - # normal and record ticks both show up and they stumble over each other - # at the start... - previous_count = 0 - previous_time = -1.0 - for (i, tick) in enumerate(filter(tick -> tick.state == Makie.OneTimeRenderTick, tick_record)) + record(_ -> push!(tick_record, events(f).tick[]), f, filename, 1:10, framerate = 30) + dt = 1.0 / 30.0 + + for (i, tick) in enumerate(tick_record) @test tick.state == Makie.OneTimeRenderTick - @test tick.count == i - @test tick.time ≈ dt * i + @test tick.count == i-1 + @test tick.time ≈ dt * (i-1) @test tick.delta_time ≈ dt - previous_count = tick.count - previous_time = tick.time end finally - close(f.scene.current_screens[1]) rm(filename) end + + # test destruction of tick overwrite + f, a, p = scatter(rand(10)); + let + io = VideoStream(f) + @test events(f).tick[] == Makie.Tick(Makie.OneTimeRenderTick, 0, 0.0, 1.0 / io.options.framerate) + nothing + end + tick = Makie.Tick(Makie.UnknownTickState, 1, 1.0, 1.0) + events(f).tick[] = tick + @test events(f).tick[] == tick + + # TODO: test normal rendering end \ No newline at end of file diff --git a/src/ffmpeg-util.jl b/src/ffmpeg-util.jl index a0403faf524..1343509a1e0 100644 --- a/src/ffmpeg-util.jl +++ b/src/ffmpeg-util.jl @@ -177,13 +177,45 @@ function to_ffmpeg_cmd(vso::VideoStreamOptions, xdim::Integer=0, ydim::Integer=0 return `$(ffmpeg_prefix) $(ffmpeg_options)` end +mutable struct TickController + tick::Observable{Tick} + frame_counter::Int + frame_time::Float64 + filter_ticks::Bool + filter_callback::Observables.ObserverFunction +end + +function TickController(figlike, frametime, filter = true) + tick = events(figlike).tick + cb = if filter + on(tick -> Consume(tick.state != OneTimeRenderTick), tick, priority = typemax(Int)) + else + on(tick -> nothing, tick, priority = typemax(Int)) + end + controller = TickController(tick, 0, frametime, filter, cb) + finalizer(stop!, controller) + next_tick!(controller) + return controller +end + +function next_tick!(controller::TickController) + controller.tick[] = Tick( + OneTimeRenderTick, + controller.frame_counter, + controller.frame_counter * controller.frame_time, + controller.frame_time + ) + controller.frame_counter += 1 + return +end + +stop!(controller::TickController) = off(controller.filter_callback) struct VideoStream io::Base.PipeEndpoint process::Base.Process screen::MakieScreen - tick::Observable{Tick} - frame_counter::RefValue{Int} + tick_controller::TickController buffer::Matrix{RGB{N0f8}} path::String options::VideoStreamOptions @@ -192,7 +224,7 @@ end """ VideoStream(fig::FigureLike; format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loop=nothing, - loglevel="quiet", visible=false, connect=false, backend=current_backend(), + loglevel="quiet", visible=false, connect=false, filter_ticks=true, backend=current_backend(), screen_config...) Returns a `VideoStream` which can pipe new frames into the ffmpeg process with few allocations via [`recordframe!(stream)`](@ref). @@ -210,11 +242,15 @@ $(Base.doc(VideoStreamOptions)) * `visible=false`: make window visible or not * `connect=false`: connect window events or not * `screen_config...`: See `?Backend.Screen` or `Base.doc(Backend.Screen)` for applicable options that can be passed and forwarded to the backend. + +## Other + +* `filter_ticks`: When true, tick events other than `tick.state = Makie.OneTimeRenderTick` are removed until `save()` is called or the VideoStream object gets deleted. """ function VideoStream(fig::FigureLike; format="mp4", framerate=24, compression=nothing, profile=nothing, pixel_format=nothing, loop=nothing, - loglevel="quiet", visible=false, update=true, backend=current_backend(), - screen_config...) + loglevel="quiet", visible=false, update=true, filter_ticks=true, + backend=current_backend(), screen_config...) dir = mktempdir() path = joinpath(dir, "$(gensym(:video)).$(format)") @@ -231,7 +267,8 @@ function VideoStream(fig::FigureLike; vso = VideoStreamOptions(format, framerate, compression, profile, pixel_format, loop, loglevel, "pipe:0", true) cmd = to_ffmpeg_cmd(vso, xdim, ydim) process = open(`$(FFMPEG_jll.ffmpeg()) $cmd $path`, "w") - return VideoStream(process.in, process, screen, events(fig).tick, RefValue(0), buffer, abspath(path), vso) + tick_controller = TickController(fig, 1.0 / vso.framerate, filter_ticks) + return VideoStream(process.in, process, screen, tick_controller, buffer, abspath(path), vso) end """ @@ -240,15 +277,13 @@ end Adds a video frame to the VideoStream `io`. """ function recordframe!(io::VideoStream) - dt = 1.0 / io.options.framerate - io.frame_counter[] = io.frame_counter[] + 1 - io.tick[] = Tick(OneTimeRenderTick, io.frame_counter[], io.frame_counter[] * dt, dt) glnative = colorbuffer(io.screen, GLNative) # Make no copy if already Matrix{RGB{N0f8}} # There may be a 1px padding for odd dimensions xdim, ydim = size(glnative) copy!(view(io.buffer, 1:xdim, 1:ydim), glnative) write(io.io, io.buffer) + next_tick!(io.tick_controller) return end @@ -273,6 +308,7 @@ function save(path::String, io::VideoStream; video_options...) cp(io.path, path; force=true) end rm(io.path) + stop!(io.tick_controller) return path end diff --git a/src/recording.jl b/src/recording.jl index 827101c38f0..7f01de597db 100644 --- a/src/recording.jl +++ b/src/recording.jl @@ -145,18 +145,13 @@ end """ function record(func, figlike::FigureLike, path::AbstractString; kw_args...) format = lstrip(splitext(path)[2], '.') - # safeguard against other tick sources messing with recordings - cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) io = Record(func, figlike; format=format, visible=true, kw_args...) - off(cb) save(path, io) end function record(func, figlike::FigureLike, path::AbstractString, iter; kw_args...) format = lstrip(splitext(path)[2], '.') - cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) io = Record(func, figlike, iter; format=format, kw_args...) - off(cb) save(path, io) end From c582a5098bcfca1d0671aa2c3d02d46360a21121 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 19 Jun 2024 19:11:44 +0200 Subject: [PATCH 27/31] update docs --- docs/src/explanations/events.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/src/explanations/events.md b/docs/src/explanations/events.md index f270d1fae2c..6883ac15b8a 100644 --- a/docs/src/explanations/events.md +++ b/docs/src/explanations/events.md @@ -433,15 +433,11 @@ end For an interactive figure this will produce an animation synchronized with real time. Within `record` the tick times match up the set `framerate` such that the animation in the produced video matches up with real time. -The only exception here is WGLMakie with the lower level recording functions (anything not wrapped by `record()`). -WGLMakie still runs a regular render loop while recording, which produces `RegularRenderTick` alongside `OneTimeRenderTick`. -Both being present may cause the animation to jump around (if based on time or count) or run faster (based on delta_time). -To avoid this `record()` actively filters out `RegularRenderTick` with -```julia -cb = on(tick -> Consume(tick.state != OneTimeRenderTick), events(figlike).tick, priority = typemax(Int)) -``` -but the lower level functions do not. -If you want to use these functions with WGLMakie you will likely need to add that line before you start recording frames and `off(cb)` afterwards. +Note that the underlying `VideoStream` filters tick events other than `state = OneTimeRenderTick`. +This is done to prevent jumping (wrong count, time) or acceleration (extra ticks) of animations in videos due to extra ticks. +(This is specifically an issue with WGLMakie, as it still runs a normal renderloop while recording.) +Ticks will no longer be filtered once the `VideoStream` object is deleted or the video is saved. +The behavior can also be turned off by setting `filter_ticks = false`. For reference, a `tick` generally happens after other events have been processed and before the next frame will be drawn. In WGLMakie the order is unpredictable due to asynchronous event handling in javascript and the time it takes to move data between Julia and javascript. \ No newline at end of file From 278a0ee0bd1b2c75063f8a6dbda24e7aaff7c154 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Jun 2024 14:00:55 +0200 Subject: [PATCH 28/31] move tick generating functor to Makie --- GLMakie/src/events.jl | 22 ++++------------------ src/interaction/events.jl | 24 ++++++++++++++++++++++++ src/types.jl | 8 +------- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 6c77b790c80..2d1576224e6 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -295,26 +295,12 @@ function Makie.disconnect!(window::GLFW.Window, ::typeof(entered_window)) GLFW.SetCursorEnterCallback(window, nothing) end -# Just for finding the relevant listener -mutable struct TickCallback - event::Observable{Makie.Tick} - start_time::UInt64 - last_time::UInt64 - TickCallback(tick::Observable{Makie.Tick}) = new(tick, time_ns(), time_ns()) -end - -function (cb::TickCallback)(x::Makie.TickState) - if x > Makie.UnknownTickState # not backend or Unknown - cb.last_time = Makie.next_tick!(cb.event, x, cb.start_time, cb.last_time) - end - return nothing -end - function Makie.frame_tick(scene::Scene, screen::Screen) # Separating screen ticks from event ticks allows us to sanitize: - # Internal on-tick event updates happen first (mouseposition), no blocking - # listeners, set order - on(TickCallback(scene.events.tick), scene, screen.render_tick, priority = typemin(Int)) + # Internal on-tick event updates happen first (mouseposition), + # consuming in event.tick listeners doesn't affect backend ticks, + # more control/consistent order + on(Makie.TickCallback(scene), scene, screen.render_tick, priority = typemin(Int)) end function Makie.disconnect!(screen::Screen, ::typeof(Makie.frame_tick)) connections = filter(x -> x[2] isa TickCallback, screen.render_tick.listeners) diff --git a/src/interaction/events.jl b/src/interaction/events.jl index c20300cc4cf..20e10ce29a6 100644 --- a/src/interaction/events.jl +++ b/src/interaction/events.jl @@ -73,6 +73,30 @@ Calls `func` if one clicks on `plot`. Implemented by the backend. function onpick end +mutable struct TickCallback + event::Observable{Makie.Tick} + start_time::UInt64 + last_time::UInt64 + TickCallback(tick::Observable{Makie.Tick}) = new(tick, time_ns(), time_ns()) +end +TickCallback(scene::SceneLike) = TickCallback(events(scene).tick) + +function (cb::TickCallback)(x::Makie.TickState) + if x > Makie.UnknownTickState # not backend or Unknown + cb.last_time = Makie.next_tick!(cb.event, x, cb.start_time, cb.last_time) + end + return nothing +end + +function next_tick!(tick::Observable{Tick}, state::TickState, start_time::UInt64, last_time::UInt64) + t = time_ns() + since_start = 1e-9 * (t - start_time) + delta_time = 1e-9 * (t - last_time) + tick[] = Tick(state, tick[].count + 1, since_start, delta_time) + return t +end + + ################################################################################ ### ispressed logic ################################################################################ diff --git a/src/types.jl b/src/types.jl index ce4b8c6e0a5..8194eda65cb 100644 --- a/src/types.jl +++ b/src/types.jl @@ -52,13 +52,7 @@ struct Tick delta_time::Float64 # time since last tick end Tick() = Tick(UnknownTickState, 0, 0.0, 0.0) -function next_tick!(tick::Observable{Tick}, state, start_time, last_time) - t = time_ns() - since_start = 1e-9 * (t - start_time) - delta_time = 1e-9 * (t - last_time) - tick[] = Tick(state, tick[].count + 1, since_start, delta_time) - return t -end + """ This struct provides accessible `Observable`s to monitor the events From 32278083251b36183f27204e66d4049bf078c796 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Jun 2024 14:49:21 +0200 Subject: [PATCH 29/31] run WGLMakie ticks on an independent clock --- WGLMakie/src/display.jl | 10 +++++++--- WGLMakie/src/events.jl | 17 +++++++++++++---- WGLMakie/src/wglmakie.bundled.js | 11 +---------- WGLMakie/src/wglmakie.js | 10 +--------- docs/src/explanations/events.md | 2 +- 5 files changed, 23 insertions(+), 27 deletions(-) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index c30cf2eb6db..76363d72024 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -53,10 +53,14 @@ mutable struct Screen <: Makie.MakieScreen displayed_scenes::Set{String} config::ScreenConfig canvas::Union{Nothing,Bonito.HTMLElement} - start_time::UInt64 - last_time::UInt64 + tick_callback::Union{Nothing,Makie.TickCallback} + tick_clock::Union{Nothing,Timer} function Screen(scene::Union{Nothing,Scene}, config::ScreenConfig) - return new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, time_ns(), time_ns()) + screen = new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, nothing, nothing) + finalizer(screen) do screen + !isnothing(screen.tick_clock) && close(tick_clock) + end + return screen end end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index dccc7f7d819..129f0fc1154 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -108,15 +108,24 @@ function connect_scene_events!(screen::Screen, scene::Scene, comm::Observable) @handle msg.resize begin resize!(scene, tuple(resize...)) end - @handle msg.tick begin - screen.last_time = Makie.next_tick!( - e.tick, Makie.RegularRenderTick, screen.start_time, screen.last_time) - end catch err @warn "Error in window event callback" exception=(err, Base.catch_backtrace()) end return end + # This produces bad timings just like sleep... + screen.tick_callback = Makie.TickCallback(e.tick) + screen.tick_clock = Timer(0.0, interval = 1.0 / 30.0) do timer + if isopen(screen) + screen.tick_callback(Makie.RegularRenderTick) + # @info "tick $(e.tick[].count) $(e.tick[].delta_time)" + else + close(timer) + screen.tick_clock = nothing + end + return + end + return end diff --git a/WGLMakie/src/wglmakie.bundled.js b/WGLMakie/src/wglmakie.bundled.js index 8d28b984295..41acc44390b 100644 --- a/WGLMakie/src/wglmakie.bundled.js +++ b/WGLMakie/src/wglmakie.bundled.js @@ -22757,13 +22757,11 @@ function render_scene(scene, picking = false) { return scene.scene_children.every((x)=>render_scene(x, picking)); } function start_renderloop(three_scene) { - const { fps , renderer } = three_scene.screen; + const { fps } = three_scene.screen; const time_per_frame = 1 / fps * 1000; let last_time_stamp = performance.now(); function renderloop(timestamp) { if (timestamp - last_time_stamp > time_per_frame) { - const canvas = renderer.domElement; - canvas.dispatchEvent(new Event('render')); const all_rendered = render_scene(three_scene); if (!all_rendered) { return; @@ -22947,13 +22945,6 @@ function add_canvas_events(screen, comm, resize_to) { window.addEventListener("resize", (event)=>resize_callback_throttled()); resize_callback_throttled(); } - function tick(event) { - comm.notify({ - tick: true - }); - return false; - } - canvas.addEventListener("render", tick); } function threejs_module(canvas) { let context = canvas.getContext("webgl2", { diff --git a/WGLMakie/src/wglmakie.js b/WGLMakie/src/wglmakie.js index f3aa266b9c2..ec7dd3a24a9 100644 --- a/WGLMakie/src/wglmakie.js +++ b/WGLMakie/src/wglmakie.js @@ -63,14 +63,12 @@ export function render_scene(scene, picking = false) { function start_renderloop(three_scene) { // extract the first scene for screen, which should be shared by all scenes! - const { fps, renderer } = three_scene.screen; + const { fps } = three_scene.screen; const time_per_frame = (1 / fps) * 1000; // default is 30 fps // make sure we immediately render the first frame and dont wait 30ms let last_time_stamp = performance.now(); function renderloop(timestamp) { if (timestamp - last_time_stamp > time_per_frame) { - const canvas = renderer.domElement; - canvas.dispatchEvent( new Event('render') ); const all_rendered = render_scene(three_scene); if (!all_rendered) { // if scenes don't render it means they're not displayed anymore @@ -330,12 +328,6 @@ function add_canvas_events(screen, comm, resize_to) { // Fire the resize event once at the start to auto-size our window resize_callback_throttled(); } - - function tick(event) { - comm.notify({ tick: true, }); - return false; - } - canvas.addEventListener("render", tick); } function threejs_module(canvas) { diff --git a/docs/src/explanations/events.md b/docs/src/explanations/events.md index 6883ac15b8a..3969a19f62e 100644 --- a/docs/src/explanations/events.md +++ b/docs/src/explanations/events.md @@ -440,4 +440,4 @@ Ticks will no longer be filtered once the `VideoStream` object is deleted or the The behavior can also be turned off by setting `filter_ticks = false`. For reference, a `tick` generally happens after other events have been processed and before the next frame will be drawn. -In WGLMakie the order is unpredictable due to asynchronous event handling in javascript and the time it takes to move data between Julia and javascript. \ No newline at end of file +The exception is WGLMakie which runs an independent timer to avoid excessive message passing between Javascript and Julia. \ No newline at end of file From e3f4bf29d6c28c9e95f929c5a85c7d800d91382b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 21 Jun 2024 16:17:21 +0200 Subject: [PATCH 30/31] fix missing namespace --- GLMakie/src/events.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GLMakie/src/events.jl b/GLMakie/src/events.jl index 2d1576224e6..95e49452a74 100644 --- a/GLMakie/src/events.jl +++ b/GLMakie/src/events.jl @@ -303,6 +303,6 @@ function Makie.frame_tick(scene::Scene, screen::Screen) on(Makie.TickCallback(scene), scene, screen.render_tick, priority = typemin(Int)) end function Makie.disconnect!(screen::Screen, ::typeof(Makie.frame_tick)) - connections = filter(x -> x[2] isa TickCallback, screen.render_tick.listeners) + connections = filter(x -> x[2] isa Makie.TickCallback, screen.render_tick.listeners) foreach(x -> off(screen.render_tick, x[2]), connections) end \ No newline at end of file From 5ef7e3380ffad091dd22dfc08bbdbb2fdde25fbe Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 25 Jul 2024 20:59:42 +0200 Subject: [PATCH 31/31] use BudgetedTimer for WGLMakie --- WGLMakie/src/display.jl | 10 ++++++---- WGLMakie/src/events.jl | 10 ++++------ src/utilities/timing.jl | 13 +++++++++++-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/WGLMakie/src/display.jl b/WGLMakie/src/display.jl index 76363d72024..2493966d030 100644 --- a/WGLMakie/src/display.jl +++ b/WGLMakie/src/display.jl @@ -53,13 +53,15 @@ mutable struct Screen <: Makie.MakieScreen displayed_scenes::Set{String} config::ScreenConfig canvas::Union{Nothing,Bonito.HTMLElement} - tick_callback::Union{Nothing,Makie.TickCallback} - tick_clock::Union{Nothing,Timer} + tick_clock::Makie.BudgetedTimer function Screen(scene::Union{Nothing,Scene}, config::ScreenConfig) - screen = new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, nothing, nothing) + timer = Makie.BudgetedTimer(1.0 / 30.0) + screen = new(Channel{Bool}(1), nothing, scene, Set{String}(), config, nothing, timer) + finalizer(screen) do screen - !isnothing(screen.tick_clock) && close(tick_clock) + close(screen.tick_clock) end + return screen end end diff --git a/WGLMakie/src/events.jl b/WGLMakie/src/events.jl index 129f0fc1154..d726c925ac9 100644 --- a/WGLMakie/src/events.jl +++ b/WGLMakie/src/events.jl @@ -114,15 +114,13 @@ function connect_scene_events!(screen::Screen, scene::Scene, comm::Observable) return end - # This produces bad timings just like sleep... - screen.tick_callback = Makie.TickCallback(e.tick) - screen.tick_clock = Timer(0.0, interval = 1.0 / 30.0) do timer + tick_callback = Makie.TickCallback(e.tick) + Makie.start!(screen.tick_clock) do timer if isopen(screen) - screen.tick_callback(Makie.RegularRenderTick) + tick_callback(Makie.RegularRenderTick) # @info "tick $(e.tick[].count) $(e.tick[].delta_time)" else - close(timer) - screen.tick_clock = nothing + stop!(timer) end return end diff --git a/src/utilities/timing.jl b/src/utilities/timing.jl index 4684ed381c1..df06462f44c 100644 --- a/src/utilities/timing.jl +++ b/src/utilities/timing.jl @@ -38,7 +38,7 @@ function BudgetedTimer(callback, delta_time::AbstractFloat, start = true; min_sl timer = BudgetedTimer(callback, delta_time, true, nothing, min_sleep) if start timer.task = @async while timer.running - timer.callback() + timer.callback(timer) sleep(timer) end end @@ -50,7 +50,7 @@ function start!(timer::BudgetedTimer) timer.last_time = time_ns() timer.running = true timer.task = @async while timer.running - timer.callback() + timer.callback(timer) sleep(timer) end return @@ -61,11 +61,20 @@ function start!(callback, timer::BudgetedTimer) return start!(timer) end +function set_callback!(callback, timer::BudgetedTimer) + timer.callback = callback +end + function stop!(timer::BudgetedTimer) timer.running = false return end +function Base.close(timer::BudgetedTimer) + timer.callback = identity + stop!(timer) +end + function reset!(timer::BudgetedTimer, delta_time = timer.target_delta_time) timer.target_delta_time = delta_time timer.budget = 0.0