Skip to content

Commit 8eddfb3

Browse files
committed
Try batching draw calls
1 parent d501660 commit 8eddfb3

3 files changed

Lines changed: 100 additions & 42 deletions

File tree

src/draw.ts

Lines changed: 99 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const COLORS = '0123456789abcdef'
2828
const MIN_DEPTH = 3
2929
const MAX_DEPTH = Infinity
3030
const MAX_DRAW_CALLS = 1e4 // number of shapes to draw per frame
31+
const MAX_DRAW_TIME_MS = 15 // how long to draw a frame in ms
3132
const MAX_QUEUE_SIZE = 1e6
3233
const MIN_PATTERN_SIZE = 0.0005 // ignore patterns where either side is smaller than this
3334
const DEBUG = true as boolean
@@ -85,30 +86,6 @@ const measure = (f: () => void): number => {
8586
return performance.now() - start
8687
}
8788

88-
// ---
89-
90-
const drawScreen = (
91-
ctx: CanvasRenderingContext2D,
92-
screenSize: Size,
93-
absolutePattern: AbsolutePattern,
94-
strokeStyle: string
95-
): void => {
96-
const viewportPattern = mapPatternToViewportSpace(absolutePattern, screenSize)
97-
const [p1, p2, p3, p4] = getPatternPoints(viewportPattern)
98-
99-
ctx.strokeStyle = strokeStyle
100-
101-
ctx.beginPath()
102-
ctx.moveTo(...p1)
103-
ctx.lineTo(...p2)
104-
ctx.lineTo(...p3)
105-
ctx.lineTo(...p4)
106-
ctx.closePath()
107-
108-
ctx.fill()
109-
ctx.stroke()
110-
}
111-
11289
type QueueEntry = {
11390
currentPattern: AbsolutePattern
11491
depth: number
@@ -136,7 +113,9 @@ function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
136113

137114
// Don't add patterns that are too deep.
138115
if (entry.depth >= MAX_DEPTH) {
139-
console.log(`MAX_DEPTH (${MAX_DEPTH}) reached at depth ${entry.depth}. Breaking from generateDrawQueue loop.`)
116+
console.log(
117+
`MAX_DEPTH (${MAX_DEPTH}) reached at depth ${entry.depth}. Breaking from generateDrawQueue loop.`
118+
)
140119
break
141120
}
142121

@@ -158,7 +137,58 @@ function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
158137
}
159138
}
160139

161-
console.log(`generateDrawQueue done. Total iterations: ${iterations}. Final queue size: ${patternQueue.size}`)
140+
console.log(
141+
`generateDrawQueue done. Total iterations: ${iterations}. Final queue size: ${patternQueue.size}`
142+
)
143+
}
144+
145+
type Chunk = {
146+
depth: number
147+
patterns: AbsolutePattern[]
148+
}
149+
150+
function* generateChunkedDraws(state: State, chunkSize: number): Generator<Chunk, void, void> {
151+
const drawQueueIterator = generateDrawQueue(state)
152+
153+
let chunk: Chunk = {
154+
depth: 0,
155+
patterns: [],
156+
}
157+
158+
while (true) {
159+
const iteratorResult = drawQueueIterator.next()
160+
161+
if (iteratorResult.done) {
162+
if (chunk.patterns.length > 0) {
163+
yield chunk
164+
}
165+
166+
break
167+
}
168+
169+
const entry = iteratorResult.value
170+
171+
if (chunk.depth !== entry.depth) {
172+
if (chunk.patterns.length > 0) {
173+
yield chunk
174+
}
175+
176+
chunk = {
177+
depth: entry.depth,
178+
patterns: [entry.currentPattern],
179+
}
180+
} else {
181+
chunk.patterns.push(entry.currentPattern)
182+
}
183+
184+
if (chunk.patterns.length >= chunkSize) {
185+
yield chunk
186+
chunk = {
187+
depth: entry.depth,
188+
patterns: [],
189+
}
190+
}
191+
}
162192
}
163193

164194
export function* drawFrameIncrementally(
@@ -168,28 +198,60 @@ export function* drawFrameIncrementally(
168198
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
169199
ctx.fillStyle = 'black'
170200
ctx.lineWidth = 1
171-
201+
172202
const screenSize = getScreenSize(ctx)
173203

174-
const drawQueueIterator = generateDrawQueue(state)
204+
const chunkedDrawsIterator = generateChunkedDraws(state, 1000)
205+
206+
let iterations = 0
175207

176208
while (true) {
209+
iterations += 1
210+
177211
let drawScreenCalls = 0
178212

179213
const duration = measure(() => {
180-
// Manual iteration
214+
const start = performance.now()
215+
181216
while (true) {
182-
const iteratorResult = drawQueueIterator.next()
217+
const iteratorResult = chunkedDrawsIterator.next()
183218

184219
if (iteratorResult.done) break
185220

186-
const { currentPattern, depth } = iteratorResult.value
187-
188-
drawScreenCalls += 1
189-
190-
drawScreen(ctx, screenSize, currentPattern, COLORS[Math.min(COLORS.length - 1, depth)]!)
191-
192-
if (depth > MIN_DEPTH && drawScreenCalls >= MAX_DRAW_CALLS) break
221+
const { depth, patterns } = iteratorResult.value
222+
223+
drawScreenCalls += patterns.length
224+
225+
// Draw screen
226+
ctx.strokeStyle = COLORS[Math.min(COLORS.length - 1, depth)]!
227+
ctx.beginPath()
228+
229+
for (const absolutePattern of patterns) {
230+
const viewportPattern = mapPatternToViewportSpace(absolutePattern, screenSize)
231+
const [p1, p2, p3, p4] = getPatternPoints(viewportPattern)
232+
233+
ctx.moveTo(...p1)
234+
ctx.lineTo(...p2)
235+
ctx.lineTo(...p3)
236+
ctx.lineTo(...p4)
237+
ctx.closePath()
238+
}
239+
240+
ctx.fill()
241+
ctx.stroke()
242+
243+
if (iterations === 1) {
244+
// On the first iteration, draw until passing both MIN_DEPTH and MAX_DRAW_CALLS.
245+
// We don't want to use a time-based limit here because it would cause flickering.
246+
if (depth > MIN_DEPTH && drawScreenCalls >= MAX_DRAW_CALLS) {
247+
break
248+
}
249+
} else {
250+
// On subsequent iterations, draw until MAX_DRAW_TIME_MS is reached.
251+
if (performance.now() - start > MAX_DRAW_TIME_MS) {
252+
break
253+
}
254+
}
193255
}
194256
})
195257

src/queue.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ export class Queue<T> {
3232

3333
/**
3434
* Compact the queue to make sure it doesn't grow indefinitely.
35-
* Must be triggered manually.
36-
* This is a naive approach and could be improved.
37-
*
38-
* We could try using a circular buffer instead.
3935
*/
4036
compact() {
4137
this.#queue = this.#queue.slice(this.#head)

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"compilerOptions": {
33
"target": "ES2020",
44
"useDefineForClassFields": true,
5-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
5+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
66
"module": "ESNext",
77
"skipLibCheck": true,
88

0 commit comments

Comments
 (0)