Merge pull request #2 from jtuu/vm

Merging incomplete feature branch 'vm' behind a feature flag
This commit is contained in:
Daan Vanden Bosch 2019-10-02 20:53:41 +02:00 committed by GitHub
commit f2d746642f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 289 additions and 50 deletions

View File

@ -146,6 +146,7 @@ export enum Icon {
Redo,
Remove,
GitHub,
Play,
}
export function icon(icon: Icon): HTMLElement {
@ -179,6 +180,9 @@ export function icon(icon: Icon): HTMLElement {
case Icon.GitHub:
icon_str = "fab fa-github";
break;
case Icon.Play:
icon_str = "fas fa-play";
break;
}
return el.span({ class: icon_str });

View File

@ -69,18 +69,26 @@ export class QuestEditorToolBar extends ToolBar {
}
},
);
super({
children: [
new_quest_button,
open_file_button,
save_as_button,
undo_button,
redo_button,
area_select,
],
const run_button = new Button("Run in VM", {
icon_left: Icon.Play,
tooltip: "[Experimental] Run the current quest in a virtual machine."
});
const children = [
new_quest_button,
open_file_button,
save_as_button,
undo_button,
redo_button,
area_select,
];
if (gui_store.feature_active("vm")) {
children.push(run_button);
}
super({ children });
const quest_loaded = quest_editor_store.current_quest.map(q => q != undefined);
this.disposables(
@ -109,6 +117,8 @@ export class QuestEditorToolBar extends ToolBar {
quest_editor_store.set_current_area(area),
),
run_button.click.observe(() => quest_editor_store.run_current_quest_in_vm()),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () =>
open_file_button.click(),
),

View File

@ -1,4 +1,4 @@
import { Instruction, InstructionSegment, Segment, SegmentType } from "../instructions";
import { Instruction, InstructionSegment, Segment, SegmentType, Arg, new_arg } from "../instructions";
import {
OP_CALL,
OP_CLEAR,
@ -13,6 +13,40 @@ import {
OP_SET,
OP_SYNC,
OP_THREAD,
OP_JMP,
OP_ARG_PUSHR,
OP_ARG_PUSHL,
OP_ARG_PUSHB,
OP_ARG_PUSHW,
OP_ARG_PUSHA,
OP_ARG_PUSHO,
OP_ARG_PUSHS,
OP_ADD,
OP_ADDI,
OP_SUB,
OP_SUBI,
OP_FADD,
OP_FADDI,
OP_FSUB,
OP_FSUBI,
OP_FMUL,
OP_MUL,
OP_MULI,
OP_FMULI,
OP_DIV,
OP_FDIV,
OP_DIVI,
OP_FDIVI,
OP_MOD,
OP_MODI,
OP_AND,
OP_ANDI,
OP_OR,
OP_ORI,
OP_XOR,
OP_XORI,
OP_SHIFT_LEFT,
OP_SHIFT_RIGHT,
} from "../opcodes";
import Logger from "js-logger";
@ -27,6 +61,33 @@ export enum ExecutionResult {
Halted,
}
type BinaryNumericOperation = (a: number, b: number) => number;
const numeric_ops: Record<"add" |
"sub" |
"mul" |
"div" |
"idiv" |
"mod" |
"and" |
"or" |
"xor" |
"shl" |
"shr",
BinaryNumericOperation> = {
add: (a, b) => a + b,
sub: (a, b) => a - b,
mul: (a, b) => a * b,
div: (a, b) => a / b,
idiv: (a, b) => Math.floor(a / b),
mod: (a, b) => a % b,
and: (a, b) => a & b,
or: (a, b) => a | b,
xor: (a, b) => a ^ b,
shl: (a, b) => a << b,
shr: (a, b) => a >>> b,
};
export class VirtualMachine {
private register_store = new ArrayBuffer(REGISTER_SIZE * REGISTER_COUNT);
private register_uint8_view = new Uint8Array(this.register_store);
@ -74,7 +135,7 @@ export class VirtualMachine {
);
}
this.thread.push(new Thread(new StackElement(seg_idx!, 0), true));
this.thread.push(new Thread(new ExecutionLocation(seg_idx!, 0), true));
}
/**
@ -89,54 +150,150 @@ export class VirtualMachine {
const exec = this.thread[this.thread_idx];
const inst = this.get_next_instruction_from_thread(exec);
switch (inst.opcode) {
case OP_NOP:
const [arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7] = inst.args.map(arg => arg.value);
switch (inst.opcode.code) {
case OP_NOP.code:
break;
case OP_RET:
case OP_RET.code:
this.pop_call_stack(this.thread_idx, exec);
break;
case OP_SYNC:
case OP_SYNC.code:
this.thread_idx++;
break;
case OP_EXIT:
case OP_EXIT.code:
this.halt();
break;
case OP_THREAD:
this.start_thread(inst.args[0].value);
case OP_THREAD.code:
this.start_thread(arg0);
break;
case OP_LET:
this.set_sint(inst.args[0].value, this.get_sint(inst.args[1].value));
case OP_LET.code:
this.set_sint(arg0, this.get_sint(arg1));
break;
case OP_LETI:
this.set_sint(inst.args[0].value, inst.args[1].value);
case OP_LETI.code:
this.set_sint(arg0, arg1);
break;
case OP_LETB:
case OP_LETW:
this.set_uint(inst.args[0].value, inst.args[1].value);
case OP_LETB.code:
case OP_LETW.code:
this.set_uint(arg0, arg1);
break;
case OP_SET:
this.set_sint(inst.args[0].value, 1);
case OP_SET.code:
this.set_sint(arg0, 1);
break;
case OP_CLEAR:
this.set_sint(inst.args[0].value, 0);
case OP_CLEAR.code:
this.set_sint(arg0, 0);
break;
case OP_REV:
this.set_sint(inst.args[0].value, this.get_sint(inst.args[0].value) === 0 ? 1 : 0);
case OP_REV.code:
this.set_sint(arg0, this.get_sint(arg0) === 0 ? 1 : 0);
break;
case OP_CALL:
this.push_call_stack(exec, inst.args[0].value);
case OP_CALL.code:
this.push_call_stack(exec, arg0);
break;
case OP_JMP.code:
this.jump_to_label(exec, arg0);
break;
case OP_ARG_PUSHR.code:
// deref given register ref
this.push_arg_stack(exec, new_arg(
this.get_sint(arg0),
REGISTER_SIZE,
inst.args[0].asm
));
break;
case OP_ARG_PUSHL.code:
case OP_ARG_PUSHB.code:
case OP_ARG_PUSHW.code:
case OP_ARG_PUSHS.code:
// push arg as-is
this.push_arg_stack(exec, inst.args[0]);
break;
// arithmetic operations
case OP_ADD.code:
case OP_FADD.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.add);
break;
case OP_ADDI.code:
case OP_FADDI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.add);
break;
case OP_SUB.code:
case OP_FSUB.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.sub);
break;
case OP_SUBI.code:
case OP_FSUBI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.sub);
break;
case OP_MUL.code:
case OP_FMUL.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.mul);
break;
case OP_MULI.code:
case OP_FMULI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.mul);
break;
case OP_DIV.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.idiv);
break;
case OP_FDIV.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.div);
break;
case OP_DIVI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.idiv);
break;
case OP_FDIVI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.div);
break;
case OP_MOD.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.mod);
break;
case OP_MODI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.mod);
break;
// bit operations
case OP_AND.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.and);
break;
case OP_ANDI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.and);
break;
case OP_OR.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.or);
break;
case OP_ORI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.or);
break;
case OP_XOR.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.xor);
break;
case OP_XORI.code:
this.do_numeric_op_with_literal(arg0, arg1, numeric_ops.xor);
break;
// shift operations
case OP_SHIFT_LEFT.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.shl);
break;
case OP_SHIFT_RIGHT.code:
this.do_numeric_op_with_register(arg0, arg1, numeric_ops.shr);
break;
default:
throw new Error(`Unsupported instruction: ${inst.opcode.mnemonic}.`);
}
if (exec.stack.length) {
const top = exec.stack_top();
// advance instruction "pointer"
if (exec.call_stack.length) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx] as InstructionSegment;
// move to next instruction
if (++top.inst_idx >= segment.instructions.length) {
top.seg_idx++;
top.inst_idx = 0;
// segment ended, move to next segment
if (++top.seg_idx >= this.object_code.length) {
// eof
this.thread.splice(this.thread_idx, 1);
} else {
top.inst_idx = 0;
}
}
}
@ -174,6 +331,14 @@ export class VirtualMachine {
this.registers.setUint32(REGISTER_SIZE * reg, value);
}
private do_numeric_op_with_register(reg1: number, reg2: number, op: BinaryNumericOperation): void {
this.do_numeric_op_with_literal(reg1, this.get_sint(reg2), op);
}
private do_numeric_op_with_literal(reg: number, literal: number, op: BinaryNumericOperation): void {
this.set_sint(reg, op(this.get_sint(reg), literal));
}
private push_call_stack(exec: Thread, label: number): void {
const seg_idx = this.label_to_seg_idx.get(label);
@ -189,29 +354,58 @@ export class VirtualMachine {
}.`,
);
} else {
exec.stack.push(new StackElement(seg_idx, -1));
exec.call_stack.push(new ExecutionLocation(seg_idx, -1));
}
}
}
private pop_call_stack(idx: number, exec: Thread): void {
exec.stack.pop();
exec.call_stack.pop();
if (exec.stack.length >= 1) {
const top = exec.stack_top();
if (exec.call_stack.length >= 1) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx];
if (!segment || segment.type !== SegmentType.Instructions) {
throw new Error(`Invalid segment index ${top.seg_idx}.`);
}
} else {
// popped off the last return address
// which means this is the end of the function this thread was started on
// which means this is the end of this thread
this.thread.splice(idx, 1);
}
}
private jump_to_label(exec: Thread, label: number) {
const top = exec.call_stack_top();
const seg_idx = this.label_to_seg_idx.get(label);
if (seg_idx == undefined) {
logger.warn(`Invalid jump label: ${label}.`);
} else {
top.seg_idx = seg_idx;
top.inst_idx = -1;
}
}
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 get_next_instruction_from_thread(exec: Thread): Instruction {
if (exec.stack.length) {
const top = exec.stack_top();
if (exec.call_stack.length) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx];
if (!segment || segment.type !== SegmentType.Instructions) {
@ -237,7 +431,7 @@ export class VirtualMachine {
}
}
class StackElement {
class ExecutionLocation {
constructor(public seg_idx: number, public inst_idx: number) {}
}
@ -245,18 +439,19 @@ class Thread {
/**
* Call stack. The top element describes the instruction about to be executed.
*/
public stack: StackElement[] = [];
public call_stack: ExecutionLocation[] = [];
public arg_stack: Arg[] = [];
/**
* Global or floor-local?
*/
public global: boolean;
stack_top(): StackElement {
return this.stack[this.stack.length - 1];
call_stack_top(): ExecutionLocation {
return this.call_stack[this.call_stack.length - 1];
}
constructor(next: StackElement, global: boolean) {
this.stack = [next];
constructor(next: ExecutionLocation, global: boolean) {
this.call_stack = [next];
this.global = global;
}
}

View File

@ -28,6 +28,7 @@ import { RemoveEntityAction } from "../actions/RemoveEntityAction";
import { Euler, Vector3 } from "three";
import { vec3_to_threejs } from "../../core/rendering/conversion";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { VirtualMachine, ExecutionResult } from "../scripting/vm";
import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
@ -350,6 +351,35 @@ export class QuestEditorStore implements Disposable {
logger.warn(`Section ${entity.section_id.val} not found.`);
}
};
run_current_quest_in_vm = () => {
logger.setLevel(logger.TRACE);
const quest = this.current_quest.val;
if (!quest) {
throw new Error("No quest");
}
const vm = new VirtualMachine();
vm.load_object_code(quest.object_code);
vm.start_thread(0);
exec_loop:
while (true) {
const exec_result = vm.execute();
switch (exec_result) {
case ExecutionResult.Ok:
break;
case ExecutionResult.WaitingVsync:
vm.vsync();
break;
case ExecutionResult.Halted:
break exec_loop;
}
}
}
}
export const quest_editor_store = new QuestEditorStore();