package world import ( "testing" "github.com/stretchr/testify/require" ) func TestRender_ShiftOnlyOverBudget_DefersDirtyAndCatchesUpOnStop(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) _, err := w.AddPoint(5, 5) require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } params := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 10, MarginXPx: 4, // threshold=2 MarginYPx: 4, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ Incremental: &IncrementalPolicy{ AllowShiftOnly: true, RenderBudgetMs: 1, // 1ms budget }, }, } // First render (full) initializes state. d0 := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d0, params)) require.True(t, w.renderState.initialized) // Pretend previous render was very slow => over budget for the next frame. w.renderState.lastRenderDurationNs = 10_000_000 // 10ms // Pan right by 1 unit => incremental shift candidate. params2 := params params2.CameraXWorldFp += 1 * SCALE d1 := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d1, params2)) // Shift-only should call CopyShift but not redraw dirty rects. require.NotEmpty(t, d1.CommandsByName("CopyShift")) require.Empty(t, d1.CommandsByName("ClipRect")) require.NotEmpty(t, w.renderState.pendingDirty) // Now stop panning: dx=dy=0. This should trigger catch-up redraw of pendingDirty. w.renderState.lastRenderDurationNs = 0 // under budget params3 := params2 // same camera d2 := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d2, params3)) require.NotEmpty(t, d2.CommandsByName("ClipRect")) require.NotEmpty(t, d2.CommandsByName("AddPoint")) require.Empty(t, w.renderState.pendingDirty) } func TestRender_CatchUpWhilePanning_WhenBackUnderBudget(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) _, err := w.AddPoint(5, 5) require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } policy := &IncrementalPolicy{ AllowShiftOnly: true, RenderBudgetMs: 1, } base := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 10, MarginXPx: 4, // threshold=2 MarginYPx: 4, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ Incremental: policy, }, } // Initial full render. require.NoError(t, w.Render(&fakePrimitiveDrawer{}, base)) // Frame 1: over budget => shift-only, pendingDirty accumulates. w.renderState.lastRenderDurationNs = 10_000_000 // 10ms p1 := base p1.CameraXWorldFp += 1 * SCALE d1 := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d1, p1)) require.NotEmpty(t, d1.CommandsByName("CopyShift")) require.Empty(t, d1.CommandsByName("ClipRect")) require.NotEmpty(t, w.renderState.pendingDirty) // Frame 2: still panning, but now under budget => should shift + redraw (including pendingDirty). w.renderState.lastRenderDurationNs = 0 p2 := p1 p2.CameraXWorldFp += 1 * SCALE d2 := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d2, p2)) require.NotEmpty(t, d2.CommandsByName("CopyShift")) require.NotEmpty(t, d2.CommandsByName("ClipRect")) require.NotEmpty(t, d2.CommandsByName("AddPoint")) require.Empty(t, w.renderState.pendingDirty, "pending dirty should be cleared after successful catch-up redraw") } func TestRender_CatchUpLimit_ReducesPendingDirtyGradually(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) _, err := w.AddPoint(5, 5) require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } policy := &IncrementalPolicy{ AllowShiftOnly: true, RenderBudgetMs: 1, MaxCatchUpAreaPx: 20, // very small budget } base := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 2, MarginXPx: 4, MarginYPx: 4, // canvasH = 2 + 8 = 10 CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ Incremental: policy, }, } // Full init require.NoError(t, w.Render(&fakePrimitiveDrawer{}, base)) // Over budget => shift-only twice to accumulate pending dirty. w.renderState.lastRenderDurationNs = 10_000_000 p1 := base p1.CameraXWorldFp += 1 * SCALE require.NoError(t, w.Render(&fakePrimitiveDrawer{}, p1)) require.NotEmpty(t, w.renderState.pendingDirty) w.renderState.lastRenderDurationNs = 10_000_000 p2 := p1 p2.CameraXWorldFp += 1 * SCALE require.NoError(t, w.Render(&fakePrimitiveDrawer{}, p2)) require.NotEmpty(t, w.renderState.pendingDirty) // Under budget now, but limit catch-up. w.renderState.lastRenderDurationNs = 0 before := len(w.renderState.pendingDirty) require.Greater(t, before, 0) require.NoError(t, w.Render(&fakePrimitiveDrawer{}, p2)) after := len(w.renderState.pendingDirty) // With a tiny MaxCatchUpAreaPx we should not clear everything in one go. require.Greater(t, after, 0) require.Less(t, after, before) }