Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 16 additions & 7 deletions bench/runtime-regression.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1289,7 +1289,7 @@ await test("unequal solid borders use the shaped inner contour", async () => {
borderRadius: "5px 4px 3px 2px",
cornerShape: "squircle bevel scoop notch",
paint: { kind: "solid", color: "#246" },
border: { widths: [1, 2, 3, 1], color: "#fed" },
border: { width: [1, 2, 3, 1], color: "#fed" },
});
await handle.ready;
const explanation = handle.explain();
Expand All @@ -1304,7 +1304,7 @@ await test("unequal solid borders use the shaped inner contour", async () => {
borderRadius: "4px",
cornerShape: "bevel",
paint: { kind: "solid", color: "#246" },
border: { width: 1, colors: ["red", "blue", "red", "blue"] },
border: { width: 1, color: ["red", "blue", "red", "blue"] },
});
} catch (error) {
rejection = error;
Expand Down Expand Up @@ -1562,7 +1562,6 @@ await test("prepared batches validate before mutating", async () => {
assert(error, "invalid prepared batch was accepted");
equal(handle.explain().prepared.backgroundPosition, before.prepared.backgroundPosition, "failed batch mutated crop state");
assert(handle.explain().counters.paints === before.counters.paints, "failed batch painted");
assert(controller.flushPrepared() === 0, "failed batch leaked dirty work");
handle.dispose();
controller.destroy();
element.remove();
Expand All @@ -1575,10 +1574,13 @@ await test("prepared errors do not poison later successful state", async () => {
const handle = controller.attachPrepared(element, preparedConfig());
await handle.ready;
let error = null;
try { controller.setPreparedBackgroundPosition(element, 1, 0); } catch (caught) { error = caught; }
try {
controller.updatePreparedBatch([{ element, backgroundPosition: [1, 0] }]);
} catch (caught) {
error = caught;
}
assert(error instanceof RangeError, "invalid crop did not fail before mutation");
controller.setPreparedBackgroundPosition(element, -1, 0);
controller.flushPrepared();
controller.updatePreparedBatch([{ element, backgroundPosition: [-1, 0] }]);
assert(handle.explain().status === "active" && handle.explain().error === null, "successful crop retained current error state");
handle.dispose();
controller.destroy();
Expand Down Expand Up @@ -1705,6 +1707,13 @@ await test("generic lifecycle migrates roots and defers hidden paint", async ()
&& hiddenHandle.explain().counters.paints === paintsBeforeHiddenUpdate + 1
), "deferred reveal paint");
assert(hiddenHandle.explain().paint.layer.color === "blue", "reveal painted stale pixels");
const paintsBeforeDirectVisibility = hiddenHandle.explain().counters.paints;
await hiddenHandle.update({ visible: false });
await hiddenHandle.update({ visible: true });
assert(
hiddenHandle.explain().counters.paints === paintsBeforeDirectVisibility + 1,
"direct visibility reveal did not repaint exactly once",
);
hiddenHandle.dispose();
visibility.destroy();
wrapper.remove();
Expand Down Expand Up @@ -1760,7 +1769,7 @@ await test("solid prepared visibility batches do not require an image program",
borderRadius: "6px",
cornerShape: "bevel",
paint: { kind: "solid", color: "#08f" },
visibility: false,
visible: false,
});
await handle.ready;
assert(controller.stats().surfaces === 1, "hidden prepared entry did not preallocate its surface");
Expand Down
15 changes: 3 additions & 12 deletions notes/04-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,7 @@ Do not promise recovery of an unknown declaration already dropped by a foreign p

## Direct prepared-state path

High-frequency renderers should not repeatedly serialize and reparse CSS. Expose an internal/direct controller:

```ts
interface CornerfillHandle {
setGeometry(state: ResolvedGeometry): void;
setPaint(state: ResolvedPaint): void;
setVisibility(visible: boolean): void;
dispose(): void;
}
```
High-frequency renderers should not repeatedly serialize and reparse CSS. `attachPrepared()` accepts resolved geometry and paint state, `updatePreparedBatch()` changes crop and visibility synchronously, and the returned handle owns resize and teardown.

PolyCSS preparation can emit normalized radii, a fixed bevel shape, atlas identity, crop metadata, and canonical surface dimensions. Runtime lighting updates then change only the crop key. This stays a generic Cornerfill backend while avoiding CSSOM work in a known prepared pipeline.

Expand Down Expand Up @@ -267,14 +258,14 @@ Possible later optimizations:
- pool detached canvases by backing size;
- delay allocation for culled/offscreen entries;
- keep small canonical PolyCSS surfaces rather than transformed screen bounds;
- batch repaint scheduling while retaining per-element live image identity.
- synchronous batch repaint submission while retaining per-element live image identity.

Do not share surfaces whose pixels diverge per animation frame merely because their shape is identical.

## Historical public API sketch

The following illustrates intent but is not the shipped signature. Current
exports and behavior live in [`src/index.mts`](../src/index.mts) and
exports and behavior live in [`package.json`](../package.json) and
[`src/runtime.mts`](../src/runtime.mts).

```ts
Expand Down
3 changes: 1 addition & 2 deletions notes/09-polycss-case-study.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,10 @@ Attach once:

```ts
const handle = cornerfill.attachPrepared(leaf, {
mode: "paint",
geometry: preparedBevelTriangle,
size: preparedCanonicalSize,
paint: preparedOpaqueAtlasPaint,
visibility: preparedInitialVisibility,
visible: preparedInitialVisibility,
});
```

Expand Down
2 changes: 1 addition & 1 deletion oracle/cases.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export const oracleCases = Object.freeze([
shapeCss: "squircle bevel scoop notch",
shapeParameters: shapes(2, 0, -1, Number.NEGATIVE_INFINITY),
border: Object.freeze({
widths: Object.freeze([8, 16, 22, 5]),
width: Object.freeze([8, 16, 22, 5]),
color: "#f4efdf",
}),
paint: solid("#473bbf"),
Expand Down
4 changes: 1 addition & 3 deletions oracle/painter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,14 @@ export async function attachProductionCandidate(element, oracleCase) {
});
const handle = oracleCase.id === "mario-texel-face"
? controller.attachPrepared(element, {
mode: "paint",
size: oracleCase.size,
borderRadius: oracleCase.radiusCss,
cornerShape: oracleCase.shapeCss,
paint: Object.freeze({ ...oracleCase.paint, opaque: true }),
border: oracleCase.border ?? null,
visibility: true,
visible: true,
})
: controller.attach(element, {
mode: "paint",
paint: oracleCase.paint,
border: oracleCase.border ?? null,
});
Expand Down
27 changes: 13 additions & 14 deletions src/auto-runtime.mts
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,13 @@ export type CornerfillAutomaticCounters = Readonly<AutomaticCounters>;
export interface CornerfillAutoOptions extends CornerfillInstallOptions {
readonly adoptedStyleSheets?: boolean | undefined;
readonly autoObserve?: boolean | undefined;
readonly controller?: CornerfillControllerHandle | undefined;
readonly onError?: ((error: unknown, context: string) => void) | undefined;
readonly root?: AutoRoot | undefined;
readonly stylesheetTimeoutMs?: number | undefined;
}

interface InternalCornerfillAutoOptions extends CornerfillAutoOptions {
readonly controller?: CornerfillControllerHandle | undefined;
readonly parentAuto?: CornerfillAutoController | null | undefined;
}

Expand Down Expand Up @@ -321,18 +321,17 @@ export interface CornerfillAutoExplanation {
}

export interface CornerfillAutoControllerHandle {
readonly ready: Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null>;
readonly ready: Promise<Readonly<CornerfillAutoExplanation>>;
destroy(): void;
explain(
element?: HTMLElement | null,
): Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null;
explain(): Readonly<CornerfillAutoExplanation>;
explain(element: HTMLElement): Readonly<CornerfillEntryExplanation> | null;
refresh(
options?: Readonly<{ retryFailed?: boolean | undefined }>,
): Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null>;
): Promise<Readonly<CornerfillAutoExplanation>>;
refreshAdoptedStyleSheet(
sheet: CSSStyleSheet,
source: string,
): Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null>;
): Promise<Readonly<CornerfillAutoExplanation>>;
registerRoot(
root: ShadowRoot,
options?: Readonly<RegisterRootOptions>,
Expand Down Expand Up @@ -1520,9 +1519,9 @@ class CornerfillAutoController {
declare readonly parentAuto: CornerfillAutoController | null;
declare readonly pendingFetches: Set<AbortController>;
declare readonly pendingStylesheetWaits: Set<() => void>;
declare readonly ready: Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null>;
declare readonly ready: Promise<Readonly<CornerfillAutoExplanation>>;
declare refreshFrame: number | null;
declare refreshPromise: Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null> | null;
declare refreshPromise: Promise<Readonly<CornerfillAutoExplanation>> | null;
declare refreshQueued: boolean;
declare registrationAcquired: boolean;
declare registrationStyle: HTMLStyleElement | null;
Expand Down Expand Up @@ -2861,7 +2860,7 @@ class CornerfillAutoController {
this._configureObservation();
}

async _start(): Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null> {
async _start(): Promise<Readonly<CornerfillAutoExplanation>> {
if (this.destroyed || this.native) return this.explain();
this._ensureCarrierRegistration();
this._installObserver();
Expand Down Expand Up @@ -2891,9 +2890,7 @@ class CornerfillAutoController {
candidates = false,
attachments = false,
retryFailed = false,
}: Readonly<RefreshRequestOptions> = {}): Promise<
Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null
> {
}: Readonly<RefreshRequestOptions> = {}): Promise<Readonly<CornerfillAutoExplanation>> {
if (this.destroyed) return Promise.reject(new Error("Cornerfill auto controller is destroyed"));
if (this.native) return Promise.resolve(this.explain());
this.workRequested = true;
Expand Down Expand Up @@ -2941,7 +2938,7 @@ class CornerfillAutoController {
refreshAdoptedStyleSheet(
sheet: CSSStyleSheet,
source: string,
): Promise<Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null> {
): Promise<Readonly<CornerfillAutoExplanation>> {
if (!this.includeAdoptedStyleSheets) {
return Promise.reject(new TypeError("This automatic scope did not opt in to adopted stylesheets"));
}
Expand Down Expand Up @@ -2989,6 +2986,8 @@ class CornerfillAutoController {
return true;
}

explain(): Readonly<CornerfillAutoExplanation>;
explain(element: HTMLElement): Readonly<CornerfillEntryExplanation> | null;
explain(
element: HTMLElement | null = null,
): Readonly<CornerfillAutoExplanation> | Readonly<CornerfillEntryExplanation> | null {
Expand Down
14 changes: 6 additions & 8 deletions src/auto.mts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,10 @@ interface NativeControllerOptions {
}

interface NativeAutoController extends CornerfillAutoControllerHandle {
readonly ready: Promise<Readonly<CornerfillAutoExplanation>>;
_removeScope(shadowRoot: ShadowRoot, scope: NativeAutoController): void;
destroy(): void;
explain(element?: HTMLElement | null): Readonly<CornerfillAutoExplanation> | null;
refresh(): Promise<Readonly<CornerfillAutoExplanation>>;
refreshAdoptedStyleSheet(sheet: CSSStyleSheet, source: string): Promise<Readonly<CornerfillAutoExplanation>>;
explain(): Readonly<CornerfillAutoExplanation>;
explain(element: HTMLElement): null;
registerRoot(shadowRoot: ShadowRoot, options?: Readonly<RegisterRootOptions>): NativeAutoController;
unregisterRoot(shadowRoot: ShadowRoot): boolean;
}

function nativeController(
Expand All @@ -37,7 +33,9 @@ function nativeController(
): NativeAutoController {
let destroyed = false;
const scopes = new Map<ShadowRoot, NativeAutoController>();
const explain = (element: HTMLElement | null = null): Readonly<CornerfillAutoExplanation> | null => {
function explain(): Readonly<CornerfillAutoExplanation>;
function explain(element: HTMLElement): null;
function explain(element: HTMLElement | null = null): Readonly<CornerfillAutoExplanation> | null {
if (element) return null;
return Object.freeze({
schema: "cornerfill-auto@1",
Expand All @@ -61,7 +59,7 @@ function nativeController(
oracleQualification: CORNERFILL_ORACLE_QUALIFICATION,
runtime: null,
});
};
}
const initialExplanation = explain();
if (!initialExplanation) throw new Error("native Cornerfill explanation is unavailable");
const controller: NativeAutoController = {
Expand Down
Loading
Loading