mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-05 15:28:29 +08:00
Merge remote-tracking branch 'github/master'
This commit is contained in:
commit
a5dd34cda7
@ -1,11 +1,4 @@
|
||||
import {
|
||||
Arg,
|
||||
Instruction,
|
||||
InstructionSegment,
|
||||
new_arg,
|
||||
Segment,
|
||||
SegmentType,
|
||||
} from "../instructions";
|
||||
import { Instruction, InstructionSegment, Segment, SegmentType } from "../instructions";
|
||||
import {
|
||||
OP_ADD,
|
||||
OP_ADDI,
|
||||
@ -14,7 +7,6 @@ import {
|
||||
OP_ARG_PUSHB,
|
||||
OP_ARG_PUSHL,
|
||||
OP_ARG_PUSHR,
|
||||
OP_ARG_PUSHS,
|
||||
OP_ARG_PUSHW,
|
||||
OP_CALL,
|
||||
OP_CLEAR,
|
||||
@ -78,14 +70,19 @@ import {
|
||||
OP_STACK_PUSH,
|
||||
OP_STACK_PUSHM,
|
||||
OP_STACK_POPM,
|
||||
Param,
|
||||
Kind,
|
||||
} from "../opcodes";
|
||||
import Logger from "js-logger";
|
||||
|
||||
const logger = Logger.get("quest_editor/scripting/vm");
|
||||
|
||||
const REGISTERS_BASE_ADDRESS = 0x00a954b0;
|
||||
const REGISTER_COUNT = 256;
|
||||
const REGISTER_SIZE = 4;
|
||||
const VARIABLE_STACK_LENGTH = 16; // TODO: verify this value
|
||||
const ARG_STACK_SLOT_SIZE = 4;
|
||||
const ARG_STACK_LENGTH = 8;
|
||||
|
||||
export enum ExecutionResult {
|
||||
Ok,
|
||||
@ -164,8 +161,223 @@ function rest<T>(lst: T[]): T[] {
|
||||
return lst.slice(1);
|
||||
}
|
||||
|
||||
type Range = [number, number];
|
||||
|
||||
function ranges_overlap(a: Range, b: Range): boolean {
|
||||
return a[0] <= b[1] && b[0] <= a[1];
|
||||
}
|
||||
|
||||
class VirtualMachineMemoryBuffer extends ArrayBuffer {
|
||||
/**
|
||||
* The memory this buffer belongs to.
|
||||
*/
|
||||
public readonly memory: VirtualMachineMemory;
|
||||
/**
|
||||
* The memory address of this buffer.
|
||||
*/
|
||||
public readonly address: number;
|
||||
|
||||
constructor(memory: VirtualMachineMemory, address: number, size: number) {
|
||||
super(size);
|
||||
this.memory = memory;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public get_offset(byte_offset: number): VirtualMachineMemorySlot | null {
|
||||
return this.memory.get(this.address + byte_offset);
|
||||
}
|
||||
|
||||
public free(): void {
|
||||
this.memory.free(this.address);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a single location in memory.
|
||||
*/
|
||||
class VirtualMachineMemorySlot {
|
||||
/**
|
||||
* The memory this slot belongs to.
|
||||
*/
|
||||
public readonly memory: VirtualMachineMemory;
|
||||
/**
|
||||
* The memory address this slots represents.
|
||||
*/
|
||||
public readonly address: number;
|
||||
/**
|
||||
* The allocated buffer this slot is a part of.
|
||||
*/
|
||||
public readonly buffer: VirtualMachineMemoryBuffer;
|
||||
/**
|
||||
* The offset that this slot represents in the buffer.
|
||||
*/
|
||||
public readonly byte_offset: number;
|
||||
|
||||
constructor(
|
||||
memory: VirtualMachineMemory,
|
||||
address: number,
|
||||
buffer: VirtualMachineMemoryBuffer,
|
||||
byte_offset: number,
|
||||
) {
|
||||
this.memory = memory;
|
||||
this.address = address;
|
||||
this.buffer = buffer;
|
||||
this.byte_offset = byte_offset;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps memory addresses to buffers.
|
||||
*/
|
||||
class VirtualMachineMemory {
|
||||
private allocated_ranges: Range[] = [];
|
||||
private ranges_sorted: boolean = true;
|
||||
private memory: Map<number, VirtualMachineMemorySlot> = new Map();
|
||||
|
||||
private sort_ranges(): void {
|
||||
this.allocated_ranges.sort((a, b) => a[0] - b[0]);
|
||||
|
||||
this.ranges_sorted = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Would a buffer of the given size fit at the given address?
|
||||
*/
|
||||
private will_fit(address: number, size: number): boolean {
|
||||
const fit_range: Range = [address, address + size - 1];
|
||||
|
||||
if (!this.ranges_sorted) {
|
||||
this.sort_ranges();
|
||||
}
|
||||
|
||||
// check if it would overlap any already allocated space
|
||||
for (const alloc_range of this.allocated_ranges) {
|
||||
if (ranges_overlap(alloc_range, fit_range)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an address where a buffer of the given size would fit.
|
||||
*/
|
||||
private find_free_space(size: number): number {
|
||||
let address = 0;
|
||||
|
||||
// nothing yet allocated, we can place it wherever
|
||||
if (this.allocated_ranges.length < 1) {
|
||||
return address;
|
||||
}
|
||||
|
||||
if (!this.ranges_sorted) {
|
||||
this.sort_ranges();
|
||||
}
|
||||
|
||||
// check if buffer could fit in between allocated buffers
|
||||
for (const alloc_range of this.allocated_ranges) {
|
||||
if (!ranges_overlap(alloc_range, [address, address + size - 1])) {
|
||||
return address;
|
||||
}
|
||||
|
||||
address = alloc_range[1] + 1;
|
||||
}
|
||||
|
||||
// just place it at the end
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate a buffer of the given size at the given address.
|
||||
* If the address is omitted a suitable location is chosen.
|
||||
* @returns The allocated buffer.
|
||||
*/
|
||||
public allocate(size: number, address?: number): VirtualMachineMemoryBuffer {
|
||||
if (size <= 0) {
|
||||
throw new Error("Allocation failed: The size of the buffer must be greater than 0");
|
||||
}
|
||||
|
||||
// check if given address is good or find an address if none was given
|
||||
if (address === undefined) {
|
||||
address = this.find_free_space(size);
|
||||
} else {
|
||||
if (!this.will_fit(address, size)) {
|
||||
throw new Error(
|
||||
"Allocation failed: Cannot fit a buffer of the given size at the given address",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// save the range of allocated memory
|
||||
this.allocated_ranges.push([address, address + size - 1]);
|
||||
this.ranges_sorted = false;
|
||||
|
||||
// the actual buffer
|
||||
const buf = new VirtualMachineMemoryBuffer(this, address, size);
|
||||
|
||||
// set addresses to correct buffer offsets
|
||||
for (let offset = 0; offset < size; offset++) {
|
||||
this.memory.set(
|
||||
address + offset,
|
||||
new VirtualMachineMemorySlot(this, address, buf, offset),
|
||||
);
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the memory allocated for the buffer at the given address.
|
||||
*/
|
||||
public free(address: number): void {
|
||||
// check if address is a valid allocated buffer
|
||||
let range: Range | null = null;
|
||||
let range_idx = -1;
|
||||
|
||||
for (let i = 0; i < this.allocated_ranges.length; i++) {
|
||||
const cur = this.allocated_ranges[i];
|
||||
if (cur[0] === address) {
|
||||
range = cur;
|
||||
range_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (range === null) {
|
||||
throw new Error("Free failed: Given address is not the start of an allocated buffer");
|
||||
}
|
||||
|
||||
const [alloc_start, alloc_end] = range;
|
||||
|
||||
// remove addresses
|
||||
for (let addr = alloc_start; addr <= alloc_end; addr++) {
|
||||
this.memory.delete(addr);
|
||||
}
|
||||
|
||||
// remove range
|
||||
this.allocated_ranges.splice(range_idx, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the memory at the given address. Returns null if
|
||||
* there is nothing allocated at the given address.
|
||||
*/
|
||||
public get(address: number): VirtualMachineMemorySlot | null {
|
||||
if (this.memory.has(address)) {
|
||||
return this.memory.get(address)!;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class VirtualMachine {
|
||||
private register_store = new ArrayBuffer(REGISTER_SIZE * REGISTER_COUNT);
|
||||
private memory = new VirtualMachineMemory();
|
||||
private register_store = this.memory.allocate(
|
||||
REGISTER_SIZE * REGISTER_COUNT,
|
||||
REGISTERS_BASE_ADDRESS,
|
||||
)!;
|
||||
private register_uint8_view = new Uint8Array(this.register_store);
|
||||
private registers = new DataView(this.register_store);
|
||||
private object_code: Segment[] = [];
|
||||
@ -211,7 +423,18 @@ export class VirtualMachine {
|
||||
);
|
||||
}
|
||||
|
||||
this.thread.push(new Thread(new ExecutionLocation(seg_idx!, 0), true));
|
||||
this.thread.push(
|
||||
new Thread(
|
||||
new ExecutionLocation(seg_idx!, 0),
|
||||
this.memory.allocate(ARG_STACK_SLOT_SIZE * ARG_STACK_LENGTH),
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private dispose_thread(thread_idx: number): void {
|
||||
this.thread[thread_idx].dispose();
|
||||
this.thread.splice(thread_idx, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -227,6 +450,7 @@ export class VirtualMachine {
|
||||
const inst = this.get_next_instruction_from_thread(exec);
|
||||
|
||||
const arg_vals = inst.args.map(arg => arg.value);
|
||||
// eslint-disable-next-line
|
||||
const [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7] = arg_vals;
|
||||
|
||||
// helper for conditional jump opcodes
|
||||
@ -282,14 +506,16 @@ export class VirtualMachine {
|
||||
break;
|
||||
case OP_ARG_PUSHR.code:
|
||||
// deref given register ref
|
||||
this.push_arg_stack(exec, new_arg(this.get_sint(arg0), REGISTER_SIZE));
|
||||
exec.push_arg(this.get_sint(arg0), Kind.DWord);
|
||||
break;
|
||||
case OP_ARG_PUSHL.code:
|
||||
exec.push_arg(inst.args[0].value, Kind.DWord);
|
||||
break;
|
||||
case OP_ARG_PUSHB.code:
|
||||
exec.push_arg(inst.args[0].value, Kind.Byte);
|
||||
break;
|
||||
case OP_ARG_PUSHW.code:
|
||||
case OP_ARG_PUSHS.code:
|
||||
// push arg as-is
|
||||
this.push_arg_stack(exec, inst.args[0]);
|
||||
exec.push_arg(inst.args[0].value, Kind.Word);
|
||||
break;
|
||||
// arithmetic operations
|
||||
case OP_ADD.code:
|
||||
@ -508,7 +734,7 @@ export class VirtualMachine {
|
||||
// segment ended, move to next segment
|
||||
if (++top.seg_idx >= this.object_code.length) {
|
||||
// eof
|
||||
this.thread.splice(this.thread_idx, 1);
|
||||
this.dispose_thread(this.thread_idx);
|
||||
} else {
|
||||
top.inst_idx = 0;
|
||||
}
|
||||
@ -676,20 +902,6 @@ export class VirtualMachine {
|
||||
}
|
||||
}
|
||||
|
||||
private push_arg_stack(exec: Thread, arg: Arg): void {
|
||||
exec.arg_stack.push(arg);
|
||||
}
|
||||
|
||||
private pop_arg_stack(exec: Thread): Arg {
|
||||
const arg = exec.arg_stack.pop();
|
||||
|
||||
if (!arg) {
|
||||
throw new Error("Argument stack underflow.");
|
||||
}
|
||||
|
||||
return arg;
|
||||
}
|
||||
|
||||
private push_variable_stack(exec: Thread, base_reg: number, num_push: number): void {
|
||||
const end = base_reg + num_push;
|
||||
|
||||
@ -748,30 +960,100 @@ export class VirtualMachine {
|
||||
private clear_registers(): void {
|
||||
this.register_uint8_view.fill(0);
|
||||
}
|
||||
|
||||
private get_register_address(reg: number): number {
|
||||
return this.register_store.address + reg * REGISTER_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
class ExecutionLocation {
|
||||
constructor(public seg_idx: number, public inst_idx: number) {}
|
||||
}
|
||||
|
||||
type ArgStackTypeList = [Kind, Kind, Kind, Kind, Kind, Kind, Kind, Kind];
|
||||
|
||||
class Thread {
|
||||
/**
|
||||
* Call stack. The top element describes the instruction about to be executed.
|
||||
*/
|
||||
public call_stack: ExecutionLocation[] = [];
|
||||
public arg_stack: Arg[] = [];
|
||||
|
||||
private arg_stack_buf: VirtualMachineMemoryBuffer;
|
||||
private arg_stack_counter: number = 0;
|
||||
private arg_stack: DataView;
|
||||
private arg_stack_types: ArgStackTypeList = Array(ARG_STACK_LENGTH).fill(
|
||||
Kind.Any,
|
||||
) as ArgStackTypeList;
|
||||
|
||||
public variable_stack: number[] = [];
|
||||
/**
|
||||
* Global or floor-local?
|
||||
*/
|
||||
public global: boolean;
|
||||
|
||||
call_stack_top(): ExecutionLocation {
|
||||
constructor(
|
||||
next: ExecutionLocation,
|
||||
arg_stack_buf: VirtualMachineMemoryBuffer,
|
||||
global: boolean,
|
||||
) {
|
||||
this.call_stack = [next];
|
||||
this.global = global;
|
||||
|
||||
this.arg_stack_buf = arg_stack_buf;
|
||||
this.arg_stack = new DataView(arg_stack_buf);
|
||||
}
|
||||
|
||||
public call_stack_top(): ExecutionLocation {
|
||||
return this.call_stack[this.call_stack.length - 1];
|
||||
}
|
||||
|
||||
constructor(next: ExecutionLocation, global: boolean) {
|
||||
this.call_stack = [next];
|
||||
this.global = global;
|
||||
public push_arg(data: number, type: Kind): void {
|
||||
if (this.arg_stack_counter >= ARG_STACK_LENGTH) {
|
||||
throw new Error("Argument stack: Stack overflow");
|
||||
}
|
||||
|
||||
this.arg_stack.setUint32(this.arg_stack_counter * ARG_STACK_SLOT_SIZE, data, true);
|
||||
this.arg_stack_types[this.arg_stack_counter] = type;
|
||||
|
||||
this.arg_stack_counter++;
|
||||
}
|
||||
|
||||
public fetch_args(params: readonly Param[]): number[] {
|
||||
const args: number[] = [];
|
||||
|
||||
if (params.length !== this.arg_stack_counter) {
|
||||
logger.warn("Argument stack: Argument count mismatch");
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.length; i++) {
|
||||
const param = params[i];
|
||||
|
||||
if (param.type.kind !== this.arg_stack_types[i]) {
|
||||
logger.warn("Argument stack: Argument type mismatch");
|
||||
}
|
||||
|
||||
switch (param.type.kind) {
|
||||
case Kind.Byte:
|
||||
args.push(this.arg_stack.getUint8(i));
|
||||
break;
|
||||
case Kind.Word:
|
||||
args.push(this.arg_stack.getUint16(i, true));
|
||||
break;
|
||||
case Kind.DWord:
|
||||
case Kind.String:
|
||||
args.push(this.arg_stack.getUint32(i, true));
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unhandled param kind: Kind.${Kind[param.type.kind]}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.arg_stack_counter = 0;
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.arg_stack_buf.free();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user