perf: assets

This commit is contained in:
cha0s 2024-06-27 13:56:43 -05:00
parent c339491590
commit b12183a6ee
14 changed files with 195 additions and 150 deletions

View File

@ -248,6 +248,11 @@ export default class Sandbox {
} }
} }
reset() {
this.generator = undefined;
this.compile();
}
run(ops = 1000) { run(ops = 1000) {
let result; let result;
for (let i = 0; i < ops; ++i) { for (let i = 0; i < ops; ++i) {
@ -265,8 +270,7 @@ export default class Sandbox {
} }
const result = this.generator.next(); const result = this.generator.next();
if (result.done) { if (result.done) {
this.generator = undefined; this.reset();
this.compile();
} }
return result; return result;
} }

25
app/context/assets.js Normal file
View File

@ -0,0 +1,25 @@
import {Assets} from '@pixi/assets';
import {createContext, useContext, useEffect} from 'react';
const context = createContext();
export default context;
const loading = {};
export function useAsset(source) {
const [assets, setAssets] = useContext(context);
useEffect(() => {
if (!assets[source]) {
if (!loading[source]) {
(loading[source] = Assets.load(source)).then((asset) => {
setAssets((assets) => ({
...assets,
[source]: asset,
}));
});
}
}
}, [assets, setAssets, source]);
return assets[source];
}

View File

@ -43,57 +43,60 @@ export default class Inventory extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
const Component = this; const Component = this;
Instance.prototype.item = async function (slot) { return class InventoryInstance extends Instance {
const {slots} = this; async item(slot) {
if (!(slot in slots)) { const {slots} = this;
return undefined; if (!(slot in slots)) {
return undefined;
}
const {readAsset} = Component.ecs;
const chars = await readAsset([slots[slot].source, 'item.json'].join('/'))
const json = chars.byteLength > 0
? JSON.parse(
(new TextDecoder()).decode(chars),
)
: {};
const item = {
...slots[slot],
...json,
};
const instance = this;
const proxy = new Proxy(item, {
set(target, property, value) {
slots[slot][property] = value;
if ('qty' === property && value <= 0) {
Component.markChange(instance.entity, 'slotChange', {[slot]: false});
delete slots[slot];
}
else {
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}});
}
return true;
},
});
return proxy;
} }
const {readAsset} = Component.ecs; swapSlots(l, r) {
const json = await ( const {slots} = this;
readAsset([slots[slot].source, 'item.json'].join('/')) const tmp = slots[l];
.then((response) => (response.ok ? response.json() : {})) const change = {};
); if (slots[r]) {
const item = { change[l] = slots[l] = slots[r];
...slots[slot], }
...json, else {
}; change[l] = false;
const instance = this; delete slots[l];
const proxy = new Proxy(item, { }
set(target, property, value) { if (tmp) {
slots[slot][property] = value; change[r] = slots[r] = tmp;
if ('qty' === property && value <= 0) { }
Component.markChange(instance.entity, 'slotChange', {[slot]: false}); else {
delete slots[slot]; change[r] = false;
} delete slots[r];
else { }
Component.markChange(instance.entity, 'slotChange', {[slot]: {[property]: value}}); Component.markChange(this.entity, 'slotChange', change);
}
return true;
},
});
return proxy;
};
Instance.prototype.swapSlots = function(l, r) {
const {slots} = this;
const tmp = slots[l];
const change = {};
if (slots[r]) {
change[l] = slots[l] = slots[r];
} }
else { }
change[l] = false;
delete slots[l];
}
if (tmp) {
change[r] = slots[r] = tmp;
}
else {
change[r] = false;
delete slots[r];
}
Component.markChange(this.entity, 'slotChange', change);
};
return Instance;
} }
static properties = { static properties = {
slots: { slots: {

View File

@ -4,34 +4,32 @@ export default class Ticking extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
Instance.prototype.$$finished = []; return class TickingInstance extends Instance {
Instance.prototype.$$tickingPromises = [];
Instance.prototype.addTickingPromise = function(tickingPromise) { $$finished = [];
this.$$tickingPromises.push(tickingPromise); $$tickingPromises = [];
tickingPromise.then(() => {
this.$$finished.push(tickingPromise); addTickingPromise(tickingPromise) {
}); this.$$tickingPromises.push(tickingPromise);
tickingPromise.then(() => {
this.$$finished.push(tickingPromise);
});
}
tick(elapsed) {
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) {
tickingPromise.tick(elapsed);
}
}
} }
Instance.prototype.tick = function(elapsed) {
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
for (const tickingPromise of this.$$tickingPromises) {
tickingPromise.tick(elapsed);
}
for (const tickingPromise of this.$$finished) {
this.$$tickingPromises.splice(
this.$$tickingPromises.indexOf(tickingPromise),
1,
);
}
this.$$finished = [];
}
return Instance;
} }
static properties = { static properties = {
isTicking: {defaultValue: 1, type: 'uint8'}, isTicking: {defaultValue: 1, type: 'uint8'},

View File

@ -37,53 +37,54 @@ export default class TileLayers extends Component {
instanceFromSchema() { instanceFromSchema() {
const Instance = super.instanceFromSchema(); const Instance = super.instanceFromSchema();
const Component = this; const Component = this;
Instance.prototype.layer = function (index) { return class TileLayersInstance extends Instance {
const {layers} = this; layer(index) {
if (!(index in layers)) { const {layers} = this;
return undefined; if (!(index in layers)) {
} return undefined;
const instance = this;
class LayerProxy {
constructor(layer) {
this.layer = layer;
} }
get area() { const instance = this;
return this.layer.area; class LayerProxy {
} constructor(layer) {
get source() { this.layer = layer;
return this.layer.source; }
} get area() {
stamp(at, data) { return this.layer.area;
const changes = {}; }
for (const row in data) { get source() {
const columns = data[row]; return this.layer.source;
for (const column in columns) { }
const tile = columns[column]; stamp(at, data) {
const x = at.x + parseInt(column); const changes = {};
const y = at.y + parseInt(row); for (const row in data) {
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) { const columns = data[row];
continue; for (const column in columns) {
const tile = columns[column];
const x = at.x + parseInt(column);
const y = at.y + parseInt(row);
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
continue;
}
const calculated = y * this.layer.area.x + x;
this.layer.data[calculated] = tile;
changes[calculated] = tile;
} }
const calculated = y * this.layer.area.x + x;
this.layer.data[calculated] = tile;
changes[calculated] = tile;
} }
Component.markChange(instance.entity, 'layerChange', {[index]: changes});
} }
Component.markChange(instance.entity, 'layerChange', {[index]: changes}); tile({x, y}) {
} if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) {
tile({x, y}) { return undefined;
if (x < 0 || y < 0 || x >= this.layer.area.x || y >= this.layer.area.y) { }
return undefined; return this.layer.data[y * this.layer.area.x + x];
}
get tileSize() {
return this.layer.tileSize;
} }
return this.layer.data[y * this.layer.area.x + x];
}
get tileSize() {
return this.layer.tileSize;
} }
return new LayerProxy(layers[index]);
} }
return new LayerProxy(layers[index]); }
};
return Instance;
} }
static properties = { static properties = {
layers: { layers: {

View File

@ -71,15 +71,14 @@ export default class Wielder extends Component {
const activeItem = await this.activeItem(); const activeItem = await this.activeItem();
if (activeItem) { if (activeItem) {
ecs.readAsset([activeItem.source, state ? 'start.js' : 'stop.js'].join('/')) ecs.readAsset([activeItem.source, state ? 'start.js' : 'stop.js'].join('/'))
.then((script) => (script.ok ? script.text() : ''))
.then((code) => { .then((code) => {
if (code) { if (code.byteLength > 0) {
const context = { const context = {
ecs, ecs,
item: activeItem, item: activeItem,
wielder: entity, wielder: entity,
}; };
Ticking.addTickingPromise(Script.tickingPromise(code, context)); Ticking.addTickingPromise(Script.tickingPromise((new TextDecoder()).decode(code), context));
} }
}); });
} }

View File

@ -1,15 +0,0 @@
import {Assets} from '@pixi/assets';
import {useEffect, useState} from 'react';
export default function useAsset(source) {
const [asset, setAsset] = useState();
useEffect(() => {
if (Assets.cache.has(source)) {
setAsset(Assets.get(source));
}
else {
Assets.load(source).then(setAsset);
}
}, [setAsset, source]);
return asset;
}

View File

@ -14,7 +14,9 @@ class WorkerServer extends Server {
return ['UNIVERSE', path].join('/'); return ['UNIVERSE', path].join('/');
} }
async readAsset(path) { async readAsset(path) {
return fetch(path); return fetch(path).then((response) => (
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
));
} }
async readData(path) { async readData(path) {
const data = await get(this.constructor.qualify(path)); const data = await get(this.constructor.qualify(path));

View File

@ -6,6 +6,7 @@ import {BaseTexture} from '@pixi/core';
import {createElement, useContext} from 'react'; import {createElement, useContext} from 'react';
import {RESOLUTION} from '@/constants.js'; import {RESOLUTION} from '@/constants.js';
import AssetsContext from '@/context/assets.js';
import ClientContext from '@/context/client.js'; import ClientContext from '@/context/client.js';
import DebugContext from '@/context/debug.js'; import DebugContext from '@/context/debug.js';
import EcsContext from '@/context/ecs.js'; import EcsContext from '@/context/ecs.js';
@ -16,7 +17,7 @@ import styles from './pixi.module.css';
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST; BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
const Contexts = [ClientContext, DebugContext, EcsContext, MainEntityContext]; const Contexts = [AssetsContext, ClientContext, DebugContext, EcsContext, MainEntityContext];
const ContextBridge = ({children, render}) => { const ContextBridge = ({children, render}) => {
const contexts = Contexts.map(useContext); const contexts = Contexts.map(useContext);

View File

@ -1,6 +1,6 @@
import {Sprite as PixiSprite} from '@pixi/react'; import {Sprite as PixiSprite} from '@pixi/react';
import useAsset from '@/hooks/use-asset.js'; import {useAsset} from '@/context/assets.js';
export default function Sprite({entity}) { export default function Sprite({entity}) {
const asset = useAsset(entity.Sprite.source); const asset = useAsset(entity.Sprite.source);

View File

@ -2,7 +2,7 @@ import {PixiComponent} from '@pixi/react';
import '@pixi/spritesheet'; // NECESSARY! import '@pixi/spritesheet'; // NECESSARY!
import {CompositeTilemap} from '@pixi/tilemap'; import {CompositeTilemap} from '@pixi/tilemap';
import useAsset from '@/hooks/use-asset.js'; import {useAsset} from '@/context/assets.js';
const TileLayerInternal = PixiComponent('TileLayer', { const TileLayerInternal = PixiComponent('TileLayer', {
create: () => new CompositeTilemap(), create: () => new CompositeTilemap(),

View File

@ -2,6 +2,7 @@ import {json} from "@remix-run/node";
import {useEffect, useState} from 'react'; import {useEffect, useState} from 'react';
import {useOutletContext, useParams} from 'react-router-dom'; import {useOutletContext, useParams} from 'react-router-dom';
import AssetsContext from '@/context/assets.js';
import ClientContext from '@/context/client.js'; import ClientContext from '@/context/client.js';
import DebugContext from '@/context/debug.js'; import DebugContext from '@/context/debug.js';
import EcsContext from '@/context/ecs.js'; import EcsContext from '@/context/ecs.js';
@ -12,9 +13,28 @@ import Systems from '@/ecs-systems/index.js';
import Ui from '@/react-components/ui.jsx'; import Ui from '@/react-components/ui.jsx';
import {juggleSession} from '@/session.server'; import {juggleSession} from '@/session.server';
import {LRUCache} from 'lru-cache';
export const cache = new LRUCache({
max: 128,
});
class ClientEcs extends Ecs { class ClientEcs extends Ecs {
readAsset(uri) { readAsset(uri) {
return fetch(new URL(uri, window.location.origin)); if (!cache.has(uri)) {
let promise, resolve, reject;
promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
cache.set(uri, promise);
fetch(new URL(uri, window.location.origin))
.then(async (response) => {
resolve(response.ok ? response.arrayBuffer() : new ArrayBuffer(0));
})
.catch(reject);
}
return cache.get(uri);
} }
} }
@ -25,6 +45,7 @@ export async function loader({request}) {
export default function PlaySpecific() { export default function PlaySpecific() {
const Client = useOutletContext(); const Client = useOutletContext();
const assetsTuple = useState({});
const [client, setClient] = useState(); const [client, setClient] = useState();
const mainEntityTuple = useState(); const mainEntityTuple = useState();
const debugTuple = useState(false); const debugTuple = useState(false);
@ -102,7 +123,9 @@ export default function PlaySpecific() {
<MainEntityContext.Provider value={mainEntityTuple}> <MainEntityContext.Provider value={mainEntityTuple}>
<EcsContext.Provider value={ecsTuple}> <EcsContext.Provider value={ecsTuple}>
<DebugContext.Provider value={debugTuple}> <DebugContext.Provider value={debugTuple}>
<Ui disconnected={disconnected} /> <AssetsContext.Provider value={assetsTuple}>
<Ui disconnected={disconnected} />
</AssetsContext.Provider>
</DebugContext.Provider> </DebugContext.Provider>
</EcsContext.Provider> </EcsContext.Provider>
</MainEntityContext.Provider> </MainEntityContext.Provider>

View File

@ -69,16 +69,18 @@ export default class Script {
// } // }
// } // }
evaluateSync() {
this.sandbox.reset();
const {value: {value}} = this.sandbox.step();
return value;
}
static async fromCode(code, context = {}) { static async fromCode(code, context = {}) {
let ast; if (!cache.has(code)) {
if (cache.has(code)) { cache.set(code, this.parse(code));
ast = cache.get(code);
}
else {
cache.set(code, ast = await this.parse(code));
} }
return new this( return new this(
new Sandbox(ast, this.createContext(context)), new Sandbox(await cache.get(code), this.createContext(context)),
); );
} }

View File

@ -53,7 +53,9 @@ class SocketServer extends Server {
if ('production' === process.env.NODE_ENV) { if ('production' === process.env.NODE_ENV) {
url.protocol = 'http:'; url.protocol = 'http:';
} }
return fetch(url.href); return fetch(url.href).then((response) => (
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
));
} }
async readData(path) { async readData(path) {
const qualified = this.constructor.qualify(path); const qualified = this.constructor.qualify(path);