diff --git a/src/common/ecs/binpack.ts b/src/common/ecs/binpack.ts new file mode 100644 index 00000000..a0662cc9 --- /dev/null +++ b/src/common/ecs/binpack.ts @@ -0,0 +1,170 @@ + +var debug = true; + +export interface BoxConstraints { + left?: number; + top?: number; + width: number; + height: number; + box?: PlacedBox; +} + +enum BoxPlacement { + TopLeft=0, TopRight=1, BottomLeft=2, BottomRight=3 +} + +export interface Box { + left: number; + top: number; + right: number; + bottom: number; +} + +export interface PlacedBox extends Box { + bin: Bin; + parent: Box; + place: BoxPlacement; +} + +function boxesIntersect(a: Box, b: Box) : boolean { + return !(b.left >= a.right || b.right <= a.left || b.top >= a.bottom || b.bottom <= a.top); +} + +function getBoxPlacements(b: PlacedBox) { + let posns : BoxPlacement[]; + let snugw = b.right - b.left == b.parent.right - b.parent.left; + let snugh = b.bottom - b.top == b.parent.bottom - b.parent.top; + if (snugw && snugh) { + posns = [BoxPlacement.TopLeft]; + } else if (snugw && !snugh) { + posns = [BoxPlacement.TopLeft, BoxPlacement.BottomLeft]; + } else if (!snugw && snugh) { + posns = [BoxPlacement.TopLeft, BoxPlacement.TopRight]; + } else { + posns = [BoxPlacement.TopLeft, BoxPlacement.TopRight, + BoxPlacement.BottomLeft, BoxPlacement.BottomRight]; + } + return posns; +} + +export class Bin { + boxes: Box[] = []; + free: Box[] = []; + + constructor(public readonly binbounds: Box) { + this.free.push(binbounds); + } + getBoxes(bounds: Box, limit: number) : Box[] { + let result = []; + for (let box of this.boxes) { + //console.log(bounds, box, boxesIntersect(bounds, box)) + if (boxesIntersect(bounds, box)) { + result.push(box); + if (result.length >= limit) break; + } + } + return result; + } + fits(b: Box) { + if (!boxesIntersect(this.binbounds, b)) return false; + if (this.getBoxes(b, 1).length > 0) return false; + return true; + } + bestFit(b: Box) : Box | null { + let bestscore = 0; + let best = null; + for (let f of this.free) { + let dx = (f.right - f.left) - (b.right - b.left); + let dy = (f.bottom - f.top) - (b.bottom - b.top); + if (dx >= 0 && dy >= 0) { + let score = 1 / (1 + dx + dy); + if (score > bestscore) { + best = f; + } + } + } + return best; + } + add(b: PlacedBox) { + if (debug) console.log('added',b.left,b.top,b.right,b.bottom); + if (!this.fits(b)) { + //console.log('collided with', this.getBoxes(b, 1)); + throw new Error(`bad fit ${b.left} ${b.top} ${b.right} ${b.bottom}`) + } + // add box to list + this.boxes.push(b); + // delete bin + let i = this.free.indexOf(b.parent); + if (i < 0) throw new Error('cannot find parent'); + if (debug) console.log('removed',b.parent.left,b.parent.top,b.parent.right,b.parent.bottom); + this.free.splice(i, 1); + // split into new bins + switch (b.place) { + case BoxPlacement.TopLeft: + this.addFree( { top: b.top, left: b.right, bottom: b.bottom, right: b.parent.right } ); + this.addFree( { top: b.bottom, left: b.parent.left, bottom: b.parent.bottom, right: b.parent.right } ); + break; + case BoxPlacement.TopRight: + this.addFree( { top: b.top, left: b.parent.left, bottom: b.bottom, right: b.left } ); + this.addFree( { top: b.bottom, left: b.parent.left, bottom: b.parent.bottom, right: b.parent.right } ); + break; + case BoxPlacement.BottomLeft: + this.addFree( { top: b.parent.top, left: b.parent.left, bottom: b.top, right: b.parent.right } ); + this.addFree( { top: b.top, left: b.right, bottom: b.parent.bottom, right: b.parent.right } ); + break; + case BoxPlacement.BottomRight: + this.addFree( { top: b.parent.top, left: b.parent.left, bottom: b.top, right: b.parent.right } ); + this.addFree( { top: b.top, left: b.parent.left, bottom: b.parent.bottom, right: b.left } ); + break; + } + } + addFree(b: Box) { + if (b.bottom > b.top && b.right > b.left) { + if (debug) console.log('free',b.left,b.top,b.right,b.bottom); + this.free.push(b); + } + // TODO: merge free boxes + } +} + +export class Packer { + bins : Bin[] = []; + boxes : BoxConstraints[] = []; + + pack() : boolean { + for (let bc of this.boxes) { + let box = this.bestPlacement(bc); + if (!box) return false; + box.bin.add(box); + bc.box = box; + } + return true; + } + bestPlacement(b: BoxConstraints) : PlacedBox | null { + let left = b.left != null ? b.left : 0; + let top = b.top != null ? b.top : 0; + let right = left + b.width; + let bottom = top + b.height; + for (let bin of this.bins) { + let place : BoxPlacement = BoxPlacement.TopLeft; //TODO + let box = { left, top, right, bottom }; + let parent = bin.bestFit(box); + if (parent) { + box.left = parent.left; + box.top = parent.top; + box.right = parent.left + b.width; + box.bottom = parent.top + b.height; + /* + if (place == BoxPlacement.BottomLeft || place == BoxPlacement.BottomRight) { + box.top = parent.bottom - (box.bottom - box.top); + } + if (place == BoxPlacement.TopRight || place == BoxPlacement.BottomRight) { + box.left = parent.right - (box.right - box.left); + } + */ + return { parent, place, bin, ...box }; + } + } + return null; + } +} diff --git a/src/common/ecs/ecs.ts b/src/common/ecs/ecs.ts index 40e1a31f..a810d2b6 100644 --- a/src/common/ecs/ecs.ts +++ b/src/common/ecs/ecs.ts @@ -107,6 +107,11 @@ export interface Query extends SourceLocated { limit?: number; } +export class SystemStats { + tempstartseq: number | undefined; + tempendseq: number | undefined; +} + export interface System extends SourceLocated { name: string; actions: Action[]; @@ -117,6 +122,10 @@ export const SELECT_TYPE = ['once', 'foreach', 'join', 'with', 'if', 'select'] a export type SelectType = typeof SELECT_TYPE[number]; +export class ActionStats { + callcount: number = 0; +} + export interface ActionBase extends SourceLocated { select: SelectType; event: string; @@ -687,6 +696,7 @@ class ActionEval { if (isNaN(tempinc)) throw new ECSError(`bad temporary offset`, this.action); if (!this.sys.tempbytes) throw new ECSError(`this system has no locals`, this.action); if (tempinc < 0 || tempinc >= this.sys.tempbytes) throw new ECSError(`this system only has ${this.sys.tempbytes} locals`, this.action); + this.scope.updateTempLiveness(this.sys); return `${this.tmplabel}+${tempinc}`; //return `TEMP+${this.scope.tempOffset}+${tempinc}`; } @@ -828,10 +838,13 @@ export class EntityScope implements SourceLocated { systems: System[] = []; entities: Entity[] = []; fieldtypes: { [name: string]: 'init' | 'const' } = {}; + sysstats = new Map(); + actionstats = new Map(); bss = new UninitDataSegment(); rodata = new ConstDataSegment(); code = new CodeSegment(); componentsInScope = new Set(); + eventSeq = 0; tempOffset = 0; tempSize = 0; maxTempBytes = 0; @@ -861,6 +874,7 @@ export class EntityScope implements SourceLocated { return entity; } addUsingSystem(system: System) { + if (!system) throw new Error(); this.systems.push(system); } getEntityByName(name: string) { @@ -1063,9 +1077,12 @@ export class EntityScope implements SourceLocated { let systems = this.em.event2systems[event]; if (!systems || systems.length == 0) { // TODO: error or warning? - console.log(`warning: no system responds to "${event}"`); return ''; //throw new ECSError(`warning: no system responds to "${event}"`); + console.log(`warning: no system responds to "${event}"`); + return ''; } + this.eventSeq++; + // generate code let s = this.dialect.code(); //s += `\n; event ${event}\n`; systems = systems.filter(s => this.systems.includes(s)); @@ -1089,6 +1106,7 @@ export class EntityScope implements SourceLocated { s += this.dialect.comment(`end action ${sys.name} ${event}`); // TODO: check that this happens once? codeeval.end(); + this.getActionStats(action).callcount++; numActions++; } } @@ -1102,6 +1120,32 @@ export class EntityScope implements SourceLocated { this.maxTempBytes = Math.max(this.tempSize, this.maxTempBytes); if (n < 0) this.tempOffset = this.tempSize; } + getSystemStats(sys: System) : SystemStats { + let stats = this.sysstats.get(sys); + if (!stats) { + stats = new SystemStats(); + this.sysstats.set(sys, stats); + } + return stats; + } + getActionStats(action: Action) : ActionStats { + let stats = this.actionstats.get(action); + if (!stats) { + stats = new ActionStats(); + this.actionstats.set(action, stats); + } + return stats; + } + updateTempLiveness(sys: System) { + let stats = this.getSystemStats(sys); + let n = this.eventSeq; + if (stats.tempstartseq && stats.tempendseq) { + stats.tempstartseq = Math.min(stats.tempstartseq, n); + stats.tempendseq = Math.max(stats.tempendseq, n); + } else { + stats.tempstartseq = stats.tempendseq = n; + } + } includeResource(symbol: string): string { this.resources.add(symbol); return symbol; @@ -1116,8 +1160,9 @@ export class EntityScope implements SourceLocated { let isMainScope = this.parent == null; this.tempOffset = this.maxTempBytes = 0; let start; - if (isMainScope) { - this.addUsingSystem(this.em.getSystemByName('Init')); //TODO: what if none? + let initsys = this.em.getSystemByName('Init'); + if (isMainScope && initsys) { + this.addUsingSystem(initsys); //TODO: what if none? start = this.generateCodeForEvent('main_init'); } else { start = this.generateCodeForEvent('start'); @@ -1127,6 +1172,15 @@ export class EntityScope implements SourceLocated { let code = this.generateCodeForEvent(sub); this.code.addCodeFragment(code); } + //this.showStats(); + } + showStats() { + for (let sys of this.systems) { + console.log(sys.name, this.getSystemStats(sys)); + } + for (let action of Array.from(this.actionstats.keys())) { + console.log(action.event, this.getActionStats(action)); + } } dump(file: SourceFileExport) { this.analyzeEntities(); diff --git a/src/test/testecs.ts b/src/test/testecs.ts index 2498062d..fcc31534 100644 --- a/src/test/testecs.ts +++ b/src/test/testecs.ts @@ -1,7 +1,8 @@ import assert from "assert"; -import { execFileSync } from "child_process"; +import { execFileSync, spawnSync } from "child_process"; import { readdirSync, readFileSync, writeFileSync } from "fs"; import { describe } from "mocha"; +import { Bin, Packer } from "../common/ecs/binpack"; import { ECSCompiler } from "../common/ecs/compiler"; import { Dialect_CA65, EntityManager, SourceFileExport } from "../common/ecs/ecs"; @@ -389,8 +390,24 @@ describe('Compiler', function() { let goodtxt = readFileSync(destpath, 'utf-8'); if (outtxt.trim() != goodtxt.trim()) { writeFileSync('/tmp/' + goodfn, outtxt, 'utf-8'); - execFileSync('/usr/bin/diff', [srcpath, destpath]); + console.log(spawnSync('/usr/bin/diff', [srcpath, destpath], {encoding:'utf-8'}).stdout); throw new Error(ecsfn + ' did not match test file'); } }); }); + +describe('Box Packer', function() { + it('Should pack boxes', function() { + let packer = new Packer(); + let bin1 = new Bin({ left:0, top:0, right:10, bottom:10 }); + packer.bins.push(bin1); + packer.boxes.push({ width: 5, height: 5 }); + packer.boxes.push({ width: 5, height: 5 }); + packer.boxes.push({ width: 5, height: 5 }); + packer.boxes.push({ width: 5, height: 5 }); + if (!packer.pack()) throw new Error('cannot pack') + console.log(packer.boxes); + console.log(packer.bins[0].free) + }); +}); +