perf: performance improvments

- switch to BTreeMap for consistant ordering of fields (less UI updates)
- cache calls to expensive getters in the vm via witha Proxy on
  DeviceRefs
- have DeviceMixin explicitly subscribe to device property changes to
  limit updates
- split fields into seperate componate to avoid rerender of other
  components
- speedup ic10emu_wasm DeviceRef::get_slots by only calling serde once.

Signed-off-by: Rachel Powers <508861+Ryex@users.noreply.github.com>
This commit is contained in:
Rachel Powers
2024-04-25 20:38:03 -07:00
parent 2480a08ada
commit cfa240c579
16 changed files with 427 additions and 212 deletions

View File

@@ -4,7 +4,7 @@ use crate::{
network::{CableConnectionType, Connection},
vm::VM,
};
use std::{collections::HashMap, ops::Deref};
use std::{collections::BTreeMap, ops::Deref};
use itertools::Itertools;
@@ -32,7 +32,7 @@ pub struct SlotOccupant {
pub max_quantity: u32,
pub sorting_class: SortingClass,
pub damage: f64,
fields: HashMap<SlotLogicType, LogicField>,
fields: BTreeMap<SlotLogicType, LogicField>,
}
impl SlotOccupant {
@@ -71,7 +71,7 @@ impl SlotOccupant {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotOccupantTemplate {
pub id: Option<u32>,
pub fields: HashMap<SlotLogicType, LogicField>,
pub fields: BTreeMap<SlotLogicType, LogicField>,
}
impl SlotOccupant {
@@ -83,7 +83,7 @@ impl SlotOccupant {
max_quantity: 1,
damage: 0.0,
sorting_class: SortingClass::Default,
fields: HashMap::new(),
fields: BTreeMap::new(),
}
}
@@ -106,13 +106,13 @@ impl SlotOccupant {
}
/// chainable constructor
pub fn with_fields(mut self, fields: HashMap<SlotLogicType, LogicField>) -> Self {
pub fn with_fields(mut self, fields: BTreeMap<SlotLogicType, LogicField>) -> Self {
self.fields.extend(fields);
self
}
/// chainable constructor
pub fn get_fields(&self) -> HashMap<SlotLogicType, LogicField> {
pub fn get_fields(&self) -> BTreeMap<SlotLogicType, LogicField> {
let mut copy = self.fields.clone();
copy.insert(
SlotLogicType::PrefabHash,
@@ -234,7 +234,7 @@ impl Slot {
}
}
pub fn get_fields(&self) -> HashMap<SlotLogicType, LogicField> {
pub fn get_fields(&self) -> BTreeMap<SlotLogicType, LogicField> {
let mut copy = self
.occupant
.as_ref()
@@ -546,10 +546,10 @@ pub struct Device {
pub name_hash: Option<i32>,
pub prefab: Option<Prefab>,
pub slots: Vec<Slot>,
pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
pub reagents: BTreeMap<ReagentMode, BTreeMap<i32, f64>>,
pub ic: Option<u32>,
pub connections: Vec<Connection>,
fields: HashMap<LogicType, LogicField>,
fields: BTreeMap<LogicType, LogicField>,
}
impl Device {
@@ -559,9 +559,9 @@ impl Device {
name: None,
name_hash: None,
prefab: None,
fields: HashMap::new(),
fields: BTreeMap::new(),
slots: Vec::new(),
reagents: HashMap::new(),
reagents: BTreeMap::new(),
ic: None,
connections: vec![Connection::CableNetwork {
net: None,
@@ -617,7 +617,7 @@ impl Device {
device
}
pub fn get_fields(&self, vm: &VM) -> HashMap<LogicType, LogicField> {
pub fn get_fields(&self, vm: &VM) -> BTreeMap<LogicType, LogicField> {
let mut copy = self.fields.clone();
if let Some(ic_id) = &self.ic {
let ic = vm.ics.get(ic_id).expect("our own ic to exist").borrow();
@@ -819,7 +819,7 @@ impl Device {
&self,
index: f64,
vm: &VM,
) -> Result<HashMap<SlotLogicType, LogicField>, ICError> {
) -> Result<BTreeMap<SlotLogicType, LogicField>, ICError> {
let slot = self
.slots
.get(index as usize)
@@ -908,9 +908,9 @@ pub struct DeviceTemplate {
pub name: Option<String>,
pub prefab_name: Option<String>,
pub slots: Vec<SlotTemplate>,
// pub reagents: HashMap<ReagentMode, HashMap<i32, f64>>,
// pub reagents: BTreeMap<ReagentMode, BTreeMap<i32, f64>>,
pub connections: Vec<Connection>,
pub fields: HashMap<LogicType, LogicField>,
pub fields: BTreeMap<LogicType, LogicField>,
}
impl Device {
@@ -959,7 +959,7 @@ impl Device {
prefab: template.prefab_name.map(|name| Prefab::new(&name)),
slots,
// reagents: template.reagents,
reagents: HashMap::new(),
reagents: BTreeMap::new(),
ic,
connections: template.connections,
fields,

View File

@@ -29,7 +29,7 @@ pub mod generated {
fn try_from(value: f64) -> Result<Self, <LogicType as TryFrom<f64>>::Error> {
if let Some(lt) = LogicType::iter().find(|lt| {
lt.get_str("value")
.map(|val| val.parse::<u8>().unwrap() as f64 == value)
.map(|val| val.parse::<u16>().unwrap() as f64 == value)
.unwrap_or(false)
}) {
Ok(lt)

View File

@@ -2,7 +2,7 @@ use core::f64;
use serde::{Deserialize, Serialize};
use std::{cell::{Cell, RefCell}, ops::Deref, string::ToString};
use std::{
collections::{HashMap, HashSet},
collections::{BTreeMap, HashSet},
error::Error,
fmt::Display,
u32,
@@ -13,8 +13,7 @@ use itertools::Itertools;
use time::format_description;
use crate::{
grammar::{self, ParseError},
vm::VM,
device::SlotType, grammar::{self, LogicType, ParseError, SlotLogicType}, vm::VM
};
use serde_with::serde_as;
@@ -190,8 +189,8 @@ pub struct IC {
/// Instruction Count since last yield
pub ic: Cell<u16>,
pub stack: RefCell<[f64; 512]>,
pub aliases: RefCell<HashMap<String, grammar::Operand>>,
pub defines: RefCell<HashMap<String, f64>>,
pub aliases: RefCell<BTreeMap<String, grammar::Operand>>,
pub defines: RefCell<BTreeMap<String, f64>>,
pub pins: RefCell<[Option<u32>; 6]>,
pub code: RefCell<String>,
pub program: RefCell<Program>,
@@ -210,8 +209,8 @@ pub struct FrozenIC {
pub ic: u16,
#[serde_as(as = "[_; 512]")]
pub stack: [f64; 512],
pub aliases: HashMap<String, grammar::Operand>,
pub defines: HashMap<String, f64>,
pub aliases: BTreeMap<String, grammar::Operand>,
pub defines: BTreeMap<String, f64>,
pub pins: [Option<u32>; 6],
pub state: ICState,
pub code: String,
@@ -261,7 +260,7 @@ impl From<FrozenIC> for IC {
pub struct Program {
pub instructions: Vec<grammar::Instruction>,
pub errors: Vec<ICError>,
pub labels: HashMap<String, u32>,
pub labels: BTreeMap<String, u32>,
}
impl Default for Program {
@@ -275,14 +274,14 @@ impl Program {
Program {
instructions: Vec::new(),
errors: Vec::new(),
labels: HashMap::new(),
labels: BTreeMap::new(),
}
}
pub fn try_from_code(code: &str) -> Result<Self, ICError> {
let parse_tree = grammar::parse(code)?;
let mut labels_set = HashSet::new();
let mut labels = HashMap::new();
let mut labels = BTreeMap::new();
let errors = Vec::new();
let instructions = parse_tree
.into_iter()
@@ -320,7 +319,7 @@ impl Program {
pub fn from_code_with_invalid(code: &str) -> Self {
let parse_tree = grammar::parse_with_invlaid(code);
let mut labels_set = HashSet::new();
let mut labels = HashMap::new();
let mut labels = BTreeMap::new();
let mut errors = Vec::new();
let instructions = parse_tree
.into_iter()
@@ -380,8 +379,8 @@ impl IC {
pins: RefCell::new([None; 6]),
program: RefCell::new(Program::new()),
code: RefCell::new(String::new()),
aliases: RefCell::new(HashMap::new()),
defines: RefCell::new(HashMap::new()),
aliases: RefCell::new(BTreeMap::new()),
defines: RefCell::new(BTreeMap::new()),
state: RefCell::new(ICState::Start),
}
}
@@ -391,8 +390,8 @@ impl IC {
self.ic.replace(0);
self.registers.replace([0.0; 18]);
self.stack.replace([0.0; 512]);
self.aliases.replace(HashMap::new());
self.defines.replace(HashMap::new());
self.aliases.replace(BTreeMap::new());
self.defines.replace(BTreeMap::new());
self.state.replace(ICState::Start);
}
@@ -522,6 +521,16 @@ impl IC {
}
}
pub fn propgate_line_number(&self, vm: &VM) {
if let Some(device) = vm.devices.get(&self.device) {
let mut device_ref = device.borrow_mut();
let _ = device_ref.set_field(LogicType::LineNumber, self.ip.get() as f64, vm, true);
if let Some(slot) = device_ref.slots.iter_mut().find(|slot| slot.typ == SlotType::ProgrammableChip) {
let _ = slot.set_field(SlotLogicType::LineNumber, self.ip.get() as f64, true);
}
}
}
/// processes one line of the contained program
pub fn step(&self, vm: &VM, advance_ip_on_err: bool) -> Result<bool, LineError> {
// TODO: handle sleep
@@ -2552,6 +2561,7 @@ impl IC {
if result.is_ok() || advance_ip_on_err {
self.ic.set(self.ic.get() + 1);
self.set_ip(next_ip);
self.propgate_line_number(vm);
}
result
}

View File

@@ -6,7 +6,7 @@ use crate::{
};
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
collections::{BTreeMap, HashSet},
rc::Rc,
};
@@ -40,9 +40,9 @@ pub enum VMError {
#[derive(Debug)]
pub struct VM {
pub ics: HashMap<u32, Rc<RefCell<interpreter::IC>>>,
pub devices: HashMap<u32, Rc<RefCell<Device>>>,
pub networks: HashMap<u32, Rc<RefCell<Network>>>,
pub ics: BTreeMap<u32, Rc<RefCell<interpreter::IC>>>,
pub devices: BTreeMap<u32, Rc<RefCell<Device>>>,
pub networks: BTreeMap<u32, Rc<RefCell<Network>>>,
pub default_network: u32,
id_space: IdSpace,
network_id_space: IdSpace,
@@ -64,12 +64,12 @@ impl VM {
let mut network_id_space = IdSpace::default();
let default_network_key = network_id_space.next();
let default_network = Rc::new(RefCell::new(Network::new(default_network_key)));
let mut networks = HashMap::new();
let mut networks = BTreeMap::new();
networks.insert(default_network_key, default_network);
let mut vm = VM {
ics: HashMap::new(),
devices: HashMap::new(),
ics: BTreeMap::new(),
devices: BTreeMap::new(),
networks,
default_network: default_network_key,
id_space: id_gen,

View File

@@ -13,6 +13,7 @@ use types::{Registers, Stack};
use std::{cell::RefCell, rc::Rc, str::FromStr};
use itertools::Itertools;
// use std::iter::FromIterator;
// use itertools::Itertools;
use wasm_bindgen::prelude::*;
@@ -86,17 +87,9 @@ impl DeviceRef {
serde_wasm_bindgen::to_value(&self.device.borrow().get_fields(&self.vm.borrow())).unwrap()
}
#[wasm_bindgen(getter, skip_typescript)]
pub fn slots(&self) -> Vec<JsValue> {
self.device
.borrow()
.slots
.iter()
.map(|slot| {
let flat_slot: types::Slot = slot.into();
serde_wasm_bindgen::to_value(&flat_slot).unwrap()
})
.collect_vec()
#[wasm_bindgen(getter)]
pub fn slots(&self) -> types::Slots {
types::Slots::from_iter(self.device.borrow().slots.iter())
}
#[wasm_bindgen(getter, skip_typescript)]
@@ -490,9 +483,17 @@ impl VMRef {
}
#[wasm_bindgen(js_name = "setSlotOccupant", skip_typescript)]
pub fn set_slot_occupant(&self, id: u32, index: usize, template: JsValue) -> Result<(), JsError> {
pub fn set_slot_occupant(
&self,
id: u32,
index: usize,
template: JsValue,
) -> Result<(), JsError> {
let template: SlotOccupantTemplate = serde_wasm_bindgen::from_value(template)?;
Ok(self.vm.borrow_mut().set_slot_occupant(id, index, template)?)
Ok(self
.vm
.borrow_mut()
.set_slot_occupant(id, index, template)?)
}
#[wasm_bindgen(js_name = "removeSlotOccupant")]

View File

@@ -1,8 +1,9 @@
#![allow(non_snake_case)]
// use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::BTreeMap;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use tsify::Tsify;
@@ -18,14 +19,16 @@ pub struct Stack(#[serde_as(as = "[_; 512]")] pub [f64; 512]);
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Registers(#[serde_as(as = "[_; 18]")] pub [f64; 18]);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde_as]
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct SlotOccupant {
pub id: u32,
pub prefab_hash: i32,
pub quantity: u32,
pub max_quantity: u32,
pub damage: f64,
pub fields: HashMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
pub fields: BTreeMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
}
impl From<&ic10emu::device::SlotOccupant> for SlotOccupant {
@@ -41,11 +44,13 @@ impl From<&ic10emu::device::SlotOccupant> for SlotOccupant {
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde_as]
#[derive(Tsify, Debug, Clone, Default, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Slot {
pub typ: ic10emu::device::SlotType,
pub occupant: Option<SlotOccupant>,
pub fields: HashMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
pub fields: BTreeMap<ic10emu::grammar::SlotLogicType, ic10emu::device::LogicField>,
}
impl From<&ic10emu::device::Slot> for Slot {
@@ -58,6 +63,17 @@ impl From<&ic10emu::device::Slot> for Slot {
}
}
#[serde_as]
#[derive(Tsify, Debug, Clone, Serialize, Deserialize)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct Slots(pub Vec<Slot>);
impl<'a> FromIterator<&'a ic10emu::device::Slot> for Slots {
fn from_iter<T: IntoIterator<Item = &'a ic10emu::device::Slot>>(iter: T) -> Self {
Slots(iter.into_iter().map(|slot| slot.into()).collect_vec())
}
}
include!(concat!(env!("OUT_DIR"), "/ts_types.rs"));
// #[serde_as]

View File

@@ -7,19 +7,19 @@ export interface LogicField {
export type LogicFields = Map<LogicType, LogicField>;
export type SlotLogicFields = Map<SlotLogicType, LogicField>;
export interface SlotOccupant {
readonly id: number;
readonly prefab_hash: number;
readonly quantity: number;
readonly max_quantity: number;
readonly damage: number;
readonly fields: SlotLogicFields;
}
export interface Slot {
readonly typ: SlotType;
readonly occupant: SlotOccupant | undefined;
readonly fields: SlotLogicFields;
}
// export interface SlotOccupant {
// readonly id: number;
// readonly prefab_hash: number;
// readonly quantity: number;
// readonly max_quantity: number;
// readonly damage: number;
// readonly fields: SlotLogicFields;
// }
// export interface Slot {
// readonly typ: SlotType;
// readonly occupant: SlotOccupant | undefined;
// readonly fields: SlotLogicFields;
// }
export type Reagents = Map<string, Map<number, number>>;

View File

@@ -124,7 +124,20 @@ export const demoVMState: VMState = {
},
},
],
fields: {},
fields: {
"PrefabHash": {
field_type: "Read",
value: -128473777,
},
"Setting": {
field_type: "ReadWrite",
value: 0,
},
"RequiredPower": {
field_type: "Read",
value: 0,
}
},
},
],
networks: [

View File

@@ -12,6 +12,7 @@ import type {
Aliases,
Defines,
Pins,
LogicType,
} from "ic10emu_wasm";
import { structuralEqual } from "utils";
import { LitElement, PropertyValueMap } from "lit";
@@ -21,6 +22,7 @@ type Constructor<T = {}> = new (...args: any[]) => T;
export declare class VMDeviceMixinInterface {
deviceID: number;
activeICId: number;
device: DeviceRef;
name: string | null;
nameHash: number | null;
@@ -41,8 +43,24 @@ export declare class VMDeviceMixinInterface {
_handleDeviceModified(e: CustomEvent): void;
updateDevice(): void;
updateIC(): void;
subscribe(...sub: VMDeviceMixinSubscription[]): void;
unsubscribe(filter: (sub: VMDeviceMixinSubscription) => boolean): void;
}
export type VMDeviceMixinSubscription =
| "name"
| "nameHash"
| "prefabName"
| "fields"
| "slots"
| "slots-count"
| "reagents"
| "connections"
| "ic"
| "active-ic"
| { field: LogicType }
| { slot: number };
export const VMDeviceMixin = <T extends Constructor<LitElement>>(
superClass: T,
) => {
@@ -57,8 +75,23 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
this.updateDevice();
}
@state() private deviceSubscriptions: VMDeviceMixinSubscription[] = [];
subscribe(...sub: VMDeviceMixinSubscription[]) {
this.deviceSubscriptions = this.deviceSubscriptions.concat(sub);
}
// remove subscripotions matching the filter
unsubscribe(filter: (sub: VMDeviceMixinSubscription) => boolean) {
this.deviceSubscriptions = this.deviceSubscriptions.filter(
(sub) => !filter(sub),
);
}
device: DeviceRef;
@state() activeICId: number;
@state() name: string | null = null;
@state() nameHash: number | null = null;
@state() prefabName: string | null;
@@ -107,7 +140,6 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
this._handleDevicesModified.bind(this),
),
);
}
_handleDeviceModified(e: CustomEvent) {
@@ -115,49 +147,93 @@ export const VMDeviceMixin = <T extends Constructor<LitElement>>(
const activeIc = window.VM.vm.activeIC;
if (this.deviceID === id) {
this.updateDevice();
} else if (id === activeIc.id) {
this.requestUpdate();
} else if (id === activeIc.id && this.deviceSubscriptions.includes("active-ic")) {
this.updateDevice();
}
}
_handleDevicesModified(e: CustomEvent) {
_handleDevicesModified(e: CustomEvent<number[]>) {
const activeIc = window.VM.vm.activeIC;
const ids = e.detail;
this.requestUpdate();
if (ids.includes(this.deviceID)) {
this.updateDevice()
} else if (ids.includes(activeIc.id) && this.deviceSubscriptions.includes("active-ic")) {
this.updateDevice();
}
}
updateDevice() {
this.device = window.VM.vm.devices.get(this.deviceID)!;
const name = this.device.name ?? null;
if (this.name !== name) {
this.name = name;
}
const nameHash = this.device.nameHash ?? null;
if (this.nameHash !== nameHash) {
this.nameHash = nameHash;
}
const prefabName = this.device.prefabName ?? null;
if (this.prefabName !== prefabName) {
this.prefabName = prefabName;
}
const fields = this.device.fields;
if (!structuralEqual(this.fields, fields)) {
this.fields = fields;
}
const slots = this.device.slots;
if (!structuralEqual(this.slots, slots)) {
this.slots = slots;
}
const reagents = this.device.reagents;
if (!structuralEqual(this.reagents, reagents)) {
this.reagents = reagents;
}
const connections = this.device.connections;
if (!structuralEqual(this.connections, connections)) {
this.connections = connections;
}
if (typeof this.device.ic !== "undefined") {
this.updateIC();
for (const sub of this.deviceSubscriptions) {
if (typeof sub === "string") {
if (sub == "name") {
const name = this.device.name ?? null;
if (this.name !== name) {
this.name = name;
}
} else if (sub === "nameHash") {
const nameHash = this.device.nameHash ?? null;
if (this.nameHash !== nameHash) {
this.nameHash = nameHash;
}
} else if (sub === "prefabName") {
const prefabName = this.device.prefabName ?? null;
if (this.prefabName !== prefabName) {
this.prefabName = prefabName;
}
} else if (sub === "fields") {
const fields = this.device.fields;
if (!structuralEqual(this.fields, fields)) {
this.fields = fields;
}
} else if (sub === "slots") {
const slots = this.device.slots;
if (!structuralEqual(this.slots, slots)) {
this.slots = slots;
}
} else if (sub === "slots-count") {
const slots = this.device.slots;
if (typeof this.slots === "undefined") {
this.slots = slots;
} else if (this.slots.length !== slots.length) {
this.slots = slots;
}
} else if (sub === "reagents") {
const reagents = this.device.reagents;
if (!structuralEqual(this.reagents, reagents)) {
this.reagents = reagents;
}
} else if (sub === "connections") {
const connections = this.device.connections;
if (!structuralEqual(this.connections, connections)) {
this.connections = connections;
}
} else if (sub === "ic") {
if (typeof this.device.ic !== "undefined") {
this.updateIC();
}
} else if (sub === "active-ic") {
const activeIc = window.VM.vm?.activeIC;
if (this.activeICId !== activeIc.id) {
this.activeICId = activeIc.id;
}
}
} else {
if ( "field" in sub ) {
const fields = this.device.fields;
if (this.fields.get(sub.field) !== fields.get(sub.field)) {
this.fields = fields;
}
} else if ( "slot" in sub) {
const slots = this.device.slots;
if (typeof this.slots === "undefined" || this.slots.length < sub.slot) {
this.slots = slots;
} else if (!structuralEqual(this.slots[sub.slot], slots[sub.slot])) {
this.slots = slots;
}
}
}
}
}
@@ -224,10 +300,12 @@ export const VMActiveICMixin = <T extends Constructor<LitElement>>(
return root;
}
disconnectedCallback(): void {
window.VM.get().then((vm) =>
vm.removeEventListener("vm-run-ic", this._handleDeviceModified.bind(this)),
vm.removeEventListener(
"vm-run-ic",
this._handleDeviceModified.bind(this),
),
);
window.App.app.session.removeEventListener(
"session-active-ic",
@@ -274,7 +352,7 @@ export const VMDeviceDBMixin = <T extends Constructor<LitElement>>(
window.VM.vm.removeEventListener(
"vm-device-db-loaded",
this._handleDeviceDBLoad.bind(this),
)
);
}
_handleDeviceDBLoad(e: CustomEvent) {

View File

@@ -7,6 +7,12 @@ import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.js"
@customElement("vm-ic-controls")
export class VMICControls extends VMActiveICMixin(BaseElement) {
constructor() {
super();
this.subscribe("ic", "active-ic")
}
static styles = [
...defaultCss,
css`

View File

@@ -3,19 +3,11 @@ import { customElement, property, query, state } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils";
import {
LogicType,
Slot,
SlotLogicType,
SlotOccupant,
SlotType,
} from "ic10emu_wasm";
import { parseIntWithHexOrBinary, parseNumber } from "utils";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
import "./slot";
import { when } from "lit/directives/when.js";
import { cache } from "lit/directives/cache.js";
import "./fields";
import { until } from "lit/directives/until.js";
import { repeat } from "lit/directives/repeat.js";
@@ -30,6 +22,16 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
constructor() {
super();
this.open = false;
this.subscribe(
"prefabName",
"name",
"nameHash",
"reagents",
"slots-count",
"reagents",
"connections",
"active-ic",
);
}
static styles = [
@@ -72,13 +74,6 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
.device-name-hash::part(input) {
width: 7rem;
}
.slot-header.image {
width: 1.5rem;
height: 1.5rem;
border: var(--sl-panel-border-width) solid var(--sl-panel-border-color);
border-radius: var(--sl-border-radius-medium);
background-color: var(--sl-color-neutral-0);
}
sl-divider {
--spacing: 0.25rem;
}
@@ -139,12 +134,12 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
}
renderHeader(): HTMLTemplateResult {
const activeIc = window.VM.vm.activeIC;
const thisIsActiveIc = activeIc.id === this.deviceID;
const thisIsActiveIc = this.activeICId === this.deviceID;
const badges: HTMLTemplateResult[] = [];
if (this.deviceID == activeIc?.id) {
if (thisIsActiveIc) {
badges.push(html`<sl-badge variant="primary" pill pulse>db</sl-badge>`);
}
const activeIc = window.VM.vm.activeIC;
activeIc?.pins?.forEach((id, index) => {
if (this.deviceID == id) {
badges.push(
@@ -185,18 +180,10 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
}
renderFields() {
const fields = Array.from(this.fields.entries());
const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
return this.delayRenderTab("fields", html`
${fields.map(([name, field], _index, _fields) => {
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${displayNumber(field.value)}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`);
return this.delayRenderTab(
"fields",
html`<vm-device-fields .deviceID=${this.deviceID}></vm-device-fields>`,
);
}
_onSlotImageErr(e: Event) {
@@ -207,18 +194,20 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" as const;
async renderSlots() {
return this.delayRenderTab("slots", html`
<div class="flex flex-row flex-wrap">
${repeat(
this.slots,
(_slot, index) => index,
(_slot, index) => html`
<vm-device-slot .deviceID=${this.deviceID} .slotIndex=${index} class-"flex flex-row max-w-lg mr-2 mb-2">
</vm-device-slot>
`,
)}
</div>
`);
return this.delayRenderTab(
"slots",
html`
<div class="flex flex-row flex-wrap">
${repeat(this.slots,
(slot, index) => slot.typ + index.toString(),
(_slot, index) => html`
<vm-device-slot .deviceID=${this.deviceID} .slotIndex=${index} class-"flex flex-row max-w-lg mr-2 mb-2">
</vm-device-slot>
`,
)}
</div>
`,
);
}
renderReagents() {
@@ -242,7 +231,10 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
</sl-select>
`;
});
return this.delayRenderTab("networks", html`<div class="networks">${networks}</div>`);
return this.delayRenderTab(
"networks",
html`<div class="networks">${networks}</div>`,
);
}
renderPins() {
@@ -303,7 +295,6 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
this.tabResolves[name].resolver(this.tabResolves[name].result);
this.tabsShown.push(name);
}
}
render(): HTMLTemplateResult {
@@ -389,18 +380,6 @@ export class VMDeviceCard extends VMDeviceDBMixin(VMDeviceMixin(BaseElement)) {
this.updateDevice();
});
}
_handleChangeField(e: CustomEvent) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceField(this.deviceID, field, val, true)) {
input.value = this.fields.get(field).value.toString();
}
this.updateDevice();
});
}
_handleDeviceRemoveButton(_e: Event) {
this.removeDialog.show();
}

View File

@@ -0,0 +1,42 @@
import { html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
import { displayNumber, parseNumber } from "utils";
import type { LogicType } from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
@customElement("vm-device-fields")
export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
constructor() {
super();
this.subscribe("fields");
}
render() {
const fields = Array.from(this.fields.entries());
const inputIdBase = `vmDeviceCard${this.deviceID}Field`;
return html`
${fields.map(([name, field], _index, _fields) => {
return html` <sl-input id="${inputIdBase}${name}" key="${name}" value="${displayNumber(field.value)}" size="small"
@sl-change=${this._handleChangeField}>
<span slot="prefix">${name}</span>
<sl-copy-button slot="suffix" from="${inputIdBase}${name}.value"></sl-copy-button>
<span slot="suffix">${field.field_type}</span>
</sl-input>`;
})}
`;
}
_handleChangeField(e: CustomEvent) {
const input = e.target as SlInput;
const field = input.getAttribute("key")! as LogicType;
const val = parseNumber(input.value);
window.VM.get().then((vm) => {
if (!vm.setDeviceField(this.deviceID, field, val, true)) {
input.value = this.fields.get(field).value.toString();
}
this.updateDevice();
});
}
}

View File

@@ -1,19 +1,17 @@
import { html, css } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { customElement, property} from "lit/decorators.js";
import { BaseElement, defaultCss } from "components";
import { VMDeviceDBMixin, VMDeviceMixin } from "virtual_machine/base_device";
import type { DeviceDB, DeviceDBEntry } from "virtual_machine/device_db";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import { clamp, displayNumber, parseIntWithHexOrBinary, parseNumber } from "utils";
import {
LogicType,
Slot,
clamp,
displayNumber,
parseNumber,
} from "utils";
import {
SlotLogicType,
SlotOccupant,
SlotType,
} from "ic10emu_wasm";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js";
import { VMDeviceCard } from "./card";
import { when } from "lit/directives/when.js";
@@ -24,10 +22,23 @@ export interface SlotModifyEvent {
@customElement("vm-device-slot")
export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
@property({ type: Number }) slotIndex: number;
private _slotIndex: number;
get slotIndex() {
return this._slotIndex;
}
@property({ type: Number })
set slotIndex(val: number) {
this._slotIndex = val;
this.unsubscribe((sub) => typeof sub === "object" && "slot" in sub);
this.subscribe({ slot: val });
}
constructor() {
super();
this.subscribe("active-ic");
}
static styles = [
@@ -101,7 +112,7 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
const template = this.slotOcccupantTemplate();
const activeIc = window.VM.vm.activeIC;
const thisIsActiveIc = activeIc.id === this.deviceID;
const thisIsActiveIc = this.activeICId === this.deviceID;
const enableQuantityInput = false;
@@ -109,13 +120,13 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
<div class="flex flex-row me-2">
<div
class="relative shrink-0 border border-neutral-200/40 rounded-lg p-1
hover:ring-2 hover:ring-purple-500 hover:ring-offset-1
hover:ring-offset-purple-500 cursor-pointer me-2"
hover:ring-2 hover:ring-purple-500 hover:ring-offset-1
hover:ring-offset-purple-500 cursor-pointer me-2"
@click=${this._handleSlotClick}
>
<div
class="absolute top-0 left-0 ml-1 mt-1 text-xs
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
>
<small>${this.slotIndex}</small>
</div>
@@ -127,7 +138,7 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
() =>
html`<div
class="absolute bottom-0 right-0 mr-1 mb-1 text-xs
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
text-neutral-200/90 font-mono bg-neutral-500/40 rounded pl-1 pr-1"
>
<small
>${slot.occupant.quantity}/${slot.occupant
@@ -156,22 +167,32 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
typeof slot.occupant !== "undefined",
() => html`
<div class="quantity-input ms-auto pl-2 mt-auto mb-auto me-2">
${ enableQuantityInput ? html`
<sl-input
type="number"
size="small"
.value=${slot.occupant.quantity.toString()}
.min=${1}
.max=${slot.occupant.max_quantity}
@sl-change=${this._handleSlotQuantityChange}
${enableQuantityInput
? html` <sl-input
type="number"
size="small"
.value=${slot.occupant.quantity.toString()}
.min=${1}
.max=${slot.occupant.max_quantity}
@sl-change=${this._handleSlotQuantityChange}
>
<div slot="help-text">
<span>Max Quantity: ${slot.occupant.max_quantity}</span>
</div>
</sl-input>`
: ""}
<sl-tooltip
content=${thisIsActiveIc && slot.typ === "ProgrammableChip"
? "Removing the selected Active IC is disabled"
: "Remove Occupant"}
>
<div slot="help-text">
<span>Max Quantity: ${slot.occupant.max_quantity}</span>
</div>
</sl-input>` : "" }
<sl-tooltip content=${thisIsActiveIc ? "Removing the selected Active IC is disabled" : "Remove Occupant" }>
<sl-icon-button class="clear-occupant" name="x-octagon" label="Remove" ?disabled=${thisIsActiveIc}
@click=${this._handleSlotOccupantRemove}></sl-icon-button>
<sl-icon-button
class="clear-occupant"
name="x-octagon"
label="Remove"
?disabled=${thisIsActiveIc && slot.typ === "ProgrammableChip"}
@click=${this._handleSlotOccupantRemove}
></sl-icon-button>
</sl-tooltip>
</div>
`,
@@ -199,8 +220,18 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
const input = e.currentTarget as SlInput;
const slot = this.slots[this.slotIndex];
const val = clamp(input.valueAsNumber, 1, slot.occupant.max_quantity);
if (!window.VM.vm.setDeviceSlotField(this.deviceID, this.slotIndex, "Quantity", val, true)) {
input.value = this.device.getSlotField(this.slotIndex, "Quantity").toString();
if (
!window.VM.vm.setDeviceSlotField(
this.deviceID,
this.slotIndex,
"Quantity",
val,
true,
)
) {
input.value = this.device
.getSlotField(this.slotIndex, "Quantity")
.toString();
}
}
@@ -255,7 +286,9 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
render() {
return html`
<ic10-details class="slot-card">
<ic10-details
class="slot-card"
>
<div class="slot-header w-full" slot="summary">
${this.renderHeader()}
</div>
@@ -263,4 +296,5 @@ export class VMDeviceSlot extends VMDeviceMixin(VMDeviceDBMixin(BaseElement)) {
</ic10-details>
`;
}
}

View File

@@ -5,6 +5,7 @@ import {
LogicType,
SlotLogicType,
SlotOccupantTemplate,
Slots,
VMRef,
init,
} from "ic10emu_wasm";
@@ -20,9 +21,39 @@ export interface ToastMessage {
id: string;
}
export interface CacheDeviceRef extends DeviceRef {
dirty: boolean
}
function cachedDeviceRef(ref: DeviceRef) {
let slotsDirty = true;
let cachedSlots: Slots = undefined;
return new Proxy<DeviceRef>(ref, {
get(target, prop, receiver) {
if (prop === "slots") {
if (typeof cachedSlots === undefined || slotsDirty) {
cachedSlots = target.slots;
slotsDirty = false;
}
return cachedSlots;
} else if (prop === "dirty") {
return slotsDirty;
}
return Reflect.get(target, prop, receiver);
},
set(target, prop, value) {
if (prop === "dirty") {
slotsDirty = value
return true;
}
return Reflect.set(target, prop, value)
}
}) as CacheDeviceRef
}
class VirtualMachine extends EventTarget {
ic10vm: VMRef;
_devices: Map<number, DeviceRef>;
_devices: Map<number, CacheDeviceRef>;
_ics: Map<number, DeviceRef>;
db: DeviceDB;
@@ -93,7 +124,7 @@ class VirtualMachine extends EventTarget {
const device_ids = this.ic10vm.devices;
for (const id of device_ids) {
if (!this._devices.has(id)) {
this._devices.set(id, this.ic10vm.getDevice(id)!);
this._devices.set(id, cachedDeviceRef(this.ic10vm.getDevice(id)!));
update_flag = true;
}
}
@@ -105,6 +136,7 @@ class VirtualMachine extends EventTarget {
}
for (const [id, device] of this._devices) {
device.dirty = true;
if (typeof device.ic !== "undefined") {
if (!this._ics.has(id)) {
this._ics.set(id, device);
@@ -204,11 +236,13 @@ class VirtualMachine extends EventTarget {
);
}
}, this);
this.updateDevice(this.activeIC, save);
this.updateDevice(this.activeIC.id, save);
if (save) this.app.session.save();
}
updateDevice(device: DeviceRef, save: boolean = true) {
updateDevice(id: number, save: boolean = true) {
const device = this._devices.get(id);
device.dirty = true;
this.dispatchEvent(
new CustomEvent("vm-device-modified", { detail: device.id }),
);
@@ -248,7 +282,7 @@ class VirtualMachine extends EventTarget {
const ic = this.activeIC!;
try {
ic.setRegister(index, val);
this.updateDevice(ic);
this.updateDevice(ic.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -260,7 +294,7 @@ class VirtualMachine extends EventTarget {
const ic = this.activeIC!;
try {
ic!.setStack(addr, val);
this.updateDevice(ic);
this.updateDevice(ic.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -296,7 +330,7 @@ class VirtualMachine extends EventTarget {
if (device) {
try {
device.setField(field, val, force);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -317,7 +351,7 @@ class VirtualMachine extends EventTarget {
if (device) {
try {
device.setSlotField(slot, field, val, force);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -335,7 +369,7 @@ class VirtualMachine extends EventTarget {
if (typeof device !== "undefined") {
try {
this.ic10vm.setDeviceConnection(id, conn, val);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -349,7 +383,7 @@ class VirtualMachine extends EventTarget {
if (typeof device !== "undefined") {
try {
this.ic10vm.setPin(id, pin, val);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -370,7 +404,7 @@ class VirtualMachine extends EventTarget {
try {
console.log("adding device", template);
const id = this.ic10vm.addDeviceFromTemplate(template);
this._devices.set(id, this.ic10vm.getDevice(id)!);
this._devices.set(id, cachedDeviceRef(this.ic10vm.getDevice(id)!));
const device_ids = this.ic10vm.devices;
this.dispatchEvent(
new CustomEvent("vm-devices-update", {
@@ -402,7 +436,7 @@ class VirtualMachine extends EventTarget {
try {
console.log("setting slot occupant", template);
this.ic10vm.setSlotOccupant(id, index, template);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);
@@ -416,7 +450,7 @@ class VirtualMachine extends EventTarget {
if (typeof device !== "undefined") {
try {
this.ic10vm.removeSlotOccupant(id, index);
this.updateDevice(device);
this.updateDevice(device.id);
return true;
} catch (err) {
this.handleVmError(err);

View File

@@ -40,6 +40,7 @@ export class VMICRegisters extends VMActiveICMixin(BaseElement) {
constructor() {
super();
this.subscribe("ic", "active-ic")
}
protected render() {

View File

@@ -37,6 +37,7 @@ export class VMICStack extends VMActiveICMixin(BaseElement) {
constructor() {
super();
this.subscribe("ic", "active-ic")
}
protected render() {