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
Improved public API soundess #27
Conversation
Refactored world and query APIs to require &mut World in safe APIs and added _unchecked alternatives.
It'd be nice to have per-component-type dynamic borrow checking eventually to make more patterns Just Work, but this is definitely a step in the right direction! |
The whole point of this change, however, is the fact that dynamic borrow checking is excessively bad for performance as we'd need to be performing checks in all the iterators and accessors, which would be prohibitively expensive. I still believe allowing the dispatcher to guarantee soundness via its preperations and dispatch is the ultimate solution here - placing these restrictions on the world proper for soundness is already, as seen here, limiting the API drastically. @TomGillen - Am I correct in my understandings below here:
I dont agree with drastically limiting all of legions use cases for allowing the public API more soundness for direct access to world. As an example, the dispatcher already provides the static garuntees for access which are needed to safely use the unchecked API's. I dont see why we need to make this API so unergonomic now for the safety of Correct me if I am off base or wrong about any of these assumptions so far. But this change basically:
I see this as drastically limiting to say, the transform project by @AThilenius, as due to the nature of transform hierarchies, it basically requires a large amount of random access for parent-child relationships. Now, those relationships must be iterated and checked on a component-by-component basis; which means we cannot randomly access say, Translation and Rotation, at the same time (without unchecked). From my perspective, this would now make the legion safe api a single-component access API, or a glorified for-loop iterator with queries, preventing any kind of multi-query execution is drastically limiting on standard use cases. I realize now after writing this all its a brainfart, but I believe the discussion of soundness/safety needs to occur more from a design perspective. I believe its safe to make checked API's in world, while the garuntees of the executor allow for the default API to not need the exclusivity garuntees. @Frizi for visibility, as this is a huge limiting change which may drastically affect the current amethyst RFC. |
The system scheduler doesn't provide sufficient safety, because a system can either run more than one query at a time, or perform random accesses while inside a query loop. Current (system) code: for (mut pos, vel) in query.iter() {
// this "safe" code could panic or perform undefined behaviour, depending on what `entity` refers to
*world.get_component_mut::<Position>(entity) = *pos;
} This PR: for (mut pos, vel) in query.iter(&mut *world) {
// this won't compile
*world.get_component_mut::<Position>(entity) = *pos;
} for (mut pos, vel) in unsafe { query.iter_unchecked(&*world) } {
// this compiles, but the user has to aknowledge the potential unsafety between
// the query and random access
unsafe { *world.get_component_mut_unchecked::<Position>(entity) = *pos };
} The issue is more pronounced if a system was doing something like nesting multiple query loops. That is only safe if the queries are designed such that they can not possibly both access the same chunks (i.e. one reads a component while the other requires the component does not exist). By default the compiler will not allow such nested loops, but it would if both loops used I don't think the |
In most cases, the most notable change to user code is that query code inside systems now looks the same as it does outside of systems. |
I don't believe that maintaining a single type -> borrow table in the
I don't see the conflict here; the dispatcher can build on top of the unchecked APIs, on the basis of the guarantees it enforces itself. |
It would be difficult to remove the runtime checking from the safe APIs and keep them in the unsafe APIs (the different code path would run very deep), which is why we instead perform runtime checks on both in debug builds and disable the checks in release. That is only safe to do with the changes in this PR. Per-component checks would certainly not be ideal for performance, but also not totally ruinous provided that there was a way of marking an entire slice as borrowed with a similar performance cost to how the borrow checking works currently. I suspect it may be possible to implement this at a similar cost to the current checks. Even so, I would still be much more comfortable if such checks could be disabled in release builds. There is one more side affect of this PR; we could now safely remove |
To be clear, I'm talking about per type checks, i.e. behavior approximately analogous to having a bunch of |
We already perform runtime borrow checks per-type, per-chunk. |
Oh, you're saying that it's unreasonably difficult to have safe and unsafe APIs that always and never hit those checks respectively? I'd be interested to see what the quantitative performance impact of just running them always would be; per-chunk strikes me as pretty coarse. Also, would lifting them out to the world scope, ignorant of individual chunks, reduce the difficulty? |
(but of course none of that needs to be fully explored right now; this PR addresses the immediate problem as-is) |
If the checks were global and not per-chunk, that would make it very difficult to run queries concurrently, even between systems. Two queries which both access the same component type can easily still be disjoint about which entities they return, and the scheduler is aware of this and takes advantage of it to allow more systems to run concurrently. @jaynus Currently in this PR, you can use the safe API even when two systems are going to be running concurrently. You only need to worry about safe vs unsafe code within a single system. If a system does not do nested queries or perform random accesses while simultaneously holding a mutable reference, then that system can be entirely written with the safe API. The sheduler ensures that you don't need to worry about what other systems are doing, but you still need to ensure that your own system is correct. I did at one point (about a year ago) manage to get queries to conditionally require |
Which is exactly the sort of case a distinct unsafe API is for, since the scheduler can enforce the necessary guarantees itself while presenting a safe API to the user. Hence, if global checks make a safe/unsafe API distinction feasible, the result might be a usability sweet spot. |
I've just ported The changes needed can be seen here: A few things become apparently that I missed previously:
This raises the question, that I don't really understand the reasoning: Queries require a mutable accessor because we cannot garuntee if it has Is there not a way we could still provide I think this would greatly help the ergonomics, and massively reduce the needed amount of |
We could do that, but it will add half as many again functions to I am 99% sure it is possible to have |
Alternatively, this is mainly for the case of read-only accessors which is the hot-path for legion, being rendering. Alternatively, how about we add the functions as explicit-immutable instead of mutable (reverse of the rusty-method, but more sensible in this case). This could either be as you say. I think as a simple stop-gap, would be to allow say This would be a fairly simple addition, and I think meet the needs of the renderer paths in amethyst, and providing some more flexability without needing the unsafe code. I feel this is a good compromise until specialization lands. It would also be nice to have a |
Well legion's "hot path" are query iterators as a whole, it should perform the same regardless of whether components within that are mutable or immutable.
We can do that statically with a |
I like it. Can we do that? |
Changed
World
andQuery
APIs to require&mut World
, such that it is statically impossible to write safe code with unsound borrowing. New unsafe_unchecked
alternatives have been added with the previous behaviour.This required changing the
PreparedQuery
APIs to also require they be given aPreparedWorld
reference, in order to control whether a system can execute two of its queries at the same time.PreparedWorld
now provides more of the API that is available onWorld
.PreparedWorld
allows random access to components and archetypes that a system's queries declare access to.SystemBuilder::[read/write]_component
can now be used to mark the system as accessing all archetypes.