Skip to content

Commit

Permalink
Introduce qptr ("quasi-pointer") type and associated lower->analyze…
Browse files Browse the repository at this point in the history
…->lift passes. (#24)

#### `qptr` is the first major SPIR-T feature/departure from SPIR-V, and
represents a vision for memory/pointers:
* name stands for "`q`uasi-`p`oin`t`e`r`" <sub><sup>(alternatively,
"`q`uantum")</sup></sub>, in the sense of "not quite (a pointer)"
* <sub>(primarily chosen to distinguish it from both SPIR-V pointers and
LLVM-style untyped `ptr`s)</sub>
* **untyped** _as a design principle_ ("behavior given by operations,
not types")
* maximally so, not even tracking the address space ("Storage Class"),
unlike e.g. LLVM `ptr`s
* comparable to OpenCL `Generic` physical pointers (but more flexible
and without runtime behavior)
* **for Rust-GPU**, this allows faithfully representing Rust's *untyped
memory* semantics
* <sub>(Rust also lacks explicit address spaces, and `qptr` can replace
Rust-GPU's "Storage Class" inference pass)</sub>
* **for legalization/optimization**, existing approaches (from IRs with
true pointers) can be employed
* **takes advantage** of SPIR-V's "logical"/"physical" dichotomy to
**maximize** flexibility and **minimize** effort:
* <ins>**"logical pointers"**</ins>: strictly typed, but extremely
limited dataflow <sub><sup>(without `VariablePointers`)</sup></sub>
* as they <sub><sup>(without `VariablePointers`)</sup></sub> can't be
stored to memory, returned from functions or passed through `OpPhi`s,
each "logical pointer" is strictly a *static alias* of some variable,
with a maybe-dynamic offset
* <sub>(calls allow taking "logical pointer" arguments, but only
shallowly, and the pointers have to point to a whole variable, so it's
more like such functions are "templated" over the choice of
variable)</sub>
* the limitations allow *guaranteed* recovery of pointee types (and
"Storage Class"es) for *legal* usage
* <sub>this is because both forwards (def->use) and backwards (use->def)
dataflow can connect each definition to its *static* "tree" of uses
(with e.g. `qptr.offset` nodes, and `qptr.load`/`qptr.store` leaves),
and all such "trees" are *disjoint* between pointer definitions - such a
"tree"-like structure can be seen reified in the `qptr.usage` attribute
(produced by `qptr::analyze`)</sub>
* *illegal* usage, *crucially*, **must** be legalized away to ever
produce legal SPIR-V, so there is no need to ever support recreating
SPIR-V pointers from anything more than the (very restricted) legal
subset
* <ins>**"physical pointers"**</ins>: still typed, but support casts and
have no dataflow restrictions
* SPIR-V copy of [LLVM's old `T addrspace(AS)*` pointer
types](https://releases.llvm.org/14.0.0/docs/LangRef.html#pointer-type)
(via OpenCL SPIR)
* can be treated as untyped by erasing the pointee type, and keeping
only the "Storage Class"
* <sub>(the "Storage Class" only needs to be tracked on `int2ptr` casts,
loads of physical pointers etc. - *not implemented yet*)</sub>
* dedicated operations (`QPtrOp` - shown as `qptr.*` below)
* <sub>(as a fallback, for other SPIR-V instructions, attributes record
original pointee types required for an input, or the output - for output
pointers, the SPIR-V "Storage Class" is also kept)</sub>
* basic pass pipeline:
* **`qptr::lower`**: SPIR-V `OpTypePointer` types/instructions are
lowered to `qptr` and `qptr.*` ops
* `OpAccessChain`s on composite types become pointer arithmetic (erasing
those types)
* <sub>(long-term Rust-GPU can skip this step and emit `qptr` operations
directly)</sub>
  * [this is where any future legalization passes would go]
* **`qptr::analyze`**: `qptr` uses are analyzed to generate `qptr.usage`
attributes
* <sub>(by merging compatible uses, this can itself legalize *some*
`union`-like uses that were illegal in the SPIR-V input of `qptr::lower`
- Rust-GPU could use this to support many more shapes of `enum`s)</sub>
* **`qptr::lift`**: `qptr.*` ops are lifted back to SPIR-V
`OpTypePointer` types/instructions
* `qptr.usage` attributes are used to generate pointee types that
support all (transitive) uses
* <sub>(by propagating accurate "Storage Class"es, this can itself
legalize *some* incorrect "Storage Class"es in the SPIR-V input of
`qptr::lower` - Rust-GPU could use this to replace its "Storage Class"
inference pass)</sub>

---

#### `qptr`s can point to either:
* <ins>**handles**</ins>: one or more textures, samplers, *or
**buffers***
* name [inspired by
WGSL](https://gpuweb.github.io/gpuweb/wgsl/#address-space) (which sadly
only uses it for textures/samplers)
* **handle arrays** use `qptr.handle_array_index` to select a single
handle (i.e. "descriptor indexing")
* **buffers** are opaque handles, and use `qptr.buffer_data` to obtain a
`qptr` to the *memory* contents
* (comparable to using an `OpImageTexelPointer` in SPIR-V, on a pointer
to an `OpTypeImage`)
* `qptr.buffer_dyn_len` can query the size of dynamically-sized buffers
(replacing `OpArrayLength`)
* intentionally replacing SPIR-V's choice of using a mix of
`Block`-decorated `OpTypeStruct`s in specific "Storage Class"es
<sub>(`Uniform`, `StorageBuffer`, `ShaderRecordBufferKHR`)</sub> to
encode buffers "syntactically" (i.e. mimicking their GLSL declaration
syntax)
* <ins>**memory**</ins>: byte-addressable *untyped memory*
* the memory is either defined in the shader (global/local vars), or
obtained from handles etc.
  * pointer arithmetic uses `qptr.offset` and `qptr.dyn_offset`
* again more untyped than LLVM (whose GEP was copied by SPIR-V into
`OpAccessChain`)
* to avoid separate multiplications, `qptr.dyn_offset` includes an
immediate stride <sub>(in the extreme this could be generalized to a
kind of "integer dot product", if N-dimensional arrays ever require
it)</sub>
* reading/writing typed values from/to memory uses
`qptr.load`/`qptr.store`

---

#### Example (`qptr` passes on `kajiya`'s
[`assets/shaders/wrc/wrc_see_through.rgen.hlsl`](https://github.com/EmbarkStudios/kajiya/blob/5a124d58b8fc8795d37c933168c4ce98e986ce4c/assets/shaders/wrc/wrc_see_through.rgen.hlsl)):


![image](https://user-images.githubusercontent.com/77424/225182821-888c1071-4a04-4dce-88de-ba5f5a13bda5.png)
*(type information like `OpMemberName` can be seen to be erased, and
only used offsets are recreated)*


![image](https://user-images.githubusercontent.com/77424/225184001-b707b28b-f325-4893-9316-e15688b6942e.png)
*(`qptr.*` ops can be seen as blue against the backdrop of orange
`spv.*` ops, thanks to #21)*

---

_**FIXME(@eddyb):** move/copy some of this description into the library
documentation (maybe a design document?)_
  • Loading branch information
eddyb committed Apr 21, 2023
2 parents 1ee178d + dfc3911 commit bc539b8
Show file tree
Hide file tree
Showing 23 changed files with 5,451 additions and 73 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] - ReleaseDate

### Added ⭐
- [PR#24](https://github.com/EmbarkStudios/spirt/pull/24) added `qptr` ("quasi-pointer") type
and associated passes to destroy and recreate pointer-related type information
(see [PR#24](https://github.com/EmbarkStudios/spirt/pull/24) for a much more detailed overview)
- [PR#22](https://github.com/EmbarkStudios/spirt/pull/22) added `Diag` and `Attr::Diagnostics`,
for embedding diagnostics (errors or warnings) in SPIR-T itself
- [PR#18](https://github.com/EmbarkStudios/spirt/pull/18) added anchor-based alignment
Expand Down
138 changes: 138 additions & 0 deletions examples/spv-lower-link-qptr-lift.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use std::fs;
use std::path::Path;
use std::rc::Rc;

fn main() -> std::io::Result<()> {
match &std::env::args().collect::<Vec<_>>()[..] {
[_, in_file] => {
let in_file_path = Path::new(in_file);

let save_print_plan = |suffix: &str, plan: spirt::print::Plan| {
let pretty = plan.pretty_print();
let ext = format!("{suffix}.spirt");

// FIXME(eddyb) don't allocate whole `String`s here.
fs::write(in_file_path.with_extension(&ext), pretty.to_string())?;
fs::write(
in_file_path.with_extension(ext + ".html"),
pretty
.render_to_html()
.with_dark_mode_support()
.to_html_doc(),
)
};

// FIXME(eddyb) adapt the other examples to this style.

fn eprint_duration<R>(f: impl FnOnce() -> R) -> R {
let start = std::time::Instant::now();
let r = f();
eprint!("[{:8.3}ms] ", start.elapsed().as_secs_f64() * 1000.0);
r
}

eprint_duration(|| {
let _ = spirt::spv::spec::Spec::get();
});
eprintln!("spv::spec::Spec::get");

let cx = Rc::new(spirt::Context::new());

let multi_version_printing = true;
let mut per_pass_module = vec![];
let mut after_pass = |pass, module: &spirt::Module| {
if multi_version_printing {
per_pass_module.push((pass, module.clone()));
Ok(())
} else {
save_print_plan(
&format!("after.{pass}"),
spirt::print::Plan::for_module(module),
)
}
};

let mut module =
eprint_duration(|| spirt::Module::lower_from_spv_file(cx.clone(), in_file_path))?;
eprintln!("Module::lower_from_spv_file({})", in_file_path.display());

let original_export_count = module.exports.len();
eprint_duration(|| {
spirt::passes::link::minimize_exports(&mut module, |export_key| {
matches!(export_key, spirt::ExportKey::SpvEntryPoint { .. })
})
});
eprintln!(
"link::minimize_exports: {} -> {} exports",
original_export_count,
module.exports.len()
);
//after_pass("minimize_exports", &module)?;

// HACK(eddyb) do this late enough to avoid spending time on unused
// functions, which `link::minimize_exports` makes unreachable.
eprint_duration(|| spirt::passes::legalize::structurize_func_cfgs(&mut module));
eprintln!("legalize::structurize_func_cfgs");
//after_pass("structurize_func_cfgs", &module)?;

eprint_duration(|| spirt::passes::link::resolve_imports(&mut module));
eprintln!("link::resolve_imports");
//after_pass("resolve_imports", &module)?;

// HACK(eddyb)
after_pass("", &module)?;

// HACK(eddyb) this is roughly what Rust-GPU would need.
let layout_config = &spirt::qptr::LayoutConfig {
abstract_bool_size_align: (1, 1),
logical_ptr_size_align: (4, 4),
..spirt::qptr::LayoutConfig::VULKAN_SCALAR_LAYOUT
};

eprint_duration(|| {
spirt::passes::qptr::lower_from_spv_ptrs(&mut module, layout_config)
});
eprintln!("qptr::lower_from_spv_ptrs");
after_pass("qptr::lower_from_spv_ptrs", &module)?;

eprint_duration(|| spirt::passes::qptr::analyze_uses(&mut module, layout_config));
eprintln!("qptr::analyze_uses");
after_pass("qptr::analyze_uses", &module)?;

eprint_duration(|| spirt::passes::qptr::lift_to_spv_ptrs(&mut module, layout_config));
eprintln!("qptr::lift_to_spv_ptrs");
after_pass("qptr::lift_to_spv_ptrs", &module)?;

if multi_version_printing {
// FIXME(eddyb) use a better suffix than `qptr` (or none).
save_print_plan(
"qptr",
spirt::print::Plan::for_versions(
&cx,
per_pass_module.iter().map(|(pass, module)| {
(
// HACK(eddyb)
if pass.is_empty() {
"initial".into()
} else {
format!("after {pass}")
},
module,
)
}),
),
)?;
}

//let out_file_path = in_file_path.with_extension("qptr.spv");
//eprint_duration(|| module.lift_to_spv_file(&out_file_path))?;
//eprintln!("Module::lift_to_spv_file({})", out_file_path.display());

Ok(())
}
args => {
eprintln!("Usage: {} IN", args[0]);
std::process::exit(1);
}
}
}
136 changes: 130 additions & 6 deletions src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,9 @@ impl<K: EntityOrientedMapKey<V>, V> EntityOrientedDenseMap<K, V> {
Self::default()
}

pub fn insert(&mut self, key: K, value: V) -> Option<V> {
// FIXME(eddyb) this should not allocate space unconditionally, but offer an
// API where "vacant entry" may or may not have a `&mut Option<V>` in it.
pub fn entry(&mut self, key: K) -> &mut Option<V> {
let entity = K::to_entity(key);
let (chunk_start, intra_chunk_idx) = entity.to_chunk_start_and_intra_chunk_idx();
let chunk_value_slots = self
Expand All @@ -417,7 +419,11 @@ impl<K: EntityOrientedMapKey<V>, V> EntityOrientedDenseMap<K, V> {
}

let value_slots = &mut chunk_value_slots[intra_chunk_idx];
K::get_dense_value_slot_mut(key, value_slots).replace(value)
K::get_dense_value_slot_mut(key, value_slots)
}

pub fn insert(&mut self, key: K, value: V) -> Option<V> {
self.entry(key).replace(value)
}

pub fn get(&self, key: K) -> Option<&V> {
Expand All @@ -438,6 +444,7 @@ impl<K: EntityOrientedMapKey<V>, V> EntityOrientedDenseMap<K, V> {
self.get_slot_mut(key)?.take()
}

// FIXME(eddyb) deduplicate with `entry`.
fn get_slot_mut(&mut self, key: K) -> Option<&mut Option<V>> {
let entity = K::to_entity(key);
let (chunk_start, intra_chunk_idx) = entity.to_chunk_start_and_intra_chunk_idx();
Expand Down Expand Up @@ -514,7 +521,9 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
let old_first_def = &mut defs[old_first];

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
old_first_def.prev.is_none(),
"invalid EntityList: `first->prev != None`"
Expand Down Expand Up @@ -543,7 +552,9 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
let old_last_def = &mut defs[old_last];

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
old_last_def.next.is_none(),
"invalid EntityList: `last->next != None`"
Expand All @@ -558,6 +569,49 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
});
}

/// Insert `new_node` (defined in `defs`) into `self`, before `next`.
//
// FIXME(eddyb) unify this with the other insert methods, maybe with a new
// "insert position" type?
#[track_caller]
pub fn insert_before(&mut self, new_node: E, next: E, defs: &mut EntityDefs<E>) {
let prev = defs[next].prev.replace(new_node);

let new_node_def = &mut defs[new_node];
assert!(
new_node_def.prev.is_none() && new_node_def.next.is_none(),
"EntityList::insert_before: new node already linked into a (different?) list"
);

new_node_def.prev = prev;
new_node_def.next = Some(next);

match prev {
Some(prev) => {
let old_prev_next = defs[prev].next.replace(new_node);

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
assert!(
old_prev_next == Some(next),
"invalid EntityListNode: `node->prev->next != node`"
);
}
None => {
// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
self.0.map(|this| this.first) == Some(next),
"invalid EntityList: `node->prev == None` but `node != first`"
);

self.0.as_mut().unwrap().first = new_node;
}
}
}

/// Insert all of `list_to_prepend`'s nodes at the start of `self`.
#[track_caller]
pub fn prepend(&mut self, list_to_prepend: Self, defs: &mut EntityDefs<E>) {
Expand All @@ -582,7 +636,9 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
let a_last_def = &mut defs[a.last];

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
a_last_def.next.is_none(),
"invalid EntityList: `last->next != None`"
Expand All @@ -594,7 +650,9 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
let b_first_def = &mut defs[b.first];

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
b_first_def.prev.is_none(),
"invalid EntityList: `first->prev != None`"
Expand All @@ -608,6 +666,72 @@ impl<E: sealed::Entity<Def = EntityListNode<E, D>>, D> EntityList<E> {
last: b.last,
}))
}

/// Remove `node` (defined in `defs`) from `self`.
#[track_caller]
pub fn remove(&mut self, node: E, defs: &mut EntityDefs<E>) {
// Unlink `node->{prev,next}` first (also allowing re-insertion elsewhere).
let (prev, next) = {
let node_def = &mut defs[node];
(node_def.prev.take(), node_def.next.take())
};

// Unlink `prev->next = node` (or validate `first = node`).
match prev {
Some(prev) => {
let old_prev_next = mem::replace(&mut defs[prev].next, next);

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
assert!(
old_prev_next == Some(node),
"invalid EntityListNode: `node->prev->next != node`"
);
}
None => {
// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
self.0.map(|this| this.first) == Some(node),
"invalid EntityList: `node->prev == None` but `node != first`"
);
}
}

// Unlink `next->prev = node` (or validate `last = node`).
match next {
Some(next) => {
let old_next_prev = mem::replace(&mut defs[next].prev, prev);

// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable.
assert!(
old_next_prev == Some(node),
"invalid EntityListNode: `node->next->prev != node`"
);
}
None => {
// FIXME(eddyb) this situation should be impossible anyway, as it
// involves the `EntityListNode`s links, which should be unforgeable,
// but it's still possible to keep around outdated `EntityList`s
// (should `EntityList` not implement `Copy`/`Clone` *at all*?)
assert!(
self.0.map(|this| this.last) == Some(node),
"invalid EntityList: `node->next == None` but `node != last`"
);
}
}

// Update list end-points (overwritten `first`/`last` validated above).
match (prev, next) {
(Some(_), Some(_)) => {}
(None, Some(next)) => self.0.as_mut().unwrap().first = next,
(Some(prev), None) => self.0.as_mut().unwrap().last = prev,
(None, None) => self.0 = None,
}
}
}

/// [`EntityList<E>`] iterator, but with a different API than [`Iterator`].
Expand Down
26 changes: 26 additions & 0 deletions src/func_at.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ impl<'a> Iterator for FuncAt<'a, EntityListIter<DataInst>> {
}
}

impl<'a> DoubleEndedIterator for FuncAt<'a, EntityListIter<DataInst>> {
fn next_back(&mut self) -> Option<Self::Item> {
let (prev, rest) = self.position.split_last(self.data_insts)?;
self.position = rest;
Some(self.at(prev))
}
}

impl<'a> FuncAt<'a, DataInst> {
pub fn def(self) -> &'a DataInstDef {
&self.data_insts[self.position]
Expand Down Expand Up @@ -146,6 +154,24 @@ impl<'a, P: Copy> FuncAtMut<'a, P> {
position: new_position,
}
}

/// Demote to a `FuncAt`, with the same `position`.
//
// FIXME(eddyb) maybe find a better name for this?
pub fn freeze(self) -> FuncAt<'a, P> {
let FuncAtMut {
control_regions,
control_nodes,
data_insts,
position,
} = self;
FuncAt {
control_regions,
control_nodes,
data_insts,
position,
}
}
}

impl<'a> FuncAtMut<'a, ControlRegion> {
Expand Down
Loading

0 comments on commit bc539b8

Please sign in to comment.