cvmts: replace jpeg-turbo native module with new rust module

This module also does threadpooling internally, so we don't need Piscina anymore (which I'm pretty sure was actually bottlenecking.)
This commit is contained in:
modeco80
2024-06-20 03:20:56 -04:00
parent 39521a4b1d
commit 87a377a10f
17 changed files with 632 additions and 555 deletions

View File

@@ -11,12 +11,11 @@
"author": "Elijah R, modeco80",
"license": "GPL-3.0",
"dependencies": {
"@computernewb/jpeg-turbo": "*",
"@cvmts/guac-rs": "*",
"@cvmts/jpegturbo-rs": "*",
"@cvmts/qemu": "*",
"execa": "^8.0.1",
"mnemonist": "^0.39.5",
"piscina": "^4.4.0",
"sharp": "^0.33.3",
"toml": "^3.0.0",
"ws": "^8.14.1"

View File

@@ -810,7 +810,7 @@ export default class CollabVMServer {
let display = this.VM.GetDisplay();
let displaySize = display.Size();
let encoded = await JPEGEncoder.EncodeJpeg(display.Buffer(), displaySize, rect);
let encoded = await JPEGEncoder.Encode(display.Buffer(), displaySize, rect);
return encoded.toString('base64');
}

View File

@@ -1,59 +1,52 @@
import path from 'node:path';
import Piscina from 'piscina';
import { Size, Rect } from '@cvmts/shared';
const kMaxJpegThreads = 4;
const kIdleTimeout = 25000;
// Thread pool for doing JPEG encoding for rects.
const TheJpegEncoderPool = new Piscina({
filename: path.join(import.meta.dirname + '/JPEGEncoderWorker.js'),
idleTimeout: kIdleTimeout,
maxThreads: kMaxJpegThreads
});
const TheThumbnailEncoderPool = new Piscina({
filename: path.join(import.meta.dirname + '/ThumbnailJPEGEncoderWorker.js'),
idleTimeout: kIdleTimeout,
maxThreads: kMaxJpegThreads
});
import sharp from 'sharp';
import * as jpeg from '@cvmts/jpegturbo-rs';
// A good balance. TODO: Configurable?
let gJpegQuality = 35;
export class JPEGEncoder {
const kThumbnailSize: Size = {
width: 400,
height: 300
};
// this returns appropiate Sharp options to deal with CVMTS raw framebuffers
// (which are RGBA bitmaps, essentially. We probably should abstract that out but
// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds)
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
return {
width: size.width,
height: size.height,
channels: 4
};
}
export class JPEGEncoder {
static SetQuality(quality: number) {
gJpegQuality = quality;
}
static async EncodeJpeg(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
static async Encode(canvas: Buffer, displaySize: Size, rect: Rect): Promise<Buffer> {
let offset = (rect.y * displaySize.width + rect.x) * 4;
let res = await TheJpegEncoderPool.run({
buffer: canvas.subarray(offset),
return jpeg.jpegEncode({
width: rect.width,
height: rect.height,
stride: displaySize.width,
quality: gJpegQuality
buffer: canvas.subarray(offset)
});
// TODO: There's probably (definitely) a better way to fix this
if (res == undefined) return Buffer.from([]);
// have to manually turn it back into a buffer because
// Piscina for some reason turns it into a Uint8Array
return Buffer.from(res);
}
static async EncodeThumbnail(buffer: Buffer, size: Size) : Promise<Buffer> {
let res = await TheThumbnailEncoderPool.run({
buffer: buffer,
size: size,
quality: gJpegQuality
});
static async EncodeThumbnail(buffer: Buffer, size: Size): Promise<Buffer> {
let { data, info } = await sharp(buffer, { raw: GetRawSharpOptions(size) })
.resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' })
.raw()
.toBuffer({ resolveWithObject: true });
return Buffer.from(res)
return jpeg.jpegEncode({
width: kThumbnailSize.width,
height: kThumbnailSize.height,
stride: kThumbnailSize.width,
buffer: data
});
}
}

View File

@@ -1,19 +0,0 @@
import jpegTurbo from '@computernewb/jpeg-turbo';
import Piscina from 'piscina';
export default async (opts: any) => {
try {
let res = await jpegTurbo.compress(opts.buffer, {
format: jpegTurbo.FORMAT_RGBA,
width: opts.width,
height: opts.height,
subsampling: jpegTurbo.SAMP_422,
stride: opts.stride,
quality: opts.quality || 75
});
return Piscina.move(res);
} catch {
return;
}
};

View File

@@ -1,36 +0,0 @@
import { Size } from '@cvmts/shared';
import Piscina from 'piscina';
import sharp from 'sharp';
const kThumbnailSize: Size = {
width: 400,
height: 300
};
// this returns appropiate Sharp options to deal with CVMTS raw framebuffers
// (which are RGBA bitmaps, essentially. We probably should abstract that out but
// that'd mean having to introduce that to rfb and oihwekjtgferklds;./tghnredsltg;erhds)
function GetRawSharpOptions(size: Size): sharp.CreateRaw {
return {
width: size.width,
height: size.height,
channels: 4
};
}
export default async (opts: any) => {
try {
let out = await sharp(opts.buffer, { raw: GetRawSharpOptions(opts.size) })
.resize(kThumbnailSize.width, kThumbnailSize.height, { fit: 'fill' })
.jpeg({
quality: opts.quality || 75
})
.toFormat('jpeg')
.toBuffer();
return out;
} catch {
return;
}
};