Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 73 additions & 5 deletions engine/symbian/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
//! QGLWidget owns the graphics context and calls the GLES2 entry points only
//! while it is current. The software capture entry points return tightly
//! packed, top-left-origin ARGB32 pixels; those pointers remain valid until
//! the next capture, viewport change, init, or shutdown call.
//! the next capture, viewport change, init, or shutdown call. Framebuffer-only
//! hosts may instead render directly into host-owned RGB565 storage.

#![cfg_attr(any(target_os = "none", feature = "bare-platform"), no_std)]
#![cfg_attr(
Expand Down Expand Up @@ -159,6 +160,7 @@ static mut DAMAGE_BOUNDS: [i32; 4] = [0, 0, 0, 0];
static mut FRAMEBUFFER_WIDTH: u32 = 0;
static mut FRAMEBUFFER_HEIGHT: u32 = 0;
static mut FRAMEBUFFER_STRIDE: u32 = 0;
static mut FRAMEBUFFER_LENGTH: usize = 0;

/// Stock cores have no application-specific native surface. A custom static
/// library depends on this crate with default features disabled and exports
Expand Down Expand Up @@ -202,6 +204,7 @@ fn clear_framebuffer() {
FRAMEBUFFER_WIDTH = 0;
FRAMEBUFFER_HEIGHT = 0;
FRAMEBUFFER_STRIDE = 0;
FRAMEBUFFER_LENGTH = 0;
}
}

Expand Down Expand Up @@ -667,11 +670,12 @@ fn framebuffer_geometry(instance: &Ui, scale: u32) -> Option<(usize, usize, usiz
Some((width, height, byte_len))
}

fn remember_framebuffer_geometry(width: usize, height: usize) {
fn remember_framebuffer_geometry(width: usize, height: usize, bytes_per_pixel: usize) {
unsafe {
FRAMEBUFFER_WIDTH = width as u32;
FRAMEBUFFER_HEIGHT = height as u32;
FRAMEBUFFER_STRIDE = (width * 4) as u32;
FRAMEBUFFER_STRIDE = (width * bytes_per_pixel) as u32;
FRAMEBUFFER_LENGTH = width * height * bytes_per_pixel;
}
}

Expand Down Expand Up @@ -737,11 +741,66 @@ fn render_at_scale(scale: u32, incremental: bool) -> *const u8 {
DAMAGE_BOUNDS = [0, 0, width as i32, height as i32];
}

remember_framebuffer_geometry(width, height);
remember_framebuffer_geometry(width, height, 4);
FRAMEBUFFER.as_ptr()
}
}

/// Render incrementally into a caller-owned native RGB565 framebuffer.
///
/// Framebuffer-only hosts use this path to avoid allocating an intermediate
/// ARGB surface and converting every pixel after each render. The buffer is
/// tightly packed at the logical viewport size and remains owned by the host.
#[no_mangle]
pub extern "C" fn ui_render_rgb565_incremental(framebuffer: *mut u16, pixel_count: usize) -> i32 {
if framebuffer.is_null() {
return 0;
}
let instance = ui();
let Some((width, height, byte_len)) = framebuffer_geometry(instance, 1) else {
return 0;
};
let expected_pixels = byte_len / 4;
if pixel_count != expected_pixels {
return 0;
}
let draw_list: *const pocketjs_core::DrawList = instance.draw();
let instance_ref: &Ui = unsafe { &*(instance as *const Ui) };
let output = unsafe { core::slice::from_raw_parts_mut(framebuffer, pixel_count) };

unsafe {
DAMAGE_ATTEMPTS = DAMAGE_ATTEMPTS.wrapping_add(1);
match raster::render_scaled_rgb565_incremental(
instance_ref,
&(*draw_list).words,
output,
1,
&mut DAMAGE_TRACKER,
DamagePolicy::default(),
) {
Ok(plan) => {
let bounds = plan.bounds();
DAMAGE_REGIONS = plan.region_count() as u32;
DAMAGE_PIXELS = plan.area();
DAMAGE_BOUNDS = [bounds.x0, bounds.y0, bounds.x1, bounds.y1];
if plan.is_full_redraw() {
DAMAGE_FULL_REDRAWS = DAMAGE_FULL_REDRAWS.wrapping_add(1);
}
}
Err(_) => {
DAMAGE_FAILURES = DAMAGE_FAILURES.wrapping_add(1);
DAMAGE_REGIONS = 0;
DAMAGE_PIXELS = pixel_count as u64;
DAMAGE_BOUNDS = [0, 0, width as i32, height as i32];
raster::render_scaled_rgb565(instance_ref, &(*draw_list).words, output, 1);
DAMAGE_TRACKER.invalidate();
}
}
remember_framebuffer_geometry(width, height, 2);
}
1
}

/// Incremental-raster statistics, so a host can tell a working damage plan
/// from a silent per-frame fallback. Counts are cumulative; the region,
/// pixel and bounds values describe the most recent frame.
Expand Down Expand Up @@ -829,7 +888,7 @@ pub extern "C" fn ui_framebuffer_stride() -> u32 {

#[no_mangle]
pub extern "C" fn ui_framebuffer_len() -> usize {
unsafe { FRAMEBUFFER.len() }
unsafe { FRAMEBUFFER_LENGTH }
}

#[cfg(test)]
Expand Down Expand Up @@ -867,6 +926,15 @@ mod tests {
assert_eq!(ui_framebuffer_len(), 8);
let pixels = unsafe { core::slice::from_raw_parts(framebuffer, 8) };
assert_eq!(pixels, &[0x11, 0x22, 0x33, 0xff, 0x11, 0x22, 0x33, 0xff]);

let mut rgb565 = [0u16; 2];
assert_eq!(
ui_render_rgb565_incremental(rgb565.as_mut_ptr(), rgb565.len()),
1
);
assert_eq!(rgb565, [raster::pack_rgb565(0x33, 0x22, 0x11); 2]);
assert_eq!(ui_framebuffer_stride(), 4);
assert_eq!(ui_framebuffer_len(), 4);
ui_shutdown();
}
}
120 changes: 101 additions & 19 deletions framework/src/kinetics-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
// tracking finger-follow (gesture pan feeds drag deltas; out-of-bounds
// travel is rubber-banded with the classic iOS curve)
// fling exponential decay from the release velocity
// spring edge bounce-back — semi-implicit Euler with the engine's
// Spring constants (K=170, C=26), CARRYING the incoming velocity
// (a fling that crosses an edge keeps its momentum into the
// rubber band; this is the one place "spring with initial
// velocity" is needed, and it lives here, not in the core)
// spring edge bounce-back or retargetable programmatic follow —
// semi-implicit Euler with the engine's Spring constants
// (K=170, C=26), carrying incoming velocity across retargets
// chase per-frame ease toward a target — byte-for-byte the apps/im
// pump (0.3 of the remaining distance, snap under 0.6 px); the
// d-pad / stick-to-bottom mode
Expand Down Expand Up @@ -76,14 +74,21 @@ export interface Scroller {
nudge(delta: number): void;
/** Chase an absolute target (focus-follow, stick-to-bottom). */
chaseTo(to: number): void;
/** Spring toward an absolute target. Repeated calls preserve velocity.
* `overshootPx` adds a bounded approach point beyond the final target;
* stiffness/damping default to the edge-spring constants. */
springTo(to: number, opts?: {
overshootPx?: number;
stiffness?: number;
damping?: number;
}): void;
/** Shift offset AND every in-flight anchor by delta after a prepend, so
* backfill never moves what the user is looking at (the im rebase). */
rebase(delta: number): void;
/** The position the scroller is heading to: the chase/tween target when
* one is in flight, the current offset otherwise. */
/** The position the scroller is heading to: the chase/tween/spring final
* target when one is in flight, the current offset otherwise. */
intent(): number;
/** At the end of the range, judged on INTENT: the chase/tween target when
* one is in flight, the position otherwise (the im at-bottom rule). */
/** At the end of the range, judged on INTENT rather than transient motion. */
isAtEnd(slackPx?: number): boolean;
/** Rest position a fling from `v` would reach (for snap functions). */
projectFling(v: number): number;
Expand Down Expand Up @@ -147,7 +152,13 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
let v = 0; // px per virtual second (fling/spring)
let dragPos = 0; // unrubbered drag-space position while tracking
let target = 0; // chase target
let springBound = 0; // the edge a spring is heading to
let springBound = 0; // current spring approach/rebound point
let springFinal = 0; // clamped public intent
let springDirection = 0; // approach direction, -1 / 0 / 1
let springOvershoot = 0;
let springReturning = false;
let springK = SPRING_K;
let springC = SPRING_C;
let tweenFrom = 0;
let tweenTo = 0;
let tweenFrames = 1;
Expand Down Expand Up @@ -194,6 +205,17 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
return pos + (v0 * TICK_DT * decay) / (1 - decay);
}

function startEdgeSpring(bound: number): void {
springBound = bound;
springFinal = bound;
springDirection = 0;
springOvershoot = 0;
springReturning = false;
springK = SPRING_K;
springC = SPRING_C;
state = "spring";
}

return {
offset,
velocity: () => v,
Expand All @@ -218,9 +240,8 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
if (state !== "tracking") return;
const m = opts.max();
if (pos < 0 || pos > m) {
springBound = pos < 0 ? 0 : m;
v = releaseVelocity;
state = "spring";
startEdgeSpring(pos < 0 ? 0 : m);
return;
}
if (opts.snap) {
Expand Down Expand Up @@ -249,7 +270,13 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
},

scrollBy(delta: number, o?: { durMs?: number } | { immediate: true }): void {
const base = state === "tween" ? tweenTo : state === "chase" ? target : pos;
const base = state === "tween"
? tweenTo
: state === "chase"
? target
: state === "spring"
? springFinal
: pos;
this.scrollTo(base + delta, o);
},

Expand All @@ -270,17 +297,56 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
state = "chase";
},

springTo(to: number, o?: {
overshootPx?: number;
stiffness?: number;
damping?: number;
}): void {
const final = clampRange(to);
const delta = final - pos;
const direction = delta < 0 ? -1 : delta > 0 ? 1 : 0;
const requested = o?.overshootPx ?? 0;
const approach = Number.isFinite(requested) && requested > 0 ? requested : 0;
const requestedK = o?.stiffness;
const requestedC = o?.damping;

if (state !== "spring" && state !== "fling") v = 0;
springFinal = final;
springDirection = direction;
springOvershoot = direction === 0 ? 0 : approach;
springReturning = false;
springK = requestedK !== undefined && Number.isFinite(requestedK) && requestedK > 0
? requestedK
: SPRING_K;
springC = requestedC !== undefined && Number.isFinite(requestedC) && requestedC > 0
? requestedC
: SPRING_C;
springBound = final + direction * springOvershoot;
if (springBound === pos && v === 0) {
state = "idle";
return;
}
state = "spring";
},

rebase(delta: number): void {
dragPos += delta;
target += delta;
springBound += delta;
springFinal += delta;
tweenFrom += delta;
tweenTo += delta;
emit(pos + delta);
},

intent(): number {
return state === "chase" ? target : state === "tween" ? tweenTo : pos;
return state === "chase"
? target
: state === "tween"
? tweenTo
: state === "spring"
? springFinal
: pos;
},

isAtEnd(slackPx = 1): boolean {
Expand Down Expand Up @@ -334,27 +400,43 @@ export function createScrollerWith(cell: ScrollerCell, opts: ScrollerOptions): S
const m = opts.max();
if (p < 0 || p > m) {
// Carry the momentum into the edge spring mid-tick.
springBound = p < 0 ? 0 : m;
state = "spring";
startEdgeSpring(p < 0 ? 0 : m);
continue;
}
if (v < FLING_MIN_V && v > -FLING_MIN_V) {
settle(p);
return;
}
} else {
const b = clampRange(springBound);
const a = SPRING_K * (b - p) - SPRING_C * v;
const b = springBound;
const a = springK * (b - p) - springC * v;
v += a * TICK_DT;
p += v * TICK_DT;
const dist = b - p;
const crossedApproach = !springReturning && springOvershoot > 0 && (
springDirection > 0 ? p >= b : p <= b
);
if (crossedApproach) {
p = b;
v = 0;
springReturning = true;
springBound = springFinal;
continue;
}
if (
dist < SPRING_SETTLE_DIST &&
dist > -SPRING_SETTLE_DIST &&
v < SPRING_SETTLE_V &&
v > -SPRING_SETTLE_V
) {
settle(b);
if (!springReturning && springOvershoot > 0) {
p = b;
v = 0;
springReturning = true;
springBound = springFinal;
continue;
}
settle(springFinal);
return;
}
}
Expand Down
1 change: 1 addition & 0 deletions hosts/iphone2g/pocket_core.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ int32_t ui_debug_rect_wh(void);
void ui_debug_pause(int32_t paused);
void ui_debug_step(void);
const uint8_t *ui_render_incremental(void);
int32_t ui_render_rgb565_incremental(uint16_t *framebuffer, size_t pixel_count);
uint32_t ui_framebuffer_width(void);
uint32_t ui_framebuffer_height(void);
uint32_t ui_framebuffer_stride(void);
Expand Down
Loading