Skip to content

Commit

Permalink
Layered rendering util (#24)
Browse files Browse the repository at this point in the history
* well its a start

* apostrophy

* mostly just move ReglLayer2D over to packages, and make a minor change (that should hopefully be less surprising) to long-running-frame lifecycle callbacks

* bump the version
  • Loading branch information
froyo-np authored Jun 3, 2024
1 parent e10b436 commit 07e43f1
Show file tree
Hide file tree
Showing 10 changed files with 69 additions and 30 deletions.
3 changes: 1 addition & 2 deletions apps/layers/src/demo.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Box2D, Vec2, type box2D, type vec2 } from "@alleninstitute/vis-geometry";
import { type ColumnRequest } from "Common/loaders/scatterplot/scatterbrain-loader";
import REGL from "regl";
import { AsyncDataCache, type FrameLifecycle, type NormalStatus } from "@alleninstitute/vis-scatterbrain";
import { AsyncDataCache, ReglLayer2D, type FrameLifecycle, type NormalStatus } from "@alleninstitute/vis-scatterbrain";
import { buildRenderer } from "../../scatterplot/src/renderer";
import { buildImageRenderer } from "../../omezarr-viewer/src/image-renderer";
import { ReglLayer2D } from "./layer";
import { renderDynamicGrid, renderSlide, type RenderSettings as SlideRenderSettings } from "./data-renderers/dynamicGridSlideRenderer";
import { renderGrid, renderSlice, type RenderSettings as SliceRenderSettings } from "./data-renderers/volumeSliceRenderer";
import { renderAnnotationLayer, type RenderSettings as AnnotationRenderSettings, type SimpleAnnotation } from "./data-renderers/simpleAnnotationRenderer";
Expand Down
2 changes: 1 addition & 1 deletion apps/layers/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { box2D } from "@alleninstitute/vis-geometry";
import type REGL from "regl";
import type { ReglLayer2D } from "./layer";
import { type RenderSettings as SlideRenderSettings } from "./data-renderers/dynamicGridSlideRenderer";
import { type RenderSettings as SliceRenderSettings } from "./data-renderers/volumeSliceRenderer";
import { type RenderSettings as AnnotationRenderSettings, type SimpleAnnotation } from "./data-renderers/simpleAnnotationRenderer";
Expand All @@ -9,6 +8,7 @@ import type { DynamicGrid, DynamicGridSlide } from "./data-sources/scatterplot/d
import type { AxisAlignedZarrSliceGrid } from "./data-sources/ome-zarr/slice-grid";
import type { RenderSettings as AnnotationGridRenderSettings, CacheContentType as GpuMesh } from "./data-renderers/annotation-renderer";
import type { AnnotationGrid } from "./data-sources/annotation/annotation-grid";
import type { ReglLayer2D } from "@alleninstitute/vis-scatterbrain";
// note: right now, all layers should be considered 2D, and WebGL only...
export type Image = {
texture: REGL.Framebuffer2D
Expand Down
9 changes: 5 additions & 4 deletions packages/scatterbrain/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@alleninstitute/vis-scatterbrain",
"version": "0.0.2",
"version": "0.0.3",
"contributors": [
{
"name": "Lane Sawyer",
Expand Down Expand Up @@ -54,7 +54,8 @@
"vitest": "^1.4.0"
},
"dependencies": {
"lodash": "^4.17.21"

"@alleninstitute/vis-geometry": "workspace:*",
"lodash": "^4.17.21",
"regl": "^2.1.0"
}
}
}
2 changes: 2 additions & 0 deletions packages/scatterbrain/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export { beginLongRunningFrame } from "./render-queue";
export { AsyncDataCache } from "./dataset-cache";
export { ReglLayer2D } from './layers/layer-2D'
export * from './layers/buffer-pair'
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
import { Box2D, type box2D, type vec2 } from "@alleninstitute/vis-geometry";

// a helper to render a 2D layer, using regl
import type { Image, ImageRenderer, RenderFn } from "./types";
import { type BufferPair, swapBuffers } from "./buffer-pair";
import type REGL from "regl";
import { swapBuffers, type BufferPair } from "../../common/src/bufferPair";
import type { Image } from "./types";
import type { FrameLifecycle, NormalStatus } from "@alleninstitute/vis-scatterbrain";
import type { Camera } from "./data-renderers/types";
import type { buildImageRenderer } from "../../omezarr-viewer/src/image-renderer";
import type { FrameLifecycle, RenderCallback } from "../render-queue";
import { Box2D, type box2D, type vec2 } from '@alleninstitute/vis-geometry'

type RenderFn<R, S> =
(target: REGL.Framebuffer2D | null, thing: Readonly<R>, settings: Readonly<S>) => FrameLifecycle;
type ImageRenderer = ReturnType<typeof buildImageRenderer>
type RenderCallback = (event: { status: NormalStatus } | { status: 'error', error: unknown }) => void;
type EventType = Parameters<RenderCallback>[0]
type RequiredSettings = { camera: Camera, callback: RenderCallback }
/**
* a class that makes it easy to manage rendering 2D layers using regl
*/
type RequiredSettings = { camera: { view: box2D }, callback: RenderCallback }

export class ReglLayer2D<Renderable, RenderSettings extends RequiredSettings> {
private buffers: BufferPair<Image>;
private renderFn: RenderFn<Renderable, RenderSettings>
Expand All @@ -23,8 +17,8 @@ export class ReglLayer2D<Renderable, RenderSettings extends RequiredSettings> {
private renderImg: ImageRenderer
constructor(regl: REGL.Regl, imgRenderer: ImageRenderer, renderFn: RenderFn<Renderable, RenderSettings & RequiredSettings>, resolution: vec2) {
this.buffers = {
readFrom: { texture: regl.framebuffer(...resolution), bounds: undefined },
writeTo: { texture: regl.framebuffer(...resolution), bounds: undefined }
readFrom: { resolution, texture: regl.framebuffer(...resolution), bounds: undefined },
writeTo: { resolution, texture: regl.framebuffer(...resolution), bounds: undefined }
};
this.renderImg = imgRenderer
this.regl = regl;
Expand All @@ -44,18 +38,22 @@ export class ReglLayer2D<Renderable, RenderSettings extends RequiredSettings> {
onChange(props: {
readonly data: Readonly<Renderable>;
readonly settings: Readonly<RenderSettings>
}, cancel:boolean=true) {
}, cancel: boolean = true) {

if (cancel && this.runningFrame) {
this.runningFrame.cancelFrame();
this.runningFrame = null;
const { readFrom, writeTo } = this.buffers;
// copy our work to the prev-buffer...
if (readFrom.bounds && writeTo.bounds && Box2D.intersection(readFrom.bounds, writeTo.bounds)) {
const [width, height] = writeTo.resolution;
this.renderImg({
box: Box2D.toFlatArray(writeTo.bounds),
img: writeTo.texture,
target: readFrom.texture,
viewport: {
x: 0, y: 0, width, height
},
view: Box2D.toFlatArray(readFrom.bounds)
})
}
Expand All @@ -76,7 +74,7 @@ export class ReglLayer2D<Renderable, RenderSettings extends RequiredSettings> {
case 'finished_synchronously':
this.buffers = swapBuffers(this.buffers);
// only erase... if we would have cancelled...
if(cancel){
if (cancel) {
this.regl.clear({ framebuffer: this.buffers.writeTo.texture, color: [0, 0, 0, 0], depth: 1 })
}
this.runningFrame = null;
Expand Down
23 changes: 23 additions & 0 deletions packages/scatterbrain/src/layers/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { box2D, vec2, vec4 } from '@alleninstitute/vis-geometry';
import type { FrameLifecycle } from '../render-queue'
import type REGL from "regl";

export type RenderFn<Data, Settings> =
(target: REGL.Framebuffer2D | null, thing: Readonly<Data>, settings: Readonly<Settings>) => FrameLifecycle;

export type Image = {
resolution: vec2;
texture: REGL.Framebuffer2D;
bounds: box2D | undefined; // if undefined, it means we allocated the texture, but its empty and should not be used (except to fill it)
}

type ImageRendererProps = {
target: REGL.Framebuffer2D | null;
box: vec4;
view: vec4;
viewport: REGL.BoundingBox;
img: REGL.Texture2D | REGL.Framebuffer2D;
}

// a function which renders an axis aligned image to another axis aligned image - no funny buisness
export type ImageRenderer = (props: ImageRendererProps) => void;
16 changes: 11 additions & 5 deletions packages/scatterbrain/src/render-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export type FrameLifecycle = {
* `progress` - The frame is still running and has not finished
*/
export type NormalStatus = 'begun' | 'finished' | 'cancelled' | 'finished_synchronously' | 'progress';

export type RenderCallback = (event: { status: NormalStatus } | { status: 'error', error: unknown }) => void;
/**
* `beingLongRunningFrame` starts a long-running frame that will render a list of items asynchronously based on
* the provided data, settings, and rendering functions.
Expand Down Expand Up @@ -59,17 +59,23 @@ export function beginLongRunningFrame<Column, Item, Settings>(
settings: Settings,
requestsForItem: (item: Item, settings: Settings, signal?: AbortSignal) => Record<string, () => Promise<Column>>,
render: (item: Item, settings: Settings, columns: Record<string, Column | undefined>) => void,
lifecycleCallback: (event: { status: NormalStatus } | { status: 'error'; error: unknown }) => void,
lifecycleCallback: RenderCallback,
cacheKeyForRequest: (requestKey: string, item: Item, settings: Settings) => string = (key) => key,
queueTimeBudgetMS: number = queueProcessingIntervalMS / 3
): FrameLifecycle {
const abort = new AbortController();
const reportNormalStatus = (status: NormalStatus) => {
lifecycleCallback({ status });
};
const queue: Item[] = [];
const taskCancelCallbacks: Array<() => void> = [];

const reportNormalStatus = (status: NormalStatus) => {
// we want to report our status, however the flow of events can be confusing -
// our callers anticipate an asynchronous (long running) frame to be started,
// but there are scenarios in which the whole thing is completely synchronous
// callers who are scheduling things may be surprised that their frame finished
// before the code that handles it appears to start. thus, we make the entire lifecycle callback
// system async, to prevent surprises.
Promise.resolve().then(() => lifecycleCallback({ status }));
};
// when starting a frame, we greedily attempt to render any tasks that are already in the cache
// however, if there is too much overhead (or too many tasks) we would risk hogging the main thread
// thus - obey the limit (its a soft limit)
Expand Down
1 change: 1 addition & 0 deletions packages/scatterbrain/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"./*"
]
},
"moduleResolution": "Bundler",
"module": "ES2022",
"target": "ES2022",
"lib": [
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 07e43f1

Please sign in to comment.