fix(sokol): remove HiDPI camera offset double-scaling - #29
Conversation
labelle-gfx's camera.toBackend() builds the offset as
`{ getScreenWidth()/2, getScreenHeight()/2 }`. In the sokol backend
getScreenWidth/Height return `screen_w`/`screen_h` — the physical
framebuffer dimensions set by setScreenSize() — so cam.offset is
already in physical pixels when the camera is handed to the backend.
a610afd added `cam.offset * (screen / design)` in toNdcX/Y,
screenToWorld and worldToScreen under the assumption that the offset
was in design coordinates. On HiDPI mobile (e.g. 2400×1080 physical
vs 1024×768 design) this multiplies an already-physical value by
screen/design ≈ 2.3, shoving the world-space origin off-screen:
cam.offset.x = screen_w/2 = 1200
1200 * (2400/1024) = 2812.5
NDC = (2812.5 / 2400) * 2 - 1 = 1.34 // outside [-1,1]
Use cam.offset directly: (offset.x / screen_w) * 2 - 1 lands at 0 as
intended. Verified on Android emulator (arm64) by loading the
flying-platform scene — before the fix the world was offset far
off-screen; after the fix entities render centered on the camera.
Screen-space (no-camera) branch still divides by design_w/design_h
because screen-space positions remain in design coordinates.
PR SummaryMedium Risk Overview
Reviewed by Cursor Bugbot for commit df8260e. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Code Review
This pull request simplifies coordinate transformations in the Sokol backend by removing redundant HiDPI scaling for camera offsets. The logic was updated in toNdcX, toNdcY, screenToWorld, and worldToScreen because the camera offset is already provided in physical pixels, making the manual scaling between design and physical dimensions unnecessary. I have no feedback to provide.
There was a problem hiding this comment.
Pull request overview
Fixes incorrect camera coordinate transforms in the sokol backend on HiDPI/mobile by removing an extra (and incorrect) design→physical scaling of Camera2D.offset, which was pushing world-space content off-screen when screen != design.
Changes:
- Remove double-scaling of
cam.offsetintoNdcX/toNdcYwhen a camera is active. - Remove the same offset scaling from
screenToWorldandworldToScreen. - Update the in-file documentation to clarify that
cam.offsetis already in physical framebuffer pixels for the sokol backend.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Problem
On HiDPI mobile (Android, 2400×1080 physical / 1024×768 design), world-space entities render far off-screen. The flying-platform scene appears empty because the ship carcase ends up at NDC (1.34, ~−2.4) — completely outside the visible range.
Root cause
a610afd added
cam.offset * (screen / design)scaling intoNdcX/toNdcY/screenToWorld/worldToScreenunder the assumption that the camera offset was in design coordinates. It isn't.labelle-gfx's
camera.toBackend()builds the offset as:```zig
pub fn toBackend(self: *const Self) BackendImpl.Camera2D {
const dims = self.getViewportDimensions();
return .{
.offset = .{ .x = dims.width / 2.0, .y = dims.height / 2.0 },
...
};
}
pub fn getViewportDimensions(self: *const Self) struct { width: f32, height: f32 } {
...
return .{
.width = @floatFromInt(BackendImpl.getScreenWidth()),
.height = @floatFromInt(BackendImpl.getScreenHeight()),
};
}
```
In the sokol backend
getScreenWidth/getScreenHeightreturnscreen_w/screen_h— the physical framebuffer dimensions set bysetScreenSize(). So by the timebeginMode2D(camera)stores it,cam.offsetis already in physical pixels.Multiplying that already-physical value by
screen/design(≈ 2.3 on this device) shoves the world origin off-screen:```
cam.offset.x = screen_w / 2 = 1200
1200 * (2400 / 1024) = 2812.5
NDC = (2812.5 / 2400) * 2 - 1 = 1.34 // outside [-1, 1]
```
Fix
Drop the
* (fw/fdw)/* (fh/fdh)correction from all four functions and usecam.offsetdirectly. Dividing physical-pixelscreen_xby physicalscreen_walready yields correct NDC:(1200 / 2400) * 2 - 1 = 0.The screen-space (no-camera) branch still divides by
design_w/design_h— screen-space positions are still authored in design coordinates, so that path is untouched.Test plan