191 lines
4.9 KiB
Go
191 lines
4.9 KiB
Go
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)
|
|
}
|