chore(deps): integrate cef2gtk presentation synchronization
- commit
- 8e234aa0672cf5b4d0f5f6cda42a89e26d8e3459
- author
- brice · · 2026-07-14 08:45 UTC
- committer
- brice · · 2026-07-14 09:35 UTC
- parent
- 0d8c582
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
7 files · +134 -19
M go.mod +1 -1
| @@ -7,7 +7,7 @@ require ( | |||
| 7 | 7 | github.com/andybalholm/brotli v1.2.2 | |
| 8 | 8 | github.com/bnema/purego v0.11.0-bnema.4 | |
| 9 | 9 | github.com/bnema/purego-cef v0.13.3 | |
| 10 | - | github.com/bnema/purego-cef2gtk v0.8.3 | |
| 10 | + | github.com/bnema/purego-cef2gtk v0.8.4-0.20260714083412-f217ece342de | |
| 11 | 11 | github.com/bnema/purego-pipewire v0.1.4 | |
| 12 | 12 | github.com/bnema/purego-sqlite v0.1.3 | |
| 13 | 13 | github.com/bnema/puregotk v0.7.0 | |
| @@ -7,7 +7,7 @@ require ( | |||||
| 7 | github.com/andybalholm/brotli v1.2.2 | 7 | github.com/andybalholm/brotli v1.2.2 | ||
| 8 | github.com/bnema/purego v0.11.0-bnema.4 | 8 | github.com/bnema/purego v0.11.0-bnema.4 | ||
| 9 | github.com/bnema/purego-cef v0.13.3 | 9 | github.com/bnema/purego-cef v0.13.3 | ||
| 10 | - | github.com/bnema/purego-cef2gtk v0.8.3 | 10 | + | github.com/bnema/purego-cef2gtk v0.8.4-0.20260714083412-f217ece342de |
| 11 | github.com/bnema/purego-pipewire v0.1.4 | 11 | github.com/bnema/purego-pipewire v0.1.4 | ||
| 12 | github.com/bnema/purego-sqlite v0.1.3 | 12 | github.com/bnema/purego-sqlite v0.1.3 | ||
| 13 | github.com/bnema/puregotk v0.7.0 | 13 | github.com/bnema/puregotk v0.7.0 | ||
M go.sum +2 -0 generated file: collapsed
M internal/logging/startup_trace.go +5 -1
| @@ -151,9 +151,13 @@ func (st *StartupTrace) mark(name string) bool { | |||
| 151 | 151 | ||
| 152 | 152 | now := st.now() | |
| 153 | 153 | milestone := Milestone{Name: name, Elapsed: now.Sub(st.t0)} | |
| 154 | + | // Published fields are integer milliseconds. Compute delta on that same | |
| 155 | + | // scale so delta_ms always exactly reconstructs t_ms in collected evidence. | |
| 156 | + | previousMilliseconds := int64(0) | |
| 154 | 157 | if len(st.milestones) > 0 { | |
| 155 | - | milestone.Delta = milestone.Elapsed - st.milestones[len(st.milestones)-1].Elapsed | |
| 158 | + | previousMilliseconds = st.milestones[len(st.milestones)-1].Elapsed.Milliseconds() | |
| 156 | 159 | } | |
| 160 | + | milestone.Delta = time.Duration(milestone.Elapsed.Milliseconds()-previousMilliseconds) * time.Millisecond | |
| 157 | 161 | st.milestones = append(st.milestones, milestone) | |
| 158 | 162 | if st.logger == nil { | |
| 159 | 163 | st.buffered = append(st.buffered, milestone) | |
| @@ -151,9 +151,13 @@ func (st *StartupTrace) mark(name string) bool { | |||||
| 151 | 151 | ||||
| 152 | now := st.now() | 152 | now := st.now() | ||
| 153 | milestone := Milestone{Name: name, Elapsed: now.Sub(st.t0)} | 153 | milestone := Milestone{Name: name, Elapsed: now.Sub(st.t0)} | ||
| 154 | + | // Published fields are integer milliseconds. Compute delta on that same | |||
| 155 | + | // scale so delta_ms always exactly reconstructs t_ms in collected evidence. | |||
| 156 | + | previousMilliseconds := int64(0) | |||
| 154 | if len(st.milestones) > 0 { | 157 | if len(st.milestones) > 0 { | ||
| 155 | - | milestone.Delta = milestone.Elapsed - st.milestones[len(st.milestones)-1].Elapsed | 158 | + | previousMilliseconds = st.milestones[len(st.milestones)-1].Elapsed.Milliseconds() |
| 156 | } | 159 | } | ||
| 160 | + | milestone.Delta = time.Duration(milestone.Elapsed.Milliseconds()-previousMilliseconds) * time.Millisecond | |||
| 157 | st.milestones = append(st.milestones, milestone) | 161 | st.milestones = append(st.milestones, milestone) | ||
| 158 | if st.logger == nil { | 162 | if st.logger == nil { | ||
| 159 | st.buffered = append(st.buffered, milestone) | 163 | st.buffered = append(st.buffered, milestone) | ||
M internal/logging/startup_trace_test.go +15 -0
| @@ -36,6 +36,21 @@ func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) { | |||
| 36 | 36 | require.False(t, trace.Mark("first_gtk_presentation")) | |
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | + | func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) { | |
| 40 | + | now := time.Unix(100, 0) | |
| 41 | + | trace := newStartupTrace(func() time.Time { return now }) | |
| 42 | + | ||
| 43 | + | now = now.Add(1500 * time.Microsecond) | |
| 44 | + | require.True(t, trace.Mark("process_entry")) | |
| 45 | + | now = now.Add(900 * time.Microsecond) | |
| 46 | + | require.True(t, trace.Mark("config_complete")) | |
| 47 | + | ||
| 48 | + | require.Equal(t, int64(1), trace.milestones[0].Elapsed.Milliseconds()) | |
| 49 | + | require.Equal(t, int64(1), trace.milestones[0].Delta.Milliseconds()) | |
| 50 | + | require.Equal(t, int64(2), trace.milestones[1].Elapsed.Milliseconds()) | |
| 51 | + | require.Equal(t, int64(1), trace.milestones[1].Delta.Milliseconds()) | |
| 52 | + | } | |
| 53 | + | ||
| 39 | 54 | func TestStartupTraceFinishCannotFabricateFirstGTKPresentation(t *testing.T) { | |
| 40 | 55 | now := time.Unix(100, 0) | |
| 41 | 56 | trace := newStartupTrace(func() time.Time { return now }) | |
| @@ -36,6 +36,21 @@ func TestStartupTraceAcceptsOnlyOrderedOneShotMilestones(t *testing.T) { | |||||
| 36 | require.False(t, trace.Mark("first_gtk_presentation")) | 36 | require.False(t, trace.Mark("first_gtk_presentation")) | ||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | + | func TestStartupTraceQuantizesDeltaToPublishedMilliseconds(t *testing.T) { | |||
| 40 | + | now := time.Unix(100, 0) | |||
| 41 | + | trace := newStartupTrace(func() time.Time { return now }) | |||
| 42 | + | ||||
| 43 | + | now = now.Add(1500 * time.Microsecond) | |||
| 44 | + | require.True(t, trace.Mark("process_entry")) | |||
| 45 | + | now = now.Add(900 * time.Microsecond) | |||
| 46 | + | require.True(t, trace.Mark("config_complete")) | |||
| 47 | + | ||||
| 48 | + | require.Equal(t, int64(1), trace.milestones[0].Elapsed.Milliseconds()) | |||
| 49 | + | require.Equal(t, int64(1), trace.milestones[0].Delta.Milliseconds()) | |||
| 50 | + | require.Equal(t, int64(2), trace.milestones[1].Elapsed.Milliseconds()) | |||
| 51 | + | require.Equal(t, int64(1), trace.milestones[1].Delta.Milliseconds()) | |||
| 52 | + | } | |||
| 53 | + | ||||
| 39 | func TestStartupTraceFinishCannotFabricateFirstGTKPresentation(t *testing.T) { | 54 | func TestStartupTraceFinishCannotFabricateFirstGTKPresentation(t *testing.T) { | ||
| 40 | now := time.Unix(100, 0) | 55 | now := time.Unix(100, 0) | ||
| 41 | trace := newStartupTrace(func() time.Time { return now }) | 56 | trace := newStartupTrace(func() time.Time { return now }) | ||
M vendor/github.com/bnema/purego-cef2gtk/handlers.go +85 -14
| @@ -2,6 +2,7 @@ package cef2gtk | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "errors" | |
| 5 | + | "sync" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/purego-cef/cef" | |
| @@ -17,6 +18,48 @@ type asyncRenderQueue interface { | |||
| 17 | 18 | ImportAndQueueAsync(*cef.AcceleratedPaintInfo, func(error)) error | |
| 18 | 19 | } | |
| 19 | 20 | ||
| 21 | + | // deferredAcceleratedError prevents an async renderer that reports an error | |
| 22 | + | // synchronously from invoking a user hook while OnAcceleratedPaint holds the | |
| 23 | + | // lifecycle read lock. Errors reported after release are already outside it. | |
| 24 | + | type deferredAcceleratedError struct { | |
| 25 | + | h *renderHandler | |
| 26 | + | mu sync.Mutex | |
| 27 | + | released bool | |
| 28 | + | pending []error | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func (d *deferredAcceleratedError) report(err error) { | |
| 32 | + | if d == nil || err == nil { | |
| 33 | + | return | |
| 34 | + | } | |
| 35 | + | d.mu.Lock() | |
| 36 | + | if !d.released { | |
| 37 | + | d.pending = append(d.pending, err) | |
| 38 | + | d.mu.Unlock() | |
| 39 | + | return | |
| 40 | + | } | |
| 41 | + | d.mu.Unlock() | |
| 42 | + | d.h.handleAcceleratedError(err) | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | func (d *deferredAcceleratedError) release() { | |
| 46 | + | if d == nil { | |
| 47 | + | return | |
| 48 | + | } | |
| 49 | + | d.mu.Lock() | |
| 50 | + | if d.released { | |
| 51 | + | d.mu.Unlock() | |
| 52 | + | return | |
| 53 | + | } | |
| 54 | + | d.released = true | |
| 55 | + | pending := d.pending | |
| 56 | + | d.pending = nil | |
| 57 | + | d.mu.Unlock() | |
| 58 | + | for _, err := range pending { | |
| 59 | + | d.h.handleAcceleratedError(err) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 20 | 63 | type renderHandler struct { | |
| 21 | 64 | view *View | |
| 22 | 65 | renderer renderQueue | |
| @@ -31,7 +74,13 @@ func (v *View) RenderHandler(hooks Hooks) cef.RenderHandler { | |||
| 31 | 74 | if v == nil { | |
| 32 | 75 | return &renderHandler{staticHooks: hooks} | |
| 33 | 76 | } | |
| 34 | - | v.hooks = hooks | |
| 77 | + | ||
| 78 | + | // Serialize handler replacement with Destroy, which clears both handler and | |
| 79 | + | // renderer. Hook readers use firstPresentationMu and always copy before | |
| 80 | + | // invoking user code. | |
| 81 | + | v.renderLifecycleMu.Lock() | |
| 82 | + | defer v.renderLifecycleMu.Unlock() | |
| 83 | + | v.setHooks(hooks) | |
| 35 | 84 | v.configureFirstPresentationHooks() | |
| 36 | 85 | if v.handler != nil { | |
| 37 | 86 | return v.handler | |
| @@ -128,42 +177,64 @@ func (h *renderHandler) OnAcceleratedPaint(_ cef.Browser, _ cef.PaintElementType | |||
| 128 | 177 | if first { | |
| 129 | 178 | invokeFirstAcceleratedPaintHooks(hooks, unsupported) | |
| 130 | 179 | } | |
| 180 | + | } | |
| 131 | 181 | ||
| 132 | - | // Serialize accelerated-paint imports and render queueing with Destroy's | |
| 133 | - | // renderer teardown. The hook above may have destroyed the view. | |
| 182 | + | errorDelivery := &deferredAcceleratedError{h: h} | |
| 183 | + | queued := false | |
| 184 | + | if h.view != nil { | |
| 185 | + | // Keep imports and queueing serialized with renderer teardown. An async | |
| 186 | + | // renderer may call its error function synchronously, so defer its hook | |
| 187 | + | // delivery until after this lock has been released. | |
| 134 | 188 | h.view.renderLifecycleMu.RLock() | |
| 135 | - | defer h.view.renderLifecycleMu.RUnlock() | |
| 136 | 189 | if h.view.destroyed.Load() { | |
| 190 | + | h.view.renderLifecycleMu.RUnlock() | |
| 137 | 191 | if h.diag != nil { | |
| 138 | 192 | h.diag.RecordStaleAcceleratedPaint() | |
| 139 | 193 | } | |
| 140 | 194 | return | |
| 141 | 195 | } | |
| 196 | + | queued = h.importAndQueueAcceleratedFrame(info, errorDelivery.report) | |
| 197 | + | h.view.renderLifecycleMu.RUnlock() | |
| 198 | + | } else { | |
| 199 | + | errorDelivery.release() | |
| 200 | + | queued = h.importAndQueueAcceleratedFrame(info, errorDelivery.report) | |
| 201 | + | } | |
| 202 | + | ||
| 203 | + | // No user callback (including ProfileOptions.OnSnapshot) may run while the | |
| 204 | + | // lifecycle lock above protects renderer teardown. | |
| 205 | + | errorDelivery.release() | |
| 206 | + | if queued && h.view != nil { | |
| 207 | + | h.view.emitProfileIfDue(time.Now()) | |
| 142 | 208 | } | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // importAndQueueAcceleratedFrame performs renderer work while its caller holds | |
| 212 | + | // the lifecycle lock, if any. onError must not invoke user code synchronously. | |
| 213 | + | func (h *renderHandler) importAndQueueAcceleratedFrame(info *cef.AcceleratedPaintInfo, onError func(error)) bool { | |
| 143 | 214 | if h.renderer == nil { | |
| 144 | - | h.handleAcceleratedError(gtkgl.ErrNilAcceleratedRenderer) | |
| 145 | - | return | |
| 215 | + | onError(gtkgl.ErrNilAcceleratedRenderer) | |
| 216 | + | return false | |
| 146 | 217 | } | |
| 147 | 218 | if async, ok := h.renderer.(asyncRenderQueue); ok { | |
| 148 | - | if err := async.ImportAndQueueAsync(info, h.handleAcceleratedError); err != nil { | |
| 149 | - | h.handleAcceleratedError(err) | |
| 150 | - | return | |
| 219 | + | if err := async.ImportAndQueueAsync(info, onError); err != nil { | |
| 220 | + | onError(err) | |
| 221 | + | return false | |
| 151 | 222 | } | |
| 152 | 223 | h.recordAcceleratedFrameQueued() | |
| 153 | - | return | |
| 224 | + | return true | |
| 154 | 225 | } | |
| 155 | 226 | if _, err := h.renderer.ImportAndQueueOnGTKThread(info); err != nil { | |
| 156 | - | h.handleAcceleratedError(err) | |
| 157 | - | return | |
| 227 | + | onError(err) | |
| 228 | + | return false | |
| 158 | 229 | } | |
| 159 | 230 | h.recordAcceleratedFrameQueued() | |
| 231 | + | return true | |
| 160 | 232 | } | |
| 161 | 233 | ||
| 162 | 234 | func (h *renderHandler) recordAcceleratedFrameQueued() { | |
| 163 | 235 | h.renderer.QueueRender() | |
| 164 | 236 | if h.view != nil { | |
| 165 | 237 | h.view.recordProfileFrameQueued() | |
| 166 | - | h.view.emitProfileIfDue(time.Now()) | |
| 167 | 238 | } | |
| 168 | 239 | } | |
| 169 | 240 | func (h *renderHandler) handleAcceleratedError(err error) { | |
| @@ -188,7 +259,7 @@ func isTransientGLAreaLifecycleError(err error) bool { | |||
| 188 | 259 | } | |
| 189 | 260 | func (h *renderHandler) hooks() Hooks { | |
| 190 | 261 | if h != nil && h.view != nil { | |
| 191 | - | return h.view.hooks | |
| 262 | + | return h.view.snapshotHooks() | |
| 192 | 263 | } | |
| 193 | 264 | if h != nil { | |
| 194 | 265 | return h.staticHooks | |
| @@ -2,6 +2,7 @@ package cef2gtk | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "errors" | 4 | "errors" | ||
| 5 | + | "sync" | |||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/purego-cef/cef" | 8 | "github.com/bnema/purego-cef/cef" | ||
| @@ -17,6 +18,48 @@ type asyncRenderQueue interface { | |||||
| 17 | ImportAndQueueAsync(*cef.AcceleratedPaintInfo, func(error)) error | 18 | ImportAndQueueAsync(*cef.AcceleratedPaintInfo, func(error)) error | ||
| 18 | } | 19 | } | ||
| 19 | 20 | ||||
| 21 | + | // deferredAcceleratedError prevents an async renderer that reports an error | |||
| 22 | + | // synchronously from invoking a user hook while OnAcceleratedPaint holds the | |||
| 23 | + | // lifecycle read lock. Errors reported after release are already outside it. | |||
| 24 | + | type deferredAcceleratedError struct { | |||
| 25 | + | h *renderHandler | |||
| 26 | + | mu sync.Mutex | |||
| 27 | + | released bool | |||
| 28 | + | pending []error | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func (d *deferredAcceleratedError) report(err error) { | |||
| 32 | + | if d == nil || err == nil { | |||
| 33 | + | return | |||
| 34 | + | } | |||
| 35 | + | d.mu.Lock() | |||
| 36 | + | if !d.released { | |||
| 37 | + | d.pending = append(d.pending, err) | |||
| 38 | + | d.mu.Unlock() | |||
| 39 | + | return | |||
| 40 | + | } | |||
| 41 | + | d.mu.Unlock() | |||
| 42 | + | d.h.handleAcceleratedError(err) | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | func (d *deferredAcceleratedError) release() { | |||
| 46 | + | if d == nil { | |||
| 47 | + | return | |||
| 48 | + | } | |||
| 49 | + | d.mu.Lock() | |||
| 50 | + | if d.released { | |||
| 51 | + | d.mu.Unlock() | |||
| 52 | + | return | |||
| 53 | + | } | |||
| 54 | + | d.released = true | |||
| 55 | + | pending := d.pending | |||
| 56 | + | d.pending = nil | |||
| 57 | + | d.mu.Unlock() | |||
| 58 | + | for _, err := range pending { | |||
| 59 | + | d.h.handleAcceleratedError(err) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | ||||
| 20 | type renderHandler struct { | 63 | type renderHandler struct { | ||
| 21 | view *View | 64 | view *View | ||
| 22 | renderer renderQueue | 65 | renderer renderQueue | ||
| @@ -31,7 +74,13 @@ func (v *View) RenderHandler(hooks Hooks) cef.RenderHandler { | |||||
| 31 | if v == nil { | 74 | if v == nil { | ||
| 32 | return &renderHandler{staticHooks: hooks} | 75 | return &renderHandler{staticHooks: hooks} | ||
| 33 | } | 76 | } | ||
| 34 | - | v.hooks = hooks | 77 | + | |
| 78 | + | // Serialize handler replacement with Destroy, which clears both handler and | |||
| 79 | + | // renderer. Hook readers use firstPresentationMu and always copy before | |||
| 80 | + | // invoking user code. | |||
| 81 | + | v.renderLifecycleMu.Lock() | |||
| 82 | + | defer v.renderLifecycleMu.Unlock() | |||
| 83 | + | v.setHooks(hooks) | |||
| 35 | v.configureFirstPresentationHooks() | 84 | v.configureFirstPresentationHooks() | ||
| 36 | if v.handler != nil { | 85 | if v.handler != nil { | ||
| 37 | return v.handler | 86 | return v.handler | ||
| @@ -128,42 +177,64 @@ func (h *renderHandler) OnAcceleratedPaint(_ cef.Browser, _ cef.PaintElementType | |||||
| 128 | if first { | 177 | if first { | ||
| 129 | invokeFirstAcceleratedPaintHooks(hooks, unsupported) | 178 | invokeFirstAcceleratedPaintHooks(hooks, unsupported) | ||
| 130 | } | 179 | } | ||
| 180 | + | } | |||
| 131 | 181 | ||||
| 132 | - | // Serialize accelerated-paint imports and render queueing with Destroy's | 182 | + | errorDelivery := &deferredAcceleratedError{h: h} |
| 133 | - | // renderer teardown. The hook above may have destroyed the view. | 183 | + | queued := false |
| 184 | + | if h.view != nil { | |||
| 185 | + | // Keep imports and queueing serialized with renderer teardown. An async | |||
| 186 | + | // renderer may call its error function synchronously, so defer its hook | |||
| 187 | + | // delivery until after this lock has been released. | |||
| 134 | h.view.renderLifecycleMu.RLock() | 188 | h.view.renderLifecycleMu.RLock() | ||
| 135 | - | defer h.view.renderLifecycleMu.RUnlock() | |||
| 136 | if h.view.destroyed.Load() { | 189 | if h.view.destroyed.Load() { | ||
| 190 | + | h.view.renderLifecycleMu.RUnlock() | |||
| 137 | if h.diag != nil { | 191 | if h.diag != nil { | ||
| 138 | h.diag.RecordStaleAcceleratedPaint() | 192 | h.diag.RecordStaleAcceleratedPaint() | ||
| 139 | } | 193 | } | ||
| 140 | return | 194 | return | ||
| 141 | } | 195 | } | ||
| 196 | + | queued = h.importAndQueueAcceleratedFrame(info, errorDelivery.report) | |||
| 197 | + | h.view.renderLifecycleMu.RUnlock() | |||
| 198 | + | } else { | |||
| 199 | + | errorDelivery.release() | |||
| 200 | + | queued = h.importAndQueueAcceleratedFrame(info, errorDelivery.report) | |||
| 201 | + | } | |||
| 202 | + | ||||
| 203 | + | // No user callback (including ProfileOptions.OnSnapshot) may run while the | |||
| 204 | + | // lifecycle lock above protects renderer teardown. | |||
| 205 | + | errorDelivery.release() | |||
| 206 | + | if queued && h.view != nil { | |||
| 207 | + | h.view.emitProfileIfDue(time.Now()) | |||
| 142 | } | 208 | } | ||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // importAndQueueAcceleratedFrame performs renderer work while its caller holds | |||
| 212 | + | // the lifecycle lock, if any. onError must not invoke user code synchronously. | |||
| 213 | + | func (h *renderHandler) importAndQueueAcceleratedFrame(info *cef.AcceleratedPaintInfo, onError func(error)) bool { | |||
| 143 | if h.renderer == nil { | 214 | if h.renderer == nil { | ||
| 144 | - | h.handleAcceleratedError(gtkgl.ErrNilAcceleratedRenderer) | 215 | + | onError(gtkgl.ErrNilAcceleratedRenderer) |
| 145 | - | return | 216 | + | return false |
| 146 | } | 217 | } | ||
| 147 | if async, ok := h.renderer.(asyncRenderQueue); ok { | 218 | if async, ok := h.renderer.(asyncRenderQueue); ok { | ||
| 148 | - | if err := async.ImportAndQueueAsync(info, h.handleAcceleratedError); err != nil { | 219 | + | if err := async.ImportAndQueueAsync(info, onError); err != nil { |
| 149 | - | h.handleAcceleratedError(err) | 220 | + | onError(err) |
| 150 | - | return | 221 | + | return false |
| 151 | } | 222 | } | ||
| 152 | h.recordAcceleratedFrameQueued() | 223 | h.recordAcceleratedFrameQueued() | ||
| 153 | - | return | 224 | + | return true |
| 154 | } | 225 | } | ||
| 155 | if _, err := h.renderer.ImportAndQueueOnGTKThread(info); err != nil { | 226 | if _, err := h.renderer.ImportAndQueueOnGTKThread(info); err != nil { | ||
| 156 | - | h.handleAcceleratedError(err) | 227 | + | onError(err) |
| 157 | - | return | 228 | + | return false |
| 158 | } | 229 | } | ||
| 159 | h.recordAcceleratedFrameQueued() | 230 | h.recordAcceleratedFrameQueued() | ||
| 231 | + | return true | |||
| 160 | } | 232 | } | ||
| 161 | 233 | ||||
| 162 | func (h *renderHandler) recordAcceleratedFrameQueued() { | 234 | func (h *renderHandler) recordAcceleratedFrameQueued() { | ||
| 163 | h.renderer.QueueRender() | 235 | h.renderer.QueueRender() | ||
| 164 | if h.view != nil { | 236 | if h.view != nil { | ||
| 165 | h.view.recordProfileFrameQueued() | 237 | h.view.recordProfileFrameQueued() | ||
| 166 | - | h.view.emitProfileIfDue(time.Now()) | |||
| 167 | } | 238 | } | ||
| 168 | } | 239 | } | ||
| 169 | func (h *renderHandler) handleAcceleratedError(err error) { | 240 | func (h *renderHandler) handleAcceleratedError(err error) { | ||
| @@ -188,7 +259,7 @@ func isTransientGLAreaLifecycleError(err error) bool { | |||||
| 188 | } | 259 | } | ||
| 189 | func (h *renderHandler) hooks() Hooks { | 260 | func (h *renderHandler) hooks() Hooks { | ||
| 190 | if h != nil && h.view != nil { | 261 | if h != nil && h.view != nil { | ||
| 191 | - | return h.view.hooks | 262 | + | return h.view.snapshotHooks() |
| 192 | } | 263 | } | ||
| 193 | if h != nil { | 264 | if h != nil { | ||
| 194 | return h.staticHooks | 265 | return h.staticHooks | ||