Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
- Bumped many dependencies
- Add `Image::build` function to build an image from a `Vec<T>` and
`ImageSizeLike`, returning an error if the data size is incorrect.
- Revamping `fidget-bytecode`
- Reserve register `u8::MAX` to represent an inline immediate
- Remove separate opcodes, e.g. `SubRegReg`, `SubRegImm`, and `SubImmReg`
now all generate `BytecodeOp::Sub` (using the reserved register as needed
for immediates).
- This also removes the `enum RegOpDiscriminants` from `fidget_core`
Comment thread
mkeeter marked this conversation as resolved.

# 0.4.2
- Change `depth` member in `GeometryPixel` from `u32` to `f32` ([#381](https://github.com/mkeeter/fidget/pull/381))
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions demos/web-editor/crate/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions fidget-bytecode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ rust-version.workspace = true
fidget-core.workspace = true
workspace-hack.workspace = true

serde.workspace = true
strum.workspace = true
thiserror.workspace = true
zerocopy.workspace = true
234 changes: 166 additions & 68 deletions fidget-bytecode/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,7 @@
//! user-defined semantics, as long as the immediate is not either reserved
//! value.
//!
//! ## Register-only operations
//!
//! Register-only operations (i.e. opcodes without an immediate `f32` or `u32`)
//! are packed into a single `u32` as follows:
//! Operations are packed into the first `u32` as follows:
//!
//! | Byte | Value |
//! |------|---------------------------------------------|
Expand All @@ -32,49 +29,127 @@
//! | 2 | first input register |
//! | 3 | second input register |
//!
//! Depending on the opcode, the input register bytes may not be used.
//!
//! The second word is always `0xFF000000`
//!
//! ## Operations with an `f32` immediate
//!
//! Operations with an `f32` immediate are packed into two `u32` words.
//! The first word is similar to before:
//!
//! | Byte | Value |
//! |------|---------------------------------------------|
//! | 0 | opcode |
//! | 1 | output register |
//! | 2 | first input register |
//! | 3 | not used |
//!
//! The second word is the `f32` reinterpreted as a `u32`.
//! The opcode byte is generated automatically from [`BytecodeOp`] tags.
//!
//! ## Operations with an `u32` immediate
//!
//! Operations with a `u32` immediate (e.g.
//! [`Load`](RegOp::Load)) are also packed into two `u32`
//! words. The first word is what you'd expect:
//!
//! | Byte | Value |
//! |------|---------------------------------------------|
//! | 0 | opcode |
//! | 1 | input or output register |
//! | 2 | not used |
//! | 3 | not used |
//!
//! The second word is the `u32` immediate.
//! Depending on the opcode, the input register bytes may not be used.
//!
//! ## Opcode values
//! An input register byte of `0xFF` indicates that the second word should be
//! used as an immediate value; the `u32` should be bitcast to an `f32`.
//!
//! Opcode values are generated automatically from [`BytecodeOp`]
//! values, which are one-to-one with [`RegOp`] variants.
//! [`Load`](RegOp::Load) and [`Store`](RegOp::Store) are implemented with
//! [`BytecodeOp::Mem`], using the immediate flag `0xFF` to indicate whether the
//! operation reads or writes to memory. The second word is the `u32` immediate
//! representing a memory slot.

#![warn(missing_docs)]

use fidget_core::{compiler::RegOp, vm::VmData};
use zerocopy::IntoBytes;

pub use fidget_core::compiler::RegOpDiscriminants as BytecodeOp;
/// Error type for bytecode builder
#[derive(thiserror::Error, Debug, PartialEq)]
#[error("register 255 is reserved")]
pub struct ReservedRegister;

Comment thread
mkeeter marked this conversation as resolved.
/// Operations in the bytecode tape
#[derive(
Copy,
Clone,
Debug,
PartialEq,
serde::Serialize,
serde::Deserialize,
strum::EnumIter,
strum::EnumCount,
strum::IntoStaticStr,
strum::FromRepr,
)]
#[expect(missing_docs)]
#[repr(u8)]
pub enum BytecodeOp {
Comment thread
mkeeter marked this conversation as resolved.
Output,
Input,
Copy,
Neg,
Abs,
Recip,
Sqrt,
Square,
Floor,
Ceil,
Round,
Not,
Sin,
Cos,
Tan,
Asin,
Acos,
Atan,
Exp,
Ln,
Add,
Sub,
Mul,
Div,
Atan2,
Compare,
Mod,
Min,
Max,
And,
Or,
Mem,
}

impl From<RegOp> for BytecodeOp {
fn from(op: RegOp) -> Self {
match op {
RegOp::Input(..) => BytecodeOp::Input,
RegOp::Output(..) => BytecodeOp::Output,
RegOp::NegReg(..) => BytecodeOp::Neg,
RegOp::AbsReg(..) => BytecodeOp::Abs,
RegOp::RecipReg(..) => BytecodeOp::Recip,
RegOp::SqrtReg(..) => BytecodeOp::Sqrt,
RegOp::SquareReg(..) => BytecodeOp::Square,
RegOp::FloorReg(..) => BytecodeOp::Floor,
RegOp::CeilReg(..) => BytecodeOp::Ceil,
RegOp::RoundReg(..) => BytecodeOp::Round,
RegOp::SinReg(..) => BytecodeOp::Sin,
RegOp::CosReg(..) => BytecodeOp::Cos,
RegOp::TanReg(..) => BytecodeOp::Tan,
RegOp::AsinReg(..) => BytecodeOp::Asin,
RegOp::AcosReg(..) => BytecodeOp::Acos,
RegOp::AtanReg(..) => BytecodeOp::Atan,
RegOp::ExpReg(..) => BytecodeOp::Exp,
RegOp::LnReg(..) => BytecodeOp::Ln,
RegOp::NotReg(..) => BytecodeOp::Not,
RegOp::Load(..) | RegOp::Store(..) => BytecodeOp::Mem,
RegOp::CopyImm(..) | RegOp::CopyReg(..) => BytecodeOp::Copy,

RegOp::AddRegReg(..) | RegOp::AddRegImm(..) => BytecodeOp::Add,
RegOp::MulRegReg(..) | RegOp::MulRegImm(..) => BytecodeOp::Mul,
RegOp::DivRegReg(..)
| RegOp::DivRegImm(..)
| RegOp::DivImmReg(..) => BytecodeOp::Div,
RegOp::SubRegReg(..)
| RegOp::SubRegImm(..)
| RegOp::SubImmReg(..) => BytecodeOp::Sub,
RegOp::AtanRegReg(..)
| RegOp::AtanRegImm(..)
| RegOp::AtanImmReg(..) => BytecodeOp::Atan2,
RegOp::MinRegReg(..) | RegOp::MinRegImm(..) => BytecodeOp::Min,
RegOp::MaxRegReg(..) | RegOp::MaxRegImm(..) => BytecodeOp::Max,
RegOp::CompareRegReg(..)
| RegOp::CompareRegImm(..)
| RegOp::CompareImmReg(..) => BytecodeOp::Compare,
RegOp::ModRegReg(..)
| RegOp::ModRegImm(..)
| RegOp::ModImmReg(..) => BytecodeOp::Mod,
RegOp::AndRegReg(..) | RegOp::AndRegImm(..) => BytecodeOp::And,
RegOp::OrRegReg(..) | RegOp::OrRegImm(..) => BytecodeOp::Or,
}
}
}

/// Serialized bytecode for external evaluation
pub struct Bytecode {
Expand All @@ -96,6 +171,8 @@ impl Bytecode {
}

/// Maximum register index used by the tape
///
/// This does not include the virtual register `0xFF` used for immediates
pub fn reg_count(&self) -> u8 {
self.reg_count
}
Expand All @@ -111,33 +188,49 @@ impl Bytecode {
}

/// Builds a new bytecode object from VM data
pub fn new<const N: usize>(t: &VmData<N>) -> Self {
///
/// Returns an error if the reserved register (255) is in use
pub fn new<const N: usize>(
t: &VmData<N>,
) -> Result<Self, ReservedRegister> {
// The initial opcode is `OP_JUMP 0x0000_0000`
let mut data = vec![u32::MAX, 0u32];
let mut reg_count = 0u8;
let mut mem_count = 0u32;
for op in t.iter_asm() {
let r = BytecodeOp::from(op);
let mut word = [r as u8, 0xFF, 0xFF, 0xFF];
let mut word = [0xFF; 4];
let mut imm = None;
let mut store_reg = |i, r| {
reg_count = reg_count.max(r); // update the max reg
word[i] = r;
if r == u8::MAX {
Err(ReservedRegister)
} else {
reg_count = reg_count.max(r); // update the max reg
Comment thread
mkeeter marked this conversation as resolved.
word[i] = r;
Ok(())
}
};
match op {
RegOp::Input(reg, slot) | RegOp::Output(reg, slot) => {
store_reg(1, reg);
store_reg(1, reg)?;
imm = Some(slot);
}

RegOp::Load(reg, slot) | RegOp::Store(reg, slot) => {
store_reg(1, reg);
RegOp::Load(reg, slot) => {
store_reg(1, reg)?;
store_reg(2, u8::MAX)?;
mem_count = mem_count.max(slot);
imm = Some(slot);
}
RegOp::Store(reg, slot) => {
store_reg(1, u8::MAX)?;
store_reg(2, reg)?;
mem_count = mem_count.max(slot);
imm = Some(slot);
}

RegOp::CopyImm(out, imm_f32) => {
store_reg(1, out);
store_reg(1, out)?;
word[2] = u8::MAX;
imm = Some(imm_f32.to_bits());
}
RegOp::NegReg(out, reg)
Expand All @@ -158,28 +251,35 @@ impl Bytecode {
| RegOp::ExpReg(out, reg)
| RegOp::LnReg(out, reg)
| RegOp::NotReg(out, reg) => {
store_reg(1, out);
store_reg(2, reg);
store_reg(1, out)?;
store_reg(2, reg)?;
}

RegOp::AddRegImm(out, reg, imm_f32)
| RegOp::MulRegImm(out, reg, imm_f32)
| RegOp::DivRegImm(out, reg, imm_f32)
| RegOp::DivImmReg(out, reg, imm_f32)
| RegOp::SubImmReg(out, reg, imm_f32)
| RegOp::SubRegImm(out, reg, imm_f32)
| RegOp::AtanRegImm(out, reg, imm_f32)
| RegOp::AtanImmReg(out, reg, imm_f32)
| RegOp::MinRegImm(out, reg, imm_f32)
| RegOp::MaxRegImm(out, reg, imm_f32)
| RegOp::CompareRegImm(out, reg, imm_f32)
| RegOp::CompareImmReg(out, reg, imm_f32)
| RegOp::ModRegImm(out, reg, imm_f32)
| RegOp::ModImmReg(out, reg, imm_f32)
| RegOp::AndRegImm(out, reg, imm_f32)
| RegOp::OrRegImm(out, reg, imm_f32) => {
store_reg(1, out);
store_reg(2, reg);
store_reg(1, out)?;
store_reg(2, reg)?;
word[3] = u8::MAX;
imm = Some(imm_f32.to_bits());
}

RegOp::DivImmReg(out, reg, imm_f32)
| RegOp::SubImmReg(out, reg, imm_f32)
| RegOp::AtanImmReg(out, reg, imm_f32)
| RegOp::CompareImmReg(out, reg, imm_f32)
| RegOp::ModImmReg(out, reg, imm_f32) => {
store_reg(1, out)?;
store_reg(3, reg)?;
word[2] = u8::MAX;
Comment thread
mkeeter marked this conversation as resolved.
imm = Some(imm_f32.to_bits());
}

Expand All @@ -194,22 +294,23 @@ impl Bytecode {
| RegOp::ModRegReg(out, lhs, rhs)
| RegOp::AndRegReg(out, lhs, rhs)
| RegOp::OrRegReg(out, lhs, rhs) => {
store_reg(1, out);
store_reg(2, lhs);
store_reg(3, rhs);
store_reg(1, out)?;
store_reg(2, lhs)?;
store_reg(3, rhs)?;
}
}
};
word[0] = BytecodeOp::from(op) as u8;
data.push(u32::from_le_bytes(word));
data.push(imm.unwrap_or(0xFF000000));
}
// Add the final `OP_JUMP 0xFFFF_FFFF`
data.extend([u32::MAX, u32::MAX]);

Bytecode {
Ok(Bytecode {
data,
mem_count,
reg_count,
}
})
}
}

Expand All @@ -236,7 +337,7 @@ mod test {
let c = ctx.constant(1.0);
let out = ctx.add(x, c).unwrap();
let data = VmData::<255>::new(&ctx, &[out]).unwrap();
let bc = Bytecode::new(&data);
let bc = Bytecode::new(&data).unwrap();
let mut iter = bc.data.iter();
let mut next = || *iter.next().unwrap();
assert_eq!(next(), 0xFFFFFFFF); // start marker
Expand All @@ -246,10 +347,7 @@ mod test {
[BytecodeOp::Input as u8, 0, 0xFF, 0xFF]
);
assert_eq!(next(), 0); // input slot 0
assert_eq!(
next().to_le_bytes(),
[BytecodeOp::AddRegImm as u8, 0, 0, 0xFF]
);
assert_eq!(next().to_le_bytes(), [BytecodeOp::Add as u8, 0, 0, 0xFF]);
assert_eq!(f32::from_bits(next()), 1.0);
assert_eq!(
next().to_le_bytes(),
Expand Down
2 changes: 1 addition & 1 deletion fidget-core/src/compiler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod op;

mod lru;
pub(crate) use lru::Lru;
pub use op::{RegOp, RegOpDiscriminants, SsaOp};
pub use op::{RegOp, SsaOp};

mod reg_tape;
mod ssa_tape;
Expand Down
Loading
Loading