offscreen render

This commit is contained in:
Andrey Kolosov
2019-07-31 18:00:00 +03:00
parent 9d2d3b6089
commit 9d3dc826f7
4 changed files with 2825 additions and 3227 deletions

5828
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -129,36 +129,7 @@ class App extends React.Component {
}
}
onRender({images, text, clip, belt}) {
const ctx = this.renderer;
if (!ctx) {
return;
}
for (let {x, y, w, h, image, data} of images) {
if (!image) {
image = ctx.createImageData(w, h);
image.data.set(data);
}
ctx.putImageData(image, x, y);
}
if (text.length) {
ctx.save();
ctx.font = 'bold 13px Times New Roman';
if (clip) {
const {x0, y0, x1, y1} = clip;
ctx.beginPath();
ctx.rect(x0, y0, x1 - x0, y1 - y0);
ctx.clip();
}
for (let {x, y, text: str, color} of text) {
const r = ((color >> 16) & 0xFF);
const g = ((color >> 8) & 0xFF);
const b = (color & 0xFF);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillText(str, x, y + 22);
}
ctx.restore();
}
updateBelt(belt) {
if (belt) {
const used = new Set();
let pos = 3;
@@ -185,7 +156,6 @@ class App extends React.Component {
document.removeEventListener("dragleave", this.onDragLeave, true);
this.setState({dropping: 0});
this.renderer = this.canvas.getContext("2d", {alpha: false});
this.setState({loading: true});
load_game(this, file).then(game => {

View File

@@ -6,6 +6,7 @@ import SpawnModule from './DiabloSpawn.jscc';
/* eslint-disable-next-line no-restricted-globals */
const worker = self;
let canvas = null, context = null;
let files = null;
let renderBatch = null;
let drawBelt = null;
@@ -15,38 +16,6 @@ const DApi = {
worker.postMessage({action: "error", error});
},
draw_begin() {
renderBatch = {
images: [],
text: [],
clip: null,
belt: drawBelt,
};
drawBelt = null;
},
draw_blit(x, y, w, h, data) {
if (ImageData.length) {
const image = new ImageData(w, h);
image.data.set(data);
renderBatch.images.push({x, y, image});
} else {
renderBatch.images.push({x, y, w, h, data: data.slice});
}
},
draw_clip_text(x0, y0, x1, y1) {
renderBatch.clip = {x0, y0, x1, y1};
},
draw_text(x, y, text, color) {
renderBatch.text.push({x, y, text, color});
},
draw_end() {
worker.postMessage({action: "render", batch: renderBatch});
renderBatch = null;
},
draw_belt(items) {
drawBelt = items.slice();
},
get_file_size(path) {
const data = files.get(path.toLowerCase());
return data ? data.byteLength : 0;
@@ -82,16 +51,92 @@ const DApi = {
},
};
let audioBatch = null;
const DApi_renderLegacy = {
draw_begin() {
renderBatch = {
images: [],
text: [],
clip: null,
belt: drawBelt,
};
drawBelt = null;
},
draw_blit(x, y, w, h, data) {
renderBatch.images.push({x, y, w, h, data: data.slice()});
},
draw_clip_text(x0, y0, x1, y1) {
renderBatch.clip = {x0, y0, x1, y1};
},
draw_text(x, y, text, color) {
renderBatch.text.push({x, y, text, color});
},
draw_end() {
const transfer = renderBatch.images.map(({data}) => data.buffer);
if (renderBatch.belt) {
transfer.push(renderBatch.belt.buffer);
}
worker.postMessage({action: "render", batch: renderBatch}, transfer);
renderBatch = null;
},
draw_belt(items) {
drawBelt = items.slice();
},
};
const DApi_renderOffscreen = {
draw_begin() {
context.save();
context.font = 'bold 13px Times New Roman';
},
draw_blit(x, y, w, h, data) {
const image = context.createImageData(w, h);
image.data.set(data);
context.putImageData(image, x, y);
},
draw_clip_text(x0, y0, x1, y1) {
context.beginPath();
context.rect(x0, y0, x1 - x0, y1 - y0);
context.clip();
},
draw_text(x, y, text, color) {
const r = ((color >> 16) & 0xFF);
const g = ((color >> 8) & 0xFF);
const b = (color & 0xFF);
context.fillStyle = `rgb(${r}, ${g}, ${b})`;
context.fillText(text, x, y + 22);
},
draw_end() {
context.restore();
const bitmap = canvas.transferToImageBitmap();
const transfer = [bitmap];
if (drawBelt) {
transfer.push(drawBelt.buffer);
}
worker.postMessage({action: "render", batch: {bitmap, belt: drawBelt}}, transfer);
drawBelt = null;
},
draw_belt(items) {
drawBelt = items.slice();
},
};
let audioBatch = null, audioTransfer = null;
let maxSoundId = 0, maxBatchId = 0;
["create_sound", "duplicate_sound"].forEach(func => {
DApi[func] = function(...params) {
if (audioBatch) {
maxBatchId = params[0] + 1;
audioBatch.push({func, params});
if (func === "create_sound") {
audioTransfer.push(params[1].buffer);
}
} else {
maxSoundId = params[0] + 1;
worker.postMessage({action: "audio", func, params});
const transfer = [];
if (func === "create_sound") {
transfer.push(params[1].buffer);
}
worker.postMessage({action: "audio", func, params}, transfer);
}
};
});
@@ -111,15 +156,17 @@ let wasm = null;
function call_api(func, ...params) {
audioBatch = [];
audioTransfer = [];
wasm["_" + func](...params);
if (audioBatch.length) {
maxSoundId = maxBatchId;
worker.postMessage({action: "audioBatch", batch: audioBatch});
worker.postMessage({action: "audioBatch", batch: audioBatch}, audioTransfer);
audioBatch = null;
audioTransfer = null;
}
}
async function init_game(mpq) {
async function init_game(mpq, offscreen) {
if (mpq) {
/* eslint-disable-next-line no-undef */
const reader = new FileReaderSync();
@@ -127,6 +174,14 @@ async function init_game(mpq) {
files.set('diabdat.mpq', new Uint8Array(data));
}
if (offscreen) {
canvas = new OffscreenCanvas(640, 480);
context = canvas.getContext("2d");
Object.assign(DApi, DApi_renderOffscreen);
} else {
Object.assign(DApi, DApi_renderLegacy);
}
wasm = await (mpq ? DiabloModule : SpawnModule)({
locateFile(name) {
if (name === 'DiabloSpawn.wasm') {
@@ -142,7 +197,7 @@ async function init_game(mpq) {
wasm._DApi_Init(Math.floor(performance.now()));
setInterval(() => {
call_api("DApi_Render", Math.floor(performance.now()));
call_api("DApi_Render", Math.floor(performance.now()));
}, 50);
}
@@ -150,9 +205,9 @@ worker.addEventListener("message", ({data}) => {
switch (data.action) {
case "init":
files = data.files;
init_game(data.mpq).then(
init_game(data.mpq, data.offscreen).then(
() => worker.postMessage({action: "loaded"}),
e => worker.postMessage({action: "failed", error: e.message}));
e => {debugger;worker.postMessage({action: "failed", error: e.message || e.name});});
break;
case "event":
call_api(data.func, ...data.params);

View File

@@ -2,6 +2,49 @@ import Worker from './game.worker.js';
import init_sound from './sound';
import load_spawn from './load_spawn';
function onRender(api, ctx, {bitmap, images, text, clip, belt}) {
if (bitmap) {
ctx.transferFromImageBitmap(bitmap);
} else {
for (let {x, y, w, h, data} of images) {
const image = ctx.createImageData(w, h);
image.data.set(data);
ctx.putImageData(image, x, y);
}
if (text.length) {
ctx.save();
ctx.font = 'bold 13px Times New Roman';
if (clip) {
const {x0, y0, x1, y1} = clip;
ctx.beginPath();
ctx.rect(x0, y0, x1 - x0, y1 - y0);
ctx.clip();
}
for (let {x, y, text: str, color} of text) {
const r = ((color >> 16) & 0xFF);
const g = ((color >> 8) & 0xFF);
const b = (color & 0xFF);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillText(str, x, y + 22);
}
ctx.restore();
}
}
api.updateBelt(belt);
}
function testOffscreen() {
try {
const canvas = document.createElement("canvas");
const offscreen = canvas.transferControlToOffscreen();
const context = offscreen.getContext("2d");
return context != null;
} catch (e) {
return false;
}
}
async function do_load_game(api, audio, mpq) {
const fs = await api.fs;
if (mpq) {
@@ -9,6 +52,14 @@ async function do_load_game(api, audio, mpq) {
} else {
await load_spawn(api, fs);
}
let context = null, offscreen = false;
if (testOffscreen()) {
context = api.canvas.getContext("bitmaprenderer");
offscreen = true;
} else {
context = api.canvas.getContext("2d", {alpha: false});
}
return await new Promise((resolve, reject) => {
try {
const worker = new Worker();
@@ -18,7 +69,7 @@ async function do_load_game(api, audio, mpq) {
resolve((func, ...params) => worker.postMessage({action: "event", func, params}));
break;
case "render":
api.onRender(data.batch);
onRender(api, context, data.batch);
break;
case "audio":
audio[data.func](...data.params);
@@ -46,7 +97,11 @@ async function do_load_game(api, audio, mpq) {
default:
}
});
worker.postMessage({action: "init", files: fs.files, mpq});
const transfer= [];
for (let [name, file] of fs.files) {
transfer.push(file.buffer);
}
worker.postMessage({action: "init", files: fs.files, mpq, offscreen}, transfer);
delete fs.files;
} catch (e) {
reject(e);