feat(camera): #78 deterministic Camera.zoom (Q16.16 render-time zoom)
The #78 investigation rejected hardware f32/f64 for the coordinate types (they would desync lockstep/replay/save) and identified camera zoom as the one genuinely-missing render feature. Ship it: Camera.zoom(scale) scales the whole view about the screen centre by a Q16.16 factor, threaded through the same two framebuffer chokepoints (rt_put_px/rt_fill_rect) that carry the camera offset, so it composes with Camera.set/follow/shake. Gated by an internal rt_cam_zoomed flag so a game that never zooms renders byte-for-byte identically (golden renders unchanged); Camera.zoom(1.0) turns it back off. The world coordinate types stay integer px + Q16.16 velocity, so it's a pure render-time transform and itself deterministic. Example examples/library/camera_zoom.ludic (pixel-readback verified), docs page, RFC updated (docs/RFC-POSITION-TYPES.md). Full suite 105/0, fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5af5bdd060
commit
6a83c28e05
8 changed files with 14932 additions and 14780 deletions
37
examples/library/camera_zoom.ludic
Normal file
37
examples/library/camera_zoom.ludic
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# camera_zoom.ludic — the deterministic render-time camera zoom (#78). Zoom is a
|
||||
# Q16.16 scale applied about the screen centre in the blit path; the world stays
|
||||
# integer pixels, so lockstep / replay / world_save are untouched. Verified by
|
||||
# pixel readback (Screen.pixel reads the actual framebuffer, un-transformed).
|
||||
#
|
||||
# Deterministic; a full run prints: 1 0 0 1 1
|
||||
program CameraZoom {
|
||||
# a model so the program runs the ECS and links the core runtime (the framebuffer
|
||||
# + Screen.* live in runtime/native/core.ludic, spliced for an ECS program).
|
||||
property Cam { z: fixed = 1.0 }
|
||||
model View { Cam }
|
||||
|
||||
function bi(b: bool) -> int { if b { return 1 }; return 0 }
|
||||
|
||||
entry {
|
||||
# A. no zoom: a 4x4 white box drawn at (100,100) lands at (100,100).
|
||||
Screen.clear(0)
|
||||
Screen.fill_rectangle(100, 100, 4, 4, 0xffffff)
|
||||
print(bi(Screen.pixel(101, 101) == 0xffffff)) # 1 — box is here
|
||||
print(bi(Screen.pixel(43, 83) == 0xffffff)) # 0 — nothing here yet
|
||||
|
||||
# B. 2x zoom about the screen centre (160,120): (100,100) maps to
|
||||
# ((100-160)*2+160, (100-120)*2+120) = (40, 80), size 4 -> 8, so the box
|
||||
# now covers x 40..48, y 80..88 and has left (101,101).
|
||||
Screen.clear(0)
|
||||
Camera.zoom(2.0)
|
||||
Screen.fill_rectangle(100, 100, 4, 4, 0xffffff)
|
||||
print(bi(Screen.pixel(101, 101) == 0xffffff)) # 0 — moved away
|
||||
print(bi(Screen.pixel(43, 83) == 0xffffff)) # 1 — scaled to here
|
||||
|
||||
# C. zoom back to 1.0 restores the byte-identical un-zoomed blit.
|
||||
Screen.clear(0)
|
||||
Camera.zoom(1.0)
|
||||
Screen.fill_rectangle(100, 100, 4, 4, 0xffffff)
|
||||
print(bi(Screen.pixel(101, 101) == 0xffffff)) # 1 — normal again
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue