In general I think GP is correct. There is some subset of problems that absolutely requires indexing to express efficiently.
This is something you can't do on a compute shader, given you don't have access to the built-in derivative methods (building your own won't be cheaper either).
Still, if you want those changes to persist, a compute shader would be the way to go. You _can_ do it using a pixel shader but it really is less clean and more hacky.
Interestingly enough the derivative functions are available to compute shaders as of SM 6.6. [0] Oddly SPIR-V only makes the associated opcodes [1] available to the fragment execution model for some reason. I'm not sure how something like DXVK handles that.
I'm not clear if the associated DXIL or SPIR-V opcodes are actually implemented in hardware. I couldn't immediately find anything relevant in the particular ISA I checked and I'm nowhere near motivated enough to go digging through the Mesa source code to see how the magic happens. Relevant because since you mentioned it I'm curious how much of a perf hit rolling your own is.
[0] https://microsoft.github.io/DirectX-Specs/d3d/HLSL_SM_6_6_De...
[1] https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.htm...
> There is some subset of problems that absolutely requires indexing to express efficiently.
Sure. But it's almost certainly quicker to run a shader over them, and ignore the values you don't want to operate on than it is to copy the data back, modify it in a safe bounds checked array in rust, and then copy it again.
Use a compute shader. Run only as many invocations as you care about. Use explicit indexing in the shader to fetch and store.
Obviously that doesn't make sense if you're targeting 90% of the slots in the array. But if you're only targeting 10% or if the offsets aren't a monotonic sequence it will probably be more efficient - and it involves explicit indexing.