Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpConvertUToPtr support #767

Open
expenses opened this issue Oct 13, 2021 · 13 comments
Open

OpConvertUToPtr support #767

expenses opened this issue Oct 13, 2021 · 13 comments
Assignees
Labels
s: qptr may fix This might be fixed by the "qptr" experiment (https://github.com/EmbarkStudios/spirt/pull/24) t: enhancement A new feature or improvement to an existing one.

Comments

@expenses
Copy link
Contributor

I started looking into how OpConvertUToPtr can be implemented - initially for RuntimeArrays. Something like this works well, but is not generic:

unsafe fn load_f32_runtime_array_from_handle(handle: u64) -> &'static mut RuntimeArray<f32> {
    asm!(
        "%f32 = OpTypeFloat 32",
        "%runtime_array = OpTypeRuntimeArray %f32",
        "%runtime_array_ptr = OpTypePointer Generic %runtime_array",
        "%result = OpConvertUToPtr %runtime_array_ptr {handle}",
        "OpReturnValue %result",
        handle = in(reg) handle,
        options(noreturn)
    )
}

I know that @msiglreith implemented loading resources from handles in main...msiglreith:glace, but it looks like that requires a lot of codegen changes - ideally we should keep things at the asm! layer if possible.

Is there a way to make the above function generic over T, or will this require a few codegen changes?

@expenses expenses added the t: enhancement A new feature or improvement to an existing one. label Oct 13, 2021
@khyperia
Copy link
Contributor

I believe this will work:

unsafe fn load_runtime_array_from_handle<T>(handle: u64) -> &'static mut RuntimeArray<T> {
    let result: *mut RuntimeArray<T>;
    asm!(
        "{result} = OpConvertUToPtr typeof{result} {handle}",
        handle = in(reg) handle,
        result = out(reg) result,
    );
    &mut *result
}

However, this runs into the issue that ConvertUToPtr requires non-Logical addressing, which will require (significant?) compiler changes to support. Also, I know at least the Addresses capability is not supported on Vulkan, and I'm not sure about PhysicalStorageBufferAddresses, would have to check (one of those two things needs to be supported for ConvertUToPtr to work). But, this is at least the way to write such a thing.

(I think you could probably just straight up return &'static mut T instead of RuntimeArray, doesn't have to be specialized to RuntimeArray - and probably should return *mut T for saftey, &'static mut T is a little spooky to me~)

@khyperia
Copy link
Contributor

Here's a compiletest, in case we need it in the future (currently fails with "OpConvertUToPtr not supported in Logical addressing")

// build-pass
// compile-flags: -C target-feature=+PhysicalStorageBufferAddresses,+ext:SPV_KHR_physical_storage_buffer

use spirv_std::*;

unsafe fn convert_u_to_ptr<T>(handle: u64) -> *mut T {
    let result: *mut T;
    asm!(
        "{result} = OpConvertUToPtr typeof{result} {handle}",
        handle = in(reg) handle,
        result = out(reg) result,
    );
    result
}

#[spirv(fragment)]
pub fn main(out: &mut u32) {
    unsafe {
        let x: *mut RuntimeArray<u32> = convert_u_to_ptr(100);
        *out = *(*x).index(0);
    }
}

@expenses
Copy link
Contributor Author

Utgh, I forgot I was on @msiglreith's branch when I tested that 😅

I've been making some progress here: main...expenses:convert-u-to-ptr

it now fails with alignment issues.

@msiglreith
Copy link
Contributor

One issue I had with physical storage addresses was the pointer storage class inference. Afaik the current inference algorithm assumes/requires every pointer being Generic and OpConvertUToPtr requires generating a OpTypePointer out of thin-air in the asm. The default type would be Function which conflicts with the requested pointer type.

@expenses
Copy link
Contributor Author

Utgh, I forgot I was on @msiglreith's branch when I tested that 😅

I've been making some progress here: main...expenses:convert-u-to-ptr

it now fails with alignment issues.

Okay, most issues have been resolved, but I'm still getting alignment errors with some struct fields:

#[repr(C)]
pub struct Struct {
    pub a: Mat4,
    pub b: Mat4,
    pub c: Vec3,
    pub d: f32,
    pub e: u32,
    pub f: u32,
    pub g: u32,
    pub h: bool,
}

#[spirv(fragment)]
pub fn main(out: &mut u32) {
    unsafe {
        let x: *mut RuntimeArray<u32> = arch::convert_u_to_ptr(100);
        let y: *mut Struct = arch::convert_u_to_ptr(200);

        let mat = (*y).a;

        *out = *(*x).index(0) + (*y).g;
    }
}

It's happy with (*y).g but not (*y).a:

error: error:0:0 - Memory accesses with PhysicalStorageBufferEXT must use Aligned.

@expenses
Copy link
Contributor Author

Solved that problem as well - just had to OpCopyMemory instructions. main...expenses:convert-u-to-ptr now works well for my purposes. It'd be good to see how we can get it to a mergeable state.

@msiglreith
Copy link
Contributor

Nice! Memory access instructions in asm blocks accessing physical storage buffers also need handling for the Aligned attribute. Unfortunately these are not covered by the load/memcpy interface.

@khyperia
Copy link
Contributor

It'd be good to see how we can get it to a mergeable state.

Unfortunately I think it's pretty far from being mergeable:

  1. We probably want to use an integrated-into-the-language version of casting (e.g. as) rather than a function intrinsic. For example, making this work. Still, a function intrinsic is useful for use in user libraries for now, until if/when we get the more integrated version implemented.
  2. We need a better way of specifying Aligned on loads than putting them on everything unconditionally.
  3. Quite a bit of investigation needs to go into figuring out what non-Logical addressing modes actually imply about what assumptions can be made in the compiler - I'm fairly sure that quite a few places we assume Logical to simplify things. Similarly, assumptions around 32 vs. 64 bit pointers are important - e.g. right now, the branch is using 64 bit pointers with PhysicalStorageBuffer64, except the rust compiler is using 32 bit pointers in the target config.
  4. There's a few questionable changes signaling significant underlying problems that need to be investigated and resolved - for example, changing the specializer to default to PhysicalStorageBuffer instead of Function, and changing integer/floats to default to 1-byte alignment.

Sadly, I don't think this is very high on Embark's priority list, so I'm not sure how much help on the above I'll be able to provide :(

@AidanConnelly
Copy link
Contributor

  1. Quite a bit of investigation needs to go into figuring out what non-Logical addressing modes actually imply about what assumptions can be made in the compiler - I'm fairly sure that quite a few places we assume Logical to simplify things

To clarify, the memory model is assumed to be logical, or it's assumed to possibly be logical?

@khyperia
Copy link
Contributor

Assumed to be logical - as in, "ah, there's a really complicated to deal with situation here that's difficult to implement, but, this situation can never happen with logical, only physical, so we're not going to deal with it right now"

@expenses
Copy link
Contributor Author

https://www.khronos.org/registry/SPIR-V/specs/unified1/SPIRV.html#Addressing_Model

PhysicalStorageBuffer64

Indicates that pointers with a storage class of PhysicalStorageBuffer are physical pointer types with an address width of 64 bits, while pointers to all other storage classes are logical.

This seems to imply that we only need to worry about non-logical pointers in the context of PhysicalStorageBuffers, which might make things easier (or not).

@expenses expenses changed the title OpConvertUToPtr support for RuntimeArrays OpConvertUToPtr support ~for RuntimeArrays~ Oct 14, 2021
@expenses expenses changed the title OpConvertUToPtr support ~for RuntimeArrays~ OpConvertUToPtr support ~~for RuntimeArrays~~ Oct 14, 2021
@expenses expenses changed the title OpConvertUToPtr support ~~for RuntimeArrays~~ OpConvertUToPtr support Oct 14, 2021
@eddyb
Copy link
Contributor

eddyb commented Oct 20, 2021

One issue I had with physical storage addresses was the pointer storage class inference. Afaik the current inference algorithm assumes/requires every pointer being Generic and OpConvertUToPtr requires generating a OpTypePointer out of thin-air in the asm. The default type would be Function which conflicts with the requested pointer type.

I think the fix for that would be to add an inference rule for OpConvertUToPtr that basically claims that the only storage class it can output is e.g. PhysicalStorageBuffer32 - seems like that would require:

  1. adding a new variant to StorageClassPat:
    /// Pattern for a SPIR-V storage class, dynamic representation (see module-level docs).
    #[derive(Debug, PartialEq, Eq)]
    pub enum StorageClassPat {
    /// Unconstrained storage class.
    Any,
  • and an import here:
    mod pat_ctors {
    pub const S: super::StorageClassPat = super::StorageClassPat::S;
    // NOTE(eddyb) it would be really nice if we could import `TyPat::{* - Any, Var}`,
    // i.e. all but those two variants.
    pub use super::TyPat::{
    Array, Either, Function, Image, IndexComposite, Matrix, Pipe, Pointer, SampledImage,
    Struct, Vector, Vector4, Void,
    };
  1. splitting Op::ConvertUToPtr out of this arm:
    Op::ConvertPtrToU | Op::SatConvertSToU | Op::SatConvertUToS | Op::ConvertUToPtr => {}
Op::ConvertUToPtr =>  sig! { (_) -> Pointer(PhysicalStorageBuffer32, _) }
  1. handle StorageClassPat::PhysicalStorageBuffer32 here:
    match pat {
    StorageClassPat::Any => Match::default(),
            StorageClassPat::PhysicalStorageBuffer32 => {
                let mut m = Match::default();
                // HACK(eddyb) choice of `0` makes this potentially overlap
                // with `Var(0)` and a different mechanism should be added.
                let found = m.storage_class_var_found
                    .get_mut_or_default(0);
                found.push(storage_class);
                found.push(InferOperand::Concrete(CopyOperand::StorageClass(StorageClass::PhysicalStorageBuffer32)));
                m
            }

(this effectively emulates something like {S} (_) -> Pointer(S, _), where the instruction has a storage class operand, that happens to contain PhysicalStorageBuffer32, and there's also a storage class inference variable in the resulting pointer type, and the use of S in both places links them during inference - but in this case, the instruction itself doesn't have a storage class operand, so we have to fake it)


Alternatively, we could hack around this with e.g. OpGenericCastToPtrExplicit to specify the storage class to infer, but then we'd have to remove that instruction since it's Kernel-only.

@eddyb
Copy link
Contributor

eddyb commented Mar 29, 2023

I've talked about this elsewhere, but my current approach to dealing with pointers is this:

(there's a lot there you could read, but the short version is that the "storage class inference" in Rust-GPU is a dead-end, and erasing pointer types can be far more effective and flexible)

With the qptr passes replacing the storage class inference, you could use OpTypePointer PhysicalStorageBuffer64 ... in asm! and it should "just" work (with no need for the types to exactly "match up" elsewhere).

@eddyb eddyb added the s: qptr may fix This might be fixed by the "qptr" experiment (https://github.com/EmbarkStudios/spirt/pull/24) label Mar 29, 2023
@eddyb eddyb self-assigned this Mar 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
s: qptr may fix This might be fixed by the "qptr" experiment (https://github.com/EmbarkStudios/spirt/pull/24) t: enhancement A new feature or improvement to an existing one.
Projects
None yet
Development

No branches or pull requests

5 participants