Skip to content

Commit

Permalink
feat: render on worker thread pool (#2604)
Browse files Browse the repository at this point in the history
  • Loading branch information
Julusian authored Oct 15, 2023
1 parent cccc183 commit c332d72
Show file tree
Hide file tree
Showing 15 changed files with 220 additions and 112 deletions.
2 changes: 1 addition & 1 deletion lib/Controls/Controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ class ControlsController extends CoreBase {
// Send the preview image shortly after
const location = this.page.getLocationOfControlId(controlId)
if (location) {
const img = this.graphics.getBank(location)
const img = this.graphics.getCachedRenderOrGeneratePlaceholder(location)
// TODO - rework this to use the shared render cache concept
client.emit(`controls:preview-${controlId}`, img?.asDataUrl)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/Data/ImportExport.js
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ class DataImportExport extends CoreBase {
return [null, clientObject]
})

client.onPromise('loadsave:control-preview', (location) => {
client.onPromise('loadsave:control-preview', async (location) => {
const importObject = client.pendingImport?.object

const controlObj =
Expand All @@ -569,7 +569,7 @@ class DataImportExport extends CoreBase {
: importObject?.pages?.[location.pageNumber]?.controls?.[location.row]?.[location.column]

if (controlObj) {
const res = this.graphics.drawPreview({
const res = await this.graphics.drawPreview({
...controlObj.style,
style: controlObj.type,
})
Expand Down
221 changes: 132 additions & 89 deletions lib/Graphics/Controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import GraphicsRenderer from './Renderer.js'
import CoreBase from '../Core/Base.js'
import { formatLocation, xyToOldBankIndex } from '../Shared/ControlId.js'
import { ImageResult } from './ImageWrapper.js'
import ImageWriteQueue from '../Resources/ImageWriteQueue.js'
import workerPool from 'workerpool'
import { isPackaged } from '../Resources/Util.js'
import { fileURLToPath } from 'url'
import path from 'path'

class GraphicsController extends CoreBase {
/**
Expand All @@ -43,6 +48,23 @@ class GraphicsController extends CoreBase {
*/
#renderLRUCache = new LRUCache({ max: 100 })

#renderQueue

#pool = workerPool.pool(
isPackaged() ? path.join(__dirname, './RenderThread.js') : fileURLToPath(new URL('./Thread.js', import.meta.url)),
{
minWorkers: 2,
maxWorkers: 6,
workerType: 'thread',
onCreateWorker: () => {
this.logger.info('Render worker created')
},
onTerminateWorker: () => {
this.logger.info('Render worker terminated')
},
}
)

constructor(registry) {
super(registry, 'graphics', 'Graphics/Controller')

Expand All @@ -52,6 +74,88 @@ class GraphicsController extends CoreBase {
remove_topbar: this.userconfig.getKey('remove_topbar'),
}

this.#renderQueue = new ImageWriteQueue(
this.logger,
async (_id, location, skipInvalidation) => {
try {
const gridSize = this.userconfig.getKey('gridSize')
const locationIsInBounds =
location &&
gridSize &&
location.column <= gridSize.maxColumn &&
location.column >= gridSize.minColumn &&
location.row <= gridSize.maxRow &&
location.row >= gridSize.minRow

const controlId = this.page.getControlIdAt(location)
const control = this.controls.getControl(controlId)
const buttonStyle = control?.getDrawStyle?.()

let render
if (location && locationIsInBounds && buttonStyle && buttonStyle.style) {
// Update the internal b_text_1_4 variable
const variableValue = buttonStyle?.text
if (location) {
setImmediate(() => {
const values = {
[`b_text_${location.pageNumber}_${location.row}_${location.column}`]: variableValue,
}

const bankIndex = xyToOldBankIndex(location.column, location.row)
if (bankIndex) values[`b_text_${location.pageNumber}_${bankIndex}`] = variableValue

this.instance.variable.setVariableValues('internal', values)
})
}

const pagename = this.page.getPageName(location.pageNumber)

// Check if the image is already present in the render cache and if so, return it
const globalShowTopBar = !this.#drawOptions.remove_topbar && buttonStyle.show_topbar === 'default'
const keyLocation = globalShowTopBar || buttonStyle.show_topbar === true ? location : undefined
const key = JSON.stringify({ options: this.#drawOptions, buttonStyle, keyLocation, pagename })
render = this.#renderLRUCache.get(key)

if (!render) {
const { buffer, draw_style } = await this.#pool.exec('drawBankImage', [
this.#drawOptions,
buttonStyle,
location,
pagename,
])
render = GraphicsRenderer.wrapDrawBankImage(buffer, draw_style, buttonStyle)
}
} else {
render = GraphicsRenderer.drawBlank(this.#drawOptions, location)
}

// Only cache the render, if it is within the valid bounds
if (locationIsInBounds && location) {
let pageCache = this.#renderCache.get(location.pageNumber)
if (!pageCache) {
pageCache = new Map()
this.#renderCache.set(location.pageNumber, pageCache)
}

let rowCache = pageCache.get(location.row)
if (!rowCache) {
rowCache = new Map()
pageCache.set(location.row, rowCache)
}

rowCache.set(location.column, render)
}

if (!skipInvalidation) {
this.emit('button_drawn', location, render)
}
} catch (e) {
this.logger.warn(`drawBankImage failed: ${e}`)
}
},
5
) // TODO - dynamic limit

FontLibrary.reset()
FontLibrary.use({
'Companion-sans': ['assets/Fonts/Arimo-Regular.ttf'],
Expand Down Expand Up @@ -84,10 +188,11 @@ class GraphicsController extends CoreBase {

/**
* Draw a preview of a button
* @param {object} buttonConfig
* @param {object} buttonStyle
*/
drawPreview(buttonConfig) {
return GraphicsRenderer.drawBankImage(this.#drawOptions, buttonConfig)
async drawPreview(buttonStyle) {
const { buffer, draw_style } = await this.#pool.exec('drawBankImage', [this.#drawOptions, buttonStyle])
return GraphicsRenderer.wrapDrawBankImage(buffer, draw_style, buttonStyle)
}

/**
Expand Down Expand Up @@ -122,91 +227,26 @@ class GraphicsController extends CoreBase {
}

invalidateButton(location) {
this.#drawAndCacheButton(location, true)
this.#drawAndCacheButton(location)
}

/**
* Regenerate every button image
* @param {boolean} emitInvalidation whether to report invalidations of each button
* @param {boolean=} skipInvalidation whether to skip reporting invalidations of each button
* @access private
*/
regenerateAll(emitInvalidation) {
regenerateAll(skipInvalidation = false) {
for (let pageNumber = 1; pageNumber <= 99; pageNumber++) {
const populatedLocations = this.page.getAllPopulatedLocationsOnPage(pageNumber)
for (const location of populatedLocations) {
this.#drawAndCacheButton(location, emitInvalidation)
this.#drawAndCacheButton(location, skipInvalidation)
}
}
}

#drawAndCacheButton(location, emitInvalidation) {
const gridSize = this.userconfig.getKey('gridSize')
const locationIsInBounds =
location &&
gridSize &&
location.column <= gridSize.maxColumn &&
location.column >= gridSize.minColumn &&
location.row <= gridSize.maxRow &&
location.row >= gridSize.minRow

const controlId = this.page.getControlIdAt(location)
const control = this.controls.getControl(controlId)
const buttonStyle = control?.getDrawStyle?.()

let render
if (location && locationIsInBounds && buttonStyle && buttonStyle.style) {
// Update the internal b_text_1_4 variable
const variableValue = buttonStyle?.text
if (location) {
setImmediate(() => {
const values = {
[`b_text_${location.pageNumber}_${location.row}_${location.column}`]: variableValue,
}

const bankIndex = xyToOldBankIndex(location.column, location.row)
if (bankIndex) values[`b_text_${location.pageNumber}_${bankIndex}`] = variableValue

this.instance.variable.setVariableValues('internal', values)
})
}

const pagename = this.page.getPageName(location.pageNumber)

// Check if the image is already present in the render cache and if so, return it
const globalShowTopBar = !this.#drawOptions.remove_topbar && buttonStyle.show_topbar === 'default'
const keyLocation = globalShowTopBar || buttonStyle.show_topbar === true ? location : undefined
const key = JSON.stringify({ options: this.#drawOptions, buttonStyle, keyLocation, pagename })
render = this.#renderLRUCache.get(key)

if (!render) {
render = GraphicsRenderer.drawBankImage(this.#drawOptions, buttonStyle, location, pagename)
}
} else {
render = GraphicsRenderer.drawBlank(this.#drawOptions, location)
}

// Only cache the render, if it is within the valid bounds
if (locationIsInBounds && location) {
let pageCache = this.#renderCache.get(location.pageNumber)
if (!pageCache) {
pageCache = new Map()
this.#renderCache.set(location.pageNumber, pageCache)
}

let rowCache = pageCache.get(location.row)
if (!rowCache) {
rowCache = new Map()
pageCache.set(location.row, rowCache)
}

rowCache.set(location.column, render)
}

if (emitInvalidation) {
this.emit('button_drawn', location, render)
}

return render
#drawAndCacheButton(location, skipInvalidation = false) {
const id = `${location.pageNumber}_${location.row}_${location.column}`
this.#renderQueue.queue(id, location, skipInvalidation)
}

/**
Expand Down Expand Up @@ -252,22 +292,25 @@ class GraphicsController extends CoreBase {
}
}

getBank(location) {
let render = this.#renderCache.get(location.pageNumber)?.get(location.row)?.get(location.column)
if (render) return render

render = this.#drawAndCacheButton(location)
if (render) {
this.emit('button_drawn', location, render)
return render
}
/**
* Get the cached render of a button
* @param {object} location
* @returns
*/
getCachedRender(location) {
return this.#renderCache.get(location.pageNumber)?.get(location.row)?.get(location.column)
}

this.logger.silly(
`!!!! ERROR: UNEXPECTED ERROR while fetching image for unbuffered button: ${formatLocation(location)}`
)
/**
* Get the cached render of a button, or generate a placeholder it if is missing
* @param {object} location
* @returns
*/
getCachedRenderOrGeneratePlaceholder(location) {
const render = this.#renderCache.get(location.pageNumber)?.get(location.row)?.get(location.column)
if (render) return render

// continue gracefully, even though something is terribly wrong
return new ImageResult(Buffer.alloc(72 * 72 * 3))
return GraphicsRenderer.drawBlank(this.#drawOptions, location)
}
}

Expand Down
16 changes: 12 additions & 4 deletions lib/Graphics/Image.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ import { Canvas, ImageData } from '@julusian/skia-canvas'
import LogController from '../Log/Controller.js'
import { PNG } from 'pngjs'

async function pngParse(pngData) {
return new Promise((resolve, reject) => {
new PNG().parse(pngData, (err, data) => {
if (err) reject(err)
else resolve(data)
})
})
}

/**
* Class for generating an image and rendering some content to it
*/
Expand Down Expand Up @@ -183,9 +192,9 @@ class Image {
* @param {'left'|'center'|'right'} halign horizontal alignment of the image in the bounding box (defaults to center)
* @param {'top'|'center'|'bottom'} valign vertical alignment of the image in the bounding box (defaults to center)
* @param {number|'crop'|'fill'|'fit'} scale the size factor of the image. Number scales by specified amount, fill scales to fill the bounding box neglecting aspect ratio, crop scales to fill the bounding box and crop if necessary, fit scales to fit the bounding box with the longer side
* @returns void
* @returns {Promise<void>}
*/
drawFromPNGdata(
async drawFromPNGdata(
data,
xStart = 0,
yStart = 0,
Expand All @@ -195,9 +204,8 @@ class Image {
valign = 'center',
scale = 1
) {
let png
let png = await pngParse(data)

png = PNG.sync.read(data)
let imageWidth = png.width
let imageHeight = png.height

Expand Down
6 changes: 3 additions & 3 deletions lib/Graphics/Preview.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class GraphicsPreview extends CoreBase {

client.join(PreviewLocationRoom(location))

const render = this.graphics.getBank(location)
const render = this.graphics.getCachedRenderOrGeneratePlaceholder(location)
return { image: render.asDataUrl, isUsed: !!render.style }
})
client.onPromise('preview:location:unsubscribe', (location, subId) => {
Expand Down Expand Up @@ -112,7 +112,7 @@ class GraphicsPreview extends CoreBase {
client,
})

return result.location ? this.graphics.getBank(result.location).asDataUrl : null
return result.location ? this.graphics.getCachedRenderOrGeneratePlaceholder(result.location).asDataUrl : null
})
client.onPromise('preview:button-reference:unsubscribe', (id) => {
const fullId = `${client.id}::${id}`
Expand Down Expand Up @@ -188,7 +188,7 @@ class GraphicsPreview extends CoreBase {

previewSession.client.emit(
`preview:button-reference:update:${previewSession.id}`,
this.graphics.getBank(result.location).asDataUrl
this.graphics.getCachedRenderOrGeneratePlaceholder(result.location).asDataUrl
)
}
}
Expand Down
Loading

0 comments on commit c332d72

Please sign in to comment.