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) {
let result;
for (let i = 0; i < ops; ++i) {
@ -265,8 +270,7 @@ export default class Sandbox {
}
const result = this.generator.next();
if (result.done) {
this.generator = undefined;
this.compile();
this.reset();
}
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() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.item = async function (slot) {
const {slots} = this;
if (!(slot in slots)) {
return undefined;
return class InventoryInstance extends Instance {
async item(slot) {
const {slots} = this;
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;
const json = await (
readAsset([slots[slot].source, 'item.json'].join('/'))
.then((response) => (response.ok ? response.json() : {}))
);
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;
};
Instance.prototype.swapSlots = function(l, r) {
const {slots} = this;
const tmp = slots[l];
const change = {};
if (slots[r]) {
change[l] = slots[l] = slots[r];
swapSlots(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);
}
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 = {
slots: {

View File

@ -4,34 +4,32 @@ export default class Ticking extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
Instance.prototype.$$finished = [];
Instance.prototype.$$tickingPromises = [];
Instance.prototype.addTickingPromise = function(tickingPromise) {
this.$$tickingPromises.push(tickingPromise);
tickingPromise.then(() => {
this.$$finished.push(tickingPromise);
});
return class TickingInstance extends Instance {
$$finished = [];
$$tickingPromises = [];
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 = {
isTicking: {defaultValue: 1, type: 'uint8'},

View File

@ -37,53 +37,54 @@ export default class TileLayers extends Component {
instanceFromSchema() {
const Instance = super.instanceFromSchema();
const Component = this;
Instance.prototype.layer = function (index) {
const {layers} = this;
if (!(index in layers)) {
return undefined;
}
const instance = this;
class LayerProxy {
constructor(layer) {
this.layer = layer;
return class TileLayersInstance extends Instance {
layer(index) {
const {layers} = this;
if (!(index in layers)) {
return undefined;
}
get area() {
return this.layer.area;
}
get source() {
return this.layer.source;
}
stamp(at, data) {
const changes = {};
for (const row in data) {
const columns = data[row];
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 instance = this;
class LayerProxy {
constructor(layer) {
this.layer = layer;
}
get area() {
return this.layer.area;
}
get source() {
return this.layer.source;
}
stamp(at, data) {
const changes = {};
for (const row in data) {
const columns = data[row];
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) {
return undefined;
tile({x, y}) {
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 = {
layers: {

View File

@ -71,15 +71,14 @@ export default class Wielder extends Component {
const activeItem = await this.activeItem();
if (activeItem) {
ecs.readAsset([activeItem.source, state ? 'start.js' : 'stop.js'].join('/'))
.then((script) => (script.ok ? script.text() : ''))
.then((code) => {
if (code) {
if (code.byteLength > 0) {
const context = {
ecs,
item: activeItem,
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('/');
}
async readAsset(path) {
return fetch(path);
return fetch(path).then((response) => (
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
));
}
async readData(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 {RESOLUTION} from '@/constants.js';
import AssetsContext from '@/context/assets.js';
import ClientContext from '@/context/client.js';
import DebugContext from '@/context/debug.js';
import EcsContext from '@/context/ecs.js';
@ -16,7 +17,7 @@ import styles from './pixi.module.css';
BaseTexture.defaultOptions.scaleMode = SCALE_MODES.NEAREST;
const Contexts = [ClientContext, DebugContext, EcsContext, MainEntityContext];
const Contexts = [AssetsContext, ClientContext, DebugContext, EcsContext, MainEntityContext];
const ContextBridge = ({children, render}) => {
const contexts = Contexts.map(useContext);

View File

@ -1,6 +1,6 @@
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}) {
const asset = useAsset(entity.Sprite.source);

View File

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

View File

@ -2,6 +2,7 @@ import {json} from "@remix-run/node";
import {useEffect, useState} from 'react';
import {useOutletContext, useParams} from 'react-router-dom';
import AssetsContext from '@/context/assets.js';
import ClientContext from '@/context/client.js';
import DebugContext from '@/context/debug.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 {juggleSession} from '@/session.server';
import {LRUCache} from 'lru-cache';
export const cache = new LRUCache({
max: 128,
});
class ClientEcs extends Ecs {
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() {
const Client = useOutletContext();
const assetsTuple = useState({});
const [client, setClient] = useState();
const mainEntityTuple = useState();
const debugTuple = useState(false);
@ -102,7 +123,9 @@ export default function PlaySpecific() {
<MainEntityContext.Provider value={mainEntityTuple}>
<EcsContext.Provider value={ecsTuple}>
<DebugContext.Provider value={debugTuple}>
<Ui disconnected={disconnected} />
<AssetsContext.Provider value={assetsTuple}>
<Ui disconnected={disconnected} />
</AssetsContext.Provider>
</DebugContext.Provider>
</EcsContext.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 = {}) {
let ast;
if (cache.has(code)) {
ast = cache.get(code);
}
else {
cache.set(code, ast = await this.parse(code));
if (!cache.has(code)) {
cache.set(code, this.parse(code));
}
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) {
url.protocol = 'http:';
}
return fetch(url.href);
return fetch(url.href).then((response) => (
response.ok ? response.arrayBuffer() : new ArrayBuffer(0)
));
}
async readData(path) {
const qualified = this.constructor.qualify(path);