ui: basic map scroller
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user