Files
galaxy-game/client/world/renderer_incremental_budget_test.go
T
2026-03-07 00:29:06 +03:00

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)
}