Skip to content

Commit a610afd

Browse files
committed
fix(sokol): scale camera offset to physical pixels for HiDPI (mobile) NDC
On desktop, design dimensions == physical dimensions so the camera offset (stored in design coordinates, e.g. 512,384) maps correctly to NDC. On HiDPI mobile (e.g. 2400×1080 physical vs 1024×768 design), the offset must be scaled by screen/design ratio before the NDC divide, otherwise the world centre lands at NDC (-0.57, 0.29) instead of (0, 0) — a white screen. Fix: multiply cam.offset by (screen_w/design_w, screen_h/design_h) in toNdcX/Y, worldToScreen, and screenToWorld.
1 parent e66841d commit a610afd

1 file changed

Lines changed: 24 additions & 9 deletions

File tree

backends/sokol/src/gfx.zig

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,24 @@ var camera_active: bool = false;
8888

8989
/// Convert screen-space pixel coordinates to NDC (-1..1) for sokol_gl.
9090
/// When a camera is active, applies forward transform: (world - target) * zoom + offset.
91+
///
92+
/// The camera offset is stored in design coordinates (e.g. design_w/2, design_h/2)
93+
/// so that the engine code is portable between backends. When physical screen
94+
/// dimensions differ from design dimensions (HiDPI / mobile), scale the offset to
95+
/// physical pixels before dividing by the physical screen extent.
9196
fn toNdcX(px: f32) f32 {
9297
if (!camera_active) {
9398
// Screen-space: map design coords directly to NDC so a design-width
9499
// quad fills exactly NDC -1..1 regardless of physical screen width.
95100
return (px / @as(f32, @floatFromInt(design_w))) * 2.0 - 1.0;
96101
}
97102
const cam = active_camera;
98-
const screen_x = (px - cam.target.x) * cam.zoom + cam.offset.x;
99-
return (screen_x / @as(f32, @floatFromInt(screen_w))) * 2.0 - 1.0;
103+
const fw = @as(f32, @floatFromInt(screen_w));
104+
const fdw = @as(f32, @floatFromInt(design_w));
105+
// Scale offset from design-space to physical-space so the camera center
106+
// lands at the correct NDC position even when design != physical (HiDPI).
107+
const screen_x = (px - cam.target.x) * cam.zoom + cam.offset.x * (fw / fdw);
108+
return (screen_x / fw) * 2.0 - 1.0;
100109
}
101110

102111
fn toNdcY(py: f32) f32 {
@@ -107,10 +116,10 @@ fn toNdcY(py: f32) f32 {
107116
}
108117
const cam = active_camera;
109118
const fh = @as(f32, @floatFromInt(screen_h));
119+
const fdh = @as(f32, @floatFromInt(design_h));
110120
// Positions arrive in screen-space Y-down (Y-flipped by renderer.toScreenY).
111-
// Apply the camera in the same screen-Y-down convention as the raylib backend:
112-
// screen_final = (py - target.y) * zoom + offset.y
113-
const screen_y = (py - cam.target.y) * cam.zoom + cam.offset.y;
121+
// Scale offset from design-space to physical-space (same HiDPI correction as X).
122+
const screen_y = (py - cam.target.y) * cam.zoom + cam.offset.y * (fh / fdh);
114123
return 1.0 - (screen_y / fh) * 2.0;
115124
}
116125

@@ -371,18 +380,24 @@ pub fn getScreenHeight() i32 {
371380
}
372381

373382
pub fn screenToWorld(pos: Vector2, camera: Camera2D) Vector2 {
383+
// Scale camera offset from design-space to physical-space (HiDPI correction).
384+
const off_x = camera.offset.x * @as(f32, @floatFromInt(screen_w)) / @as(f32, @floatFromInt(design_w));
385+
const off_y = camera.offset.y * @as(f32, @floatFromInt(screen_h)) / @as(f32, @floatFromInt(design_h));
374386
return .{
375-
.x = (pos.x - camera.offset.x) / camera.zoom + camera.target.x,
387+
.x = (pos.x - off_x) / camera.zoom + camera.target.x,
376388
// Screen Y-down convention, same as raylib backend
377-
.y = (pos.y - camera.offset.y) / camera.zoom + camera.target.y,
389+
.y = (pos.y - off_y) / camera.zoom + camera.target.y,
378390
};
379391
}
380392

381393
pub fn worldToScreen(pos: Vector2, camera: Camera2D) Vector2 {
394+
// Scale camera offset from design-space to physical-space (HiDPI correction).
395+
const off_x = camera.offset.x * @as(f32, @floatFromInt(screen_w)) / @as(f32, @floatFromInt(design_w));
396+
const off_y = camera.offset.y * @as(f32, @floatFromInt(screen_h)) / @as(f32, @floatFromInt(design_h));
382397
return .{
383-
.x = (pos.x - camera.target.x) * camera.zoom + camera.offset.x,
398+
.x = (pos.x - camera.target.x) * camera.zoom + off_x,
384399
// Screen Y-down convention, same as raylib backend
385-
.y = (pos.y - camera.target.y) * camera.zoom + camera.offset.y,
400+
.y = (pos.y - camera.target.y) * camera.zoom + off_y,
386401
};
387402
}
388403

0 commit comments

Comments
 (0)