-
Notifications
You must be signed in to change notification settings - Fork 67
Chore: Load api #1431
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
base: develop
Are you sure you want to change the base?
Chore: Load api #1431
Conversation
# Conflicts: # client/ayon_core/pipeline/load/plugins.py
|
One thing to consider regarding the containers: Some hosts like Unreal are facing situations where loaded container can be:
So you can load Artist must be able to handle cases where he needs to replace all loaded versions with another one, only versions used in certain levels, only selection. It gets even more complicated with layouts and versioning assets inside. When you load layout, you should be able to version its content independently - but when you do, I think the version of the loaded layout should be... well... invalidated - or in some state that would flag that the loaded version of the layout is not actually that version because it's content was modified. All of it is pointing maybe to some need of addressing hierachies of loaded containers within the API - so when loader Maybe worth of some brainstorming. |
|
One more point that is related to the open questions - wouldn't it be simpler to base the loading api around representation traits only and forget the legacy system? That means more context about representations for the loaders - how the loaders can decide if and what to do with specific representation? @philippe-ynput |
Isn't that what override of
I don't think we have to implement this now. But what we should be prepared for is how the hierarchy would look like -> and based on that maybe how it would be defined. Should it be like nested like Container B has a parent Container A? Would that be shown in a different view? Related to loaded assets vs instanced asset -> Would it make sense that current container would be 2 different items? I don't want to bend it to Unreal, but it might make sense to have an "asset" and then "instances" of the asset. |
|
Some quick notes from the call currently:
Sorry for the mess |
Related to this - to extend possibilities of future Scene Inventory tool, it would be cool to add some way to call some callbacks in loaders. Example: You are listing all loaded containers in Unreal. You would like to see, what containers are "just" loaded and what are actually instantiated in levels. That is something you cannot store on the container, it is runtime information that needs to be collected at that moment. Depends on design pattern - could be for example Observer/Listener/... (but that would mean adding some logic to loader creates container = ContainerItem(...)
container.register_update_callback(self.update_container_status) Now whenever container is accessed, it will execute registered callback. That callback then check current state and update some information on it, emit event, do something. This would at the end enable Scene Inventory to show if loaded assets are instantiated (and where) in Unreal. I suppose any DCC can come with similar examples. This would also allow automations around loaded stuff. |
|
@antirotor that example you provide sounds more like something that the LoadPlugin could just store as |
Not this one - that value must be computed at the time you show it (or you want to use somehow). When you load something, loader will create container and imprint. But to show if the loaded asset was instatiated (used somewhere) is something that must be determined at exact time. The loader knows what it is loading so those callbacks would be mostly defined by it. Imagine the similar thing in Houdini, where loader creates some nodes, but until the nodes are actually connected in some way to the rest of the scene, you don't know if it is used. And because the node can have different outputs or it can be linked be expressions or any other logic, it is up to the loader logic to find out. The usage status is just an example but having this callback/observer/listener logic can be useful for other things - like automating logic on changes of the loaded data. It could be even solution for invalidating layout instance if it's content has changed. That logic can be implement directly by the layout loader that will be observing it's content and will be notified by the change. The callback would then either calculate and compare checksums or just directly flag itself as "invalid". |
Sure - but isn't that exactly what
Convince me - if someone needed particular data about a container, why couldn't it just collect it itself? In what cases are we providing additional data out of the box that will be useful in the majority of cases or useful to show in the UI. And if that's the case - couldn't that just be added in the returned data on Or are you just saying you want to add a layer so that another plug-in can extend that data for any LoadPlugin. So that a studio plug-in can e.g. register their own additional custom data to be shown int he Scene Inventory. I do like that. But I feel Observer/Listener is overkill there? We'd essentially just allow I feel like I'm stupid again. Can you provide some pseudocode of what you're envisioning? |
|
You are right about the So lets investigate first the way using """Loader plugin."""
class CoolLoader(LoadPlugin):
# implementing all necessary abstract methods like `is_representation_compatible()`, etc.
...
def collect_containers(self) -> list[ContainerItem]: # btw this is currenly defined to return None
_containers = json.loads(my_app_api.get("ayon_containers"))
collected: list[ContainerItem] = []
for container in _containers:
cnt = ContainerItem.from_dict(container)
cnt.transient_data["active"] =my_app_api.is_active(container["id"])
collected.append(cnt)
return collectedand then in consuming tools: """Tool to show loaded containers and their "status"."""
# this will execute `collect_containers()` on Loader plugin
load_context = LoadContext()
for container in load_context._containers: # there is not public access for now?
print(f'container {container["container_id"]} is {container["transient_data"]["active"]}'"That's all fine but - this use of But that proposed observer/listener/... logic would make this much more flexible - first example doesn't differ that much because all the logic is related to the plugin anyway: """Loader plugin."""
class CoolLoader(LoadPlugin):
# implementing all necessary abstract methods like `is_representation_compatible()`, etc.
...
def collect_containers(self) -> list[ContainerItem]: # btw this is currenly defined to return None
_containers = json.loads(my_app_api.get("ayon_containers"))
collected: list[ContainerItem] = []
for container in _containers:
cnt = ContainerItem.from_dict(container)
cnt.register_observer(self.get_active)
collected.append(cnt)
return collected
def get_active(container: ContainerItem) -> None:
container.transient_data["active"] =my_app_api.is_active(container["id"])and then in consuming tool: """Tool to show loaded containers and their "status"."""
# this will execute `collect_containers()` on Loader plugin
load_context = LoadContext()
for container in load_context.get_containers(): # this would execute the observers
print(f'container {container["container_id"]} is {container["transient_data"]["active"]}'"so in there not much difference. But you could run class LayoutLoader(LoadPlugin):
def load_representations(self, representation_context) -> list[ContainerItem]:
layout_data = json.load(representation_context.get_representations()[0].get_trait(FileLocation).path)
# create base container
layout_container = ContainerItem(...)
for asset in layout_data:
loader_id = self.get_loader_id_for_representation(Representation(asset["representation"]))
loader = self.load_context.get_plugin(loader_id)
loaded_container = loader.load_representations(...) # now I am not sure where to get RepresentationContext here
loaded_container.register_observer(ContainerItem.Lifecycle.CHANGED, self.invalidate)
...Now I am skipping through it but you hopefully get the point. BTW Main advantage is IMO more obvious definition of data passed between and flexibility in adding/modifying. As a side note: When writing this I've noticed:
|
|
And regarding the question opened in the description: I don't think we need to handle container id and those things. Every loader is responsible for imprinting/collecting containers and all it needs is to return With the additional arguments to Name and labels - that is important thing. There are two ways , both with some merit. With representation traits, you can define name/label during the publishing. But that is describing the data, not how the loader interprets them. So that should be up to the loader (and loader can decide based on representation itself). |
|
NOTE Transient data are not and won't be tracked at all, transient data are meant for internal load plugin data. For example to store node object from a scene. Not something that a generic logic can handle in terms of capturing changes. (That's why they's called transient and are not typed.) |
To support what current
If you're talking about |
I'd say we shouldn't use direct dict access - we should use public method to get the data - because then, if we want to change where and what is saved, we just change the logic in the method. |
Please be more specific, it is meant for load plugin internal data, how could we know what the load plugin wants to store there? And we should not care... The load plugin is only one who should access the transient data. |
I think this shouldn't be handled by method argument. Loader should expose what options he can work with and get the data through that. Something like: class MyLoaderOptions(LoaderOptions):
instance_number: int
color: str
class MyLoader(LoadPlugin):
...
@staticmethod
def get_options_class() -> Type[LoaderOptions]:
return type(MyLoaderOptions)
def set_options(options: MyLoaderOptions) -> None:
self._options = options
And the code responsible for showing UI of options = loader,get_options_class()
options(**form_data)
loader.set_options(options)Advantages are that you'll have models for dialogs, you'll get type annotations, etc. Those examples could be written differently, also the options could be optional argument in loader constructor for sure. Just that they should be clearly defined.
yes, I agree |
my point is that it should be exposed only by getter method and not as public dict property on I have bigger issue with data.update({
"container_id": container_id,
"project_name": project_name,
"representation_id": representation_id,
"load_plugin_identifier": load_plugin.identifier,
"version_locked": version_locked,
})Meaning that data will have always this. There must be better way? |
That's
I would like to avoid storing data related to single load to the plugin object itself -> can lead to race condition issues, or changing the options to different values from "defaults" and then calling it again expecting it does not behave as before. # This effectivelly change how any future load works
load_plugin.set_options(...)
load_plugin.load_representation(...)
...somewhere in future e.g. in different function (considering template builder for example)...
# This will still use the options from above, even though it should use defaults
load_plugin.load_representation(...) |
Should new instance of the loader be created for every loading "session"? Btw that is a good topic to discuss. I can imagine one session, where you execute a bunch of loading actions (as items of work, not as Related - loader options presets, etc. This gets complicated - again for example in unreal - where loading something involves unreal framework that itself consumes some options. We are already juggling with options predetermined using Settings, options that could/should be changed by artist just before load and some presets (basically the latter but grouped together in enum in UI sense). |
No. In case you want to load 2 different models in one session you should not need to re-load and re-initialize plugins and re-collect containers again and again, unless you explicitly request it using
It should, and that's why it should be an argument and not state of the load plugin.
You mean allow user to "override" settings defaults to his default? Possible, with the actions, another reason why they might be useful? But in theory the local override action (to set it up) can be just different action where user would set that up, and the load plugin could use it. |
| def can_switch_container(self, container: ContainerItem) -> bool: | ||
| """Check if container can be switched. | ||
| Args: | ||
| container (ContainerItem): Container to check. | ||
| Returns: | ||
| bool: True if container can be switched, False otherwise. | ||
| """ | ||
| return False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this include checking whether it can switch to a specific representation? Or is that supposed to be checked prior to this call?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was meant if it can switch a container, before you choose representation. Representation is then validated with is_representation_compatible.
| @abstractmethod | ||
| def change_representations( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'change' terminology sounds very new to the load API that existed. I wonder if this should be update_representation instead? :/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can change to different (lower) version or switch container of other loader.
EDITED:
I see that I did add switch_containers but it would make more sense to actually use this method instead as you might also want to change the representation at the same time.
| Raises: | ||
| UnsupportedSwitchError: If switching is not supported. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if only one of the containers can't switch? Should it stop immediately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question, and I have no idea. Maybe the conversation is not relevant as I remember I wanted to use change_representation instead for this.
| scene_data (Optional[dict[str, Any]]): Additional data stored to the | ||
| scene. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any examples?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example something like layer_name. The idea is/was that scene_data are tracked for changes. Maybe not relevant?
Changelog Description
Base of new load api definition.
Additional info
I got stuck, that's why we're here.
This is a draft that is meant to trigger discussion related to load api enhancements. It is NOT in final state, there are missing pieces. The draft is meant to discuss general concept, not to fine-tune code details. Suggest better approach instead of asking why it's so bad.
Introduction
Our current load api is still unchanged since avalon. It is simple, effective but sometimes we do suffer from that simplicity. Loader plugins are class objects, usually hard to be re-used, they don't have context they lack control over how the stuff they produce, work with is handled afterwards and more other small issues connected to it. Loader plugins are also used for UI actions (no difference in load plugin and action).
NOTE
With loader actions #1421 we already started to separate "actions" and "loaders".
Goal
Define new api for load related logic.
Sum of expected features
I need confirmation for few of them. Checked ones are confirmed. Not checked need validation. Striked won't be added.
Ask for more if you have more.
Possible requirements
Structures
LoadContext
Wrapper for load api. Takes care about discovery and access to load plugins, supports api methods to collect/change/remove containers and allows to register callbacks to a specific events.
During collection phase allows to store "shared data" so load plugins can share data from scene. It is common that the load metadata are stored the same way in 99% cases of DCC integration.
ContainerItem
Holds metadata about container for load api and for UI purposes. There are expected data we want to keep stored with container that are "generic" as they are needed for core pipeline purposes, but there has to be an option to store data that are related to the DCC that should be stored with the scene metadata, and there also might be data that are important for working with the container, but are not stored to the scene metadata.
Required information
Immutable - Can't be changed after container creation.
Mutable - Can be changed during container lifetime.
Change of any of the values will trigger an event that value changed. Some of them don't have to be stored to the scene.
Transient data
Load plugin can store any objects here, that should be maintained only by the plugin itself. For example node object from the scene. Undefined structure and won't trigger any event.
Scene data
Additional data that are stored to the scene. Could be layer/node id/name for DCCs where container metadata are not stored on the "item" that represents loaded representation but to generic scene metadata. Expected to be json serializable (type-wise).
Additional scene data are a little bit problematic. The main question is if we should handle changes of values and trigger an event when it happens. If we should track changes then it must be custom dictionary object without type hints, but we'd be able to handle value changes. If we won't track the changes then we can let host inherit from
ContainerItemand let it add typed object (or own dictionary), in that case transient and scene data can be ditched out completelly.My guts say that we don't have to capture the changes, but we should provide default option for additional scene data and we should also provide transient data to give opinion how we think it should work. But I might be wrong.
Few thoughs
The issue with making loading more advanced is that some of the usages would suffer. For example current workfile template builder can load representations easily, but if a load plugin requires loader action because it is too advanced it might not be able to use the load plugin in template builder -> we might need to look into template builder implementation and add more features there to support feature parity with old load mechanism.
I was thinking that the load plugins won't be automatically shown in loader UI as they did before. Instead they would require to have a loader action (there can be one pre-implemented action in core that would make them work as they did before). That would allow to easily accomplish simple load scenarions but also support advanced load mechanism, which might require to call multiple load plugins at once (e.g. load of model, texture and animation in one step etc.). Also related to attribute definitions.
Current questions
Load methods and arguments
In current state of the draft load plugins have
load_representations,change_representationsandremove_containers(load,updateandremove). Load and change are representation related, for representation compatibility check isis_representation_compatible-> singular so it can returnbool.All of them don't have additional arguments. But they should have, so if they're e.g. called from an action, they might require more "additional" context. But how to define the arguments to be able to call just "load representation" in automation without any additional knowledge about the load plugin. Right now the idea is that there would be optional key word argument
additional_data.Another question is: Do loaders need to define attribute definitions like they did before? E.g. load the tree 10 000 times. From my point of view this is based on the fact if we'll use loader actions to show the load plugins in UI, because in that case the loader action could ask that and would pass the value to the load using 'additional_data`. That would also partially solve possible issues with template builder.