From 41204851649c8590eb68021a4943aa6dcb9580bb Mon Sep 17 00:00:00 2001 From: JornosDesktop Date: Mon, 13 Nov 2023 20:22:52 +0100 Subject: [PATCH 001/247] Remove docs, has been moved to Stride docs repo --- docs/BuildDetails.md | 78 --- docs/CONTRIBUTING.md | 23 - docs/ContributorLicenseAgreement.md | 81 --- docs/localization.md | 13 - docs/technical/asset-introspection.md | 249 ------- docs/technical/build-pipeline.md | 153 ---- docs/technical/copy-paste.md | 218 ------ docs/technical/editor-localization.md | 657 ------------------ docs/technical/media/poedit-edit-po-file.png | 3 - docs/technical/media/poedit-open-pot-file.png | 3 - 10 files changed, 1478 deletions(-) delete mode 100644 docs/BuildDetails.md delete mode 100644 docs/CONTRIBUTING.md delete mode 100644 docs/ContributorLicenseAgreement.md delete mode 100644 docs/localization.md delete mode 100644 docs/technical/asset-introspection.md delete mode 100644 docs/technical/build-pipeline.md delete mode 100644 docs/technical/copy-paste.md delete mode 100644 docs/technical/editor-localization.md delete mode 100644 docs/technical/media/poedit-edit-po-file.png delete mode 100644 docs/technical/media/poedit-open-pot-file.png diff --git a/docs/BuildDetails.md b/docs/BuildDetails.md deleted file mode 100644 index a84170cf8f..0000000000 --- a/docs/BuildDetails.md +++ /dev/null @@ -1,78 +0,0 @@ -# Build - -## Overview - -This is a technical description what happens in our build and how it is organized. This covers mostly the build architecture of Stride itself. - -* [Targets](../Targets) contains the MSBuild target files used by Games -* [sources/common/targets](../sources/common/targets) (generic) and [sources/targets](../sources/targets) (Stride-specific) contains the MSBuild target files used to build Stride itself. - -Since 3.1, we switched from our custom build system to the new csproj system with one nuget package per assembly. - -We use `TargetFrameworks` to properly compile the different platforms using a single project (Android, iOS, etc...). - -Also, we use `RuntimeIdentifiers` to select graphics platform. [MSBuild.Sdk.Extras](https://github.com/onovotny/MSBuildSdkExtras) is used to properly build NuGet packages with multiple `RuntimeIdentifiers` (not supported out of the box). - -### Limitations - -* Dependencies are per `TargetFramework` and can't be done per `RuntimeIdentifier` (tracked in [NuGet#1660](https://github.com/NuGet/Home/issues/1660)). -* FastUpToDate check doesn't work with multiple `TargetFrameworks` (tracked in [project-system#2487](https://github.com/dotnet/project-system/issues/2487)). - -## NuGet resolver - -Since we want to package tools (i.e. GameStudio, ConnectionRouter, CompilerApp) with a package that contains only the executable with proper dependencies to other NuGet runtime packages, we use NuGet API to resolve assemblies at runtime. - -The code responsible for this is located in [Stride.NuGetResolver](../sources/shared/Stride.NuGetResolver). - -Later, we might want to take advantage of .NET Core dependency resolving to do that natively. Also, we might want to use actual project information/dependencies to resolve to different runtime assemblies and better support plugins. - -## Versioning - -Stride is versioned using `SharedAssemblyInfo.cs`. -For example, assuming version `4.1.3.135+gfa0f5cc4`: -- `4.1` is the Stride major and minor version, as they are grouped in the launcher. Versions inside this group shouldn't have breaking changes -- `3` is the asset version. This can be bumped if asset files require some upgrade. -- `135` is the git height (number of commits since `4.1.3` is set), computed automatically when building packages. - Note: when building packages locally, this will typically be 1. This is the reason why the asset version needs to be bumped when asset changes to keep things ordered (otherwise the git height version `1` will always be lower than official version). -- `+gfa0f5cc4` means git commit `fa0f5cc4` - -## Assembly processor - -Assembly processor is run by both Game and Stride targets. - -It performs various transforms to the compiled assemblies: -* Generate [DataSerializer](../sources/common/core/Stride.Core/Serialization/DataSerializer.cs) serialization code (and merge it back in assembly using IL-Repack) -* Generate [UpdateEngine](../sources/engine/Stride.Engine/Updater/UpdateEngine.cs) code -* Scan for types or attributes with `[ScanAssembly]` to quickly enumerate them without needing `Assembly.GetTypes()` -* Optimize calls to [Stride.Core.Utilities](../sources/common/core/Stride.Core/Utilities.cs) -* Automatically call methods tagged with [ModuleInitializer](../sources/common/core/Stride.Core/ModuleInitializerAttribute.cs) -* Cache lambdas and various other code generation related to [Dispatcher](../sources/common/core/Stride.Core/Threading/Dispatcher.cs) -* A few other internal tasks - -For performance reasons, it is run as a MSBuild Task (avoid reload/JIT-ing). If you wish to make it run the executable directly, set `StrideAssemblyProcessorDev` to `true`. - -## Dependencies - -We want an easy mechanism to attach some files to copy alongside a referenced .dll or .exe, including content and native libraries. - -As a result, `` and `` item types were added. - -When a project declare them, they will be saved alongside the assembly with extension `.ssdeps`, to instruct referencing projects what needs to be copied. - -Also, for the specific case of ``, we automatically copy them in appropriate folders and link them if necessary. - -Note: we don't apply them transitively yet (project output won't contains the `.ssdeps` file anymore so it is mostly useful to reference from executables/apps directly) - -## Native - -By adding a reference to `Stride.Native.targets`, it is easy to build some C/C++ files that will be compiled on all platforms and automatically added to the `.ssdeps` file. - -### Limitations - -It seems that using those optimization don't work well with shadow copying and [probing privatePath](https://msdn.microsoft.com/en-us/library/823z9h8w(v=vs.110).aspx). This forces us to copy the `Direct3D11` specific assemblies to the top level `Windows` folder at startup of some tools. This is little bit unfortunate as it seems to disturb the MSBuild assembly searching (happens before `$(AssemblySearchPaths)`). As a result, inside Stride solution it is necessary to explicitely add `` to the graphics specific assemblies otherwise wrong ones might be picked up. - -This will require further investigation to avoid this copying at all. - -## Asset Compiler - -Both Games and Stride unit tests are running the asset compiler as part of the build process to create assets. \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md deleted file mode 100644 index 40bd212b7c..0000000000 --- a/docs/CONTRIBUTING.md +++ /dev/null @@ -1,23 +0,0 @@ -# Contributing to Stride - -## Check our issue tracker - -Please take a look at our [issue tracker](https://github.com/stride3d/stride/issues). - -If you are just getting started with Stride, issues marked with ['good first issue'](https://github.com/stride3d/stride/labels/good%20first%20issue) can be a good entry point. - -## Notify users - -Once you start working leave a message on the appropriate issue or create one if none exists to: -* make sure that no one else is working on that same issue -* lay out your plans and discuss it with collaborators and users to make sure it is properly architectured and would fit well in the project - -## Coding style - -Please use and follow Stride's `.editorconfig` when making changes to files. - -## Submitting Changes - -* Push your changes to a specific branch in your fork. -* Use that branch to create and fill out a pull request to the official repository. -* After creating that pull request and if it's your first time contributing a [CLA assistant](https://cla-assistant.io/) will ask you to sign the [Contributor License Agreement](https://github.com/stride3d/stride/blob/master/docs/ContributorLicenseAgreement.md). diff --git a/docs/ContributorLicenseAgreement.md b/docs/ContributorLicenseAgreement.md deleted file mode 100644 index 7ef6a76f75..0000000000 --- a/docs/ContributorLicenseAgreement.md +++ /dev/null @@ -1,81 +0,0 @@ -# Stride Individual Contributor License Agreement - -Thank you for your interest in contributing to Stride ("We" or "Us"). - -This contributor agreement ("Agreement") documents the rights granted by contributors to Us. To make this document effective, please sign it and send it to Us by electronic submission, following the instructions given by [CLA assistant](https://github.com/CLAassistant) who will send a comment in your first pull request. This is a legally binding document, so please read it carefully before agreeing to it. The Agreement may cover more than one software project managed by Us. - -## 1. Definitions - -"You" means the individual who Submits a Contribution to Us. - -"Contribution" means any work of authorship that is Submitted by You to Us in which You own or assert ownership of the Copyright. If You do not own the Copyright in the entire work of authorship, please follow the instructions in https://github.com/stride3d/stride. - -"Copyright" means all rights protecting works of authorship owned or controlled by You, including copyright, moral and neighboring rights, as appropriate, for the full term of their existence including any extensions by You. - -"Material" means the work of authorship which is made available by Us to third parties. When this Agreement covers more than one software project, the Material means the work of authorship to which the Contribution was Submitted. After You Submit the Contribution, it may be included in the Material. - -"Submit" means any form of electronic, verbal, or written communication sent to Us or our representatives, including but not limited to electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Us for the purpose of discussing and improving the Material, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." - -"Submission Date" means the date on which You Submit a Contribution to Us. - -"Effective Date" means the date You execute this Agreement or the date You first Submit a Contribution to Us, whichever is earlier. - -## 2. Grant of Rights - -### 2.1 Copyright License - -(a) You retain ownership of the Copyright in Your Contribution and have the same rights to use or license the Contribution which You would have had without entering into the Agreement. - -(b) To the maximum extent permitted by the relevant law, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable license under the Copyright covering the Contribution, with the right to sublicense such rights through multiple tiers of sublicensees, to reproduce, modify, display, perform and distribute the Contribution as part of the Material; provided that this license is conditioned upon compliance with Section 2.3. - -### 2.2 Patent License - -For patent claims including, without limitation, method, process, and apparatus claims which You own, control or have the right to grant, now or in the future, You grant to Us a perpetual, worldwide, non-exclusive, transferable, royalty-free, irrevocable patent license, with the right to sublicense these rights to multiple tiers of sublicensees, to make, have made, use, sell, offer for sale, import and otherwise transfer the Contribution and the Contribution in combination with the Material (and portions of such combination). This license is granted only to the extent that the exercise of the licensed rights infringes such patent claims; and provided that this license is conditioned upon compliance with Section 2.3. - -### 2.3 Outbound License - -Based on the grant of rights in Sections 2.1 and 2.2, if We include Your Contribution in a Material, We may license the Contribution under any license, including copyleft, permissive, commercial, or proprietary licenses. - -### 2.4 Moral Rights - -If moral rights apply to the Contribution, to the maximum extent permitted by law, You waive and agree not to assert such moral rights against Us or our successors in interest, or any of our licensees, either direct or indirect. - -### 2.5 Our Rights - -You acknowledge that We are not obligated to use Your Contribution as part of the Material and may decide to include any Contribution We consider appropriate. - -### 2.6 Reservation of Rights - -Any rights not expressly licensed under this section are expressly reserved by You. - -## 3. Agreement - -You confirm that: - -(a) You have the legal authority to enter into this Agreement. - -(b) You own the Copyright and patent claims covering the Contribution which are required to grant the rights under Section 2. - -(c) The grant of rights under Section 2 does not violate any grant of rights which You have made to third parties, including Your employer. If You are an employee, You have had Your employer approve this Agreement or sign the Entity version of this document. If You are less than eighteen years old, please have Your parents or guardian sign the Agreement. - -(d) You have followed the instructions in https://github.com/stride3d/stride, if You do not own the Copyright in the entire work of authorship Submitted. - -## 4. Disclaimer - -EXCEPT FOR THE EXPRESS WARRANTIES IN SECTION 3, THE CONTRIBUTION IS PROVIDED "AS IS". MORE PARTICULARLY, ALL EXPRESS OR IMPLIED WARRANTIES INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT ARE EXPRESSLY DISCLAIMED BY YOU TO US. TO THE EXTENT THAT ANY SUCH WARRANTIES CANNOT BE DISCLAIMED, SUCH WARRANTY IS LIMITED IN DURATION TO THE MINIMUM PERIOD PERMITTED BY LAW. - -## 5. Consequential Damage Waiver - -TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL YOU BE LIABLE FOR ANY LOSS OF PROFITS, LOSS OF ANTICIPATED SAVINGS, LOSS OF DATA, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL AND EXEMPLARY DAMAGES ARISING OUT OF THIS AGREEMENT REGARDLESS OF THE LEGAL OR EQUITABLE THEORY (CONTRACT, TORT OR OTHERWISE) UPON WHICH THE CLAIM IS BASED. - -## 6. Miscellaneous - -6.1 This Agreement will be governed by and construed in accordance with the laws of excluding its conflicts of law provisions. Under certain circumstances, the governing law in this section might be superseded by the United Nations Convention on Contracts for the International Sale of Goods ("UN Convention") and the parties intend to avoid the application of the UN Convention to this Agreement and, thus, exclude the application of the UN Convention in its entirety to this Agreement. - -6.2 This Agreement sets out the entire agreement between You and Us for Your Contributions to Us and overrides all other agreements or understandings. - -6.3 If You or We assign the rights or obligations received through this Agreement to a third party, as a condition of the assignment, that third party must agree in writing to abide by all the rights and obligations in the Agreement. - -6.4 The failure of either party to require performance by the other party of any provision of this Agreement in one situation shall not affect the right of a party to require such performance at any time in the future. A waiver of performance under a provision in one situation shall not be considered a waiver of the performance of the provision in the future or a waiver of the provision in its entirety. - -6.5 If any provision of this Agreement is found void and unenforceable, such provision will be replaced to the extent possible with a provision that comes closest to the meaning of the original provision and which is enforceable. The terms and conditions set forth in this Agreement shall apply notwithstanding any failure of essential purpose of this Agreement or any limited remedy to the maximum extent possible under law. diff --git a/docs/localization.md b/docs/localization.md deleted file mode 100644 index e010058342..0000000000 --- a/docs/localization.md +++ /dev/null @@ -1,13 +0,0 @@ -# Localization - -## Translation - -Please help us translate by updating existing translations and/or adding new language at https://hosted.weblate.org/projects/stride/ - -Translation are manually merged back from `weblate` branch to `master` branch. - -## Activate new language in Game Studio - -Once a new language has been added on weblate, it needs to be activated in the Game Studio during build & startup. - -Please check commit https://github.com/stride3d/stride/commit/c70f07f449 for an example on how to add a new language in Game Studio. diff --git a/docs/technical/asset-introspection.md b/docs/technical/asset-introspection.md deleted file mode 100644 index 0a2b27cd5c..0000000000 --- a/docs/technical/asset-introspection.md +++ /dev/null @@ -1,249 +0,0 @@ -# Asset, introspection and prefab - -## Assets - -*NOTE: Please read the Terminology section of the [Build Pipeline](build-pipeline.md) documentation first* - -### Design notes - -Assets contains various properties describing how a given **Content** should be generated. Some constraints are defined by design: - -* All types that can be referenced directly or indirectly by an asset must be serializable. This means that it should have the `[DataContract]` attribute, and the type of all its members must have it too. -* Members that cannot or should not be serialized can have the `[DataMemberIgnore]` attributes -* Other members can have additional metadata regarding serialization by using the `[DataMember]` attributes. There is also a large list of other attributes that can be used to customize serialization and presentation of those members. -* Arrays are not properly supported -* Any type of ordered collection is supported, but unordered collection (sets, bags) are not. -* Dictionaries are supported as long as the type of the key is a primitive type (see below for the definition of primitive type) -* When an asset references another asset, the member or item shouldn't use the type of the target asset, but the corresponding **Content**. For example, the ``MaterialAsset`` needs to reference a texture, it will have a ``Texture`` member and not a `TextureAsset`. -* It is possible to use the `AssetReference` type to represent a reference to any type of asset. -* Nullable value types are not properly supported -* An asset can reference multiple times the same objects through various members/items, but one of the member/item must be the "real instance", and the others must be defined as "object references", see below for more details. - -### Yaml metadata - -When assets are serialized to/deserialized from Yaml files, dictionaries of metadata is created or consumed in the process. There is one dictionary per type of metadata. The dictionary maps a property path (using `YamlAssetPath`) to a value, and is stored in a instance of `YamlAssetMetadata`. These dictionary are exchanged between the low-level Yaml serialization layer and the asset-aware layer via the `AssetItem.Metadata` property. This property is not synchronized all the time, it is just consumed after deserialization, to apply metadata to the asset, and generated just before serialization, to allow the metadata to be consumed during serialization. - -### Overrides - -The prefab and archetype system introduces the possibility to override properties of an asset. Some nodes of the property tree of an asset might have a *base*. (usually all of them in case of archetype, and some specific entities that are prefab instances in case of scene). How nodes are connected together is explained later on this documentation, but from a serialization point of view, any property that is overridden will have associated yaml metadata. Then we usa a custom serializer backend, `AssetObjectSerializerBackend`, that will append a star symbol `*` at the end of the property name in Yaml. - -### Collections - -Collections need special handling to properly support override. An item of a collection that is inherited from a base can be either modified (have another value) or deleted. Also, new items that are not present in the base can have been added. This is problematic in the case of ordered collection such as `List` because adding/deleting items changes the indices of item. - -To solve all these issues, we introduce an object called `CollectionItemIdentifiers`. There is one instance of this object per collection that supports override. This instance is created or retrieved using the `CollectionItemIdHelper`. They are stored using `ShadowObject`, which maintain weak references from the collection to the `CollectionItemIdentifiers`. This means that it is currently not possible to have overridable items in collection that are `struct`. - -A collection that can't or shouldn't have overridable items should have the `NonIdentifiableCollectionItemsAttribute`. - -The `CollectionItemIdentifiers` associates an item of the collection to a unique id. It also keep track of deleted items, to be able to tell, when an item in an instance collection is missing comparing to the base collection, if it's because it has been removed purposely from the instance collection, or if it's because it has been added after the instance collection creation to the base collection. - -Items, in the `CollectionItemIdentifiers`, are represented by their key (for dictionaries) or index (list). This means that any collection operation (add, remove...) must call the proper method of this class to properly update this collection. This is automatically done as long as the collection is updated through Quantum (see below). - -In term of inheritance and override, the item id is what connect a given item of the base to a given item of the instance. This means that items can be re-ordered, and other items can be inserted, without loosing or messing the connection between base and instances. Also, for dictionary, keys can be renamed in the instance. - -At serialization, the item id is written in front of each item (so collections are transformed to dictionaries of [`ItemId`, `TValue`] and dictionary are transformed to dictionaries of [`KeyWithId`,` TValue`], with `KeyWithId` being equivalent to a Tuple). -Here is an example of Yaml for a base collection and an instance collection: - -Base collection, with one id per item: -``` -Strings: - 309e0b5643c5a94caa799a5ea1480617: Hello - e09ec493d05e0446b75358f0e1c0fbdd: World - 9550f04dcee1d24fa8a30e41eea71a94: Example - 1da8adce3f0ce9449a9ed0e48cd32f20: BaseClass -``` -Derived collection. The first item is overridden, the 4th is a new item (added), and the last one express that the `BaseClass` entry has been deleted in the derived instance. -``` -Strings: - 309e0b5643c5a94caa799a5ea1480617*: Hi - e09ec493d05e0446b75358f0e1c0fbdd: World - 9550f04dcee1d24fa8a30e41eea71a94: Example - cfce75d38d66e24fae426d1f40aa4f8a*: Override - 1da8adce3f0ce9449a9ed0e48cd32f20: ~(Deleted) -``` - -When two assets that are connected with a base relationship are loaded, it is then possible to reconcile them: -* any item missing in the derived collection is re-added (so the `~(Deleted)` is need to purposely delete items) -* any item existing in the derived collection that doesn't exist in the base collection and doesn't have the star `*` is removed -* any item that exists in both collection but have a different value is overwritten with the value of the base collection -* overridden items (with the star `*`) are untouched - -## Quantum - -In Stride, we use an introspection framework called *Quantum*. - -### Type descriptors - -The first layer used to introspect object is in `Stride.Core.Reflection`. This assembly contains type descriptors, which are basically objects abstracting the reflection infrastructure. It is currently using .NET reflection (`System.Reflection`) but could later be implemented in a more efficient way (using `Expression`, or IL code). - -The `TypeDescriptorFactory` allows to retrieve introspection information on any type. `ObjectDescriptor`s contains descriptor for members which allow to access them. Collections, dictionaries and arrays are also handled (NOTE: arrays are not fully supported in Quantum itself). - -This assembly also provides an `AttributeRegistry` which allows to attach `Attribute`s to any class or member externally. - -> **TODO:** make sure all locations where we read `Attribute`s are using the `AttributeRegistry` and not the default .NET methods, so we properly support externally attached attributes. - -### Node graphs - -In order to introspect object, we build graphs on top of each object, representing their members, and referencing the graphs of other objects they reference through members or collection. -The classes handling theses graphs are in the `Stride.Core.Quantum` assembly. - -#### Node containers - -Nodes of the graphs are created into an instance of `NodeContainer`. Usually a single instance of `NodeContainer` is enough, but we have some scenarios where we use multiple ones: for example each instance of scene editor contains its own `NodeContainer` instance to build graphs of game-side objects, which are different from asset-side (ie. UI-side) objects, have a different lifespan, and require different metadata. - -In the GameStudio, the `NodeContainer` class has two derivations: the `AssetNodeContainer` class, which expands the primitive types to add Stride-specific types (such as `Vector3`, `Matrix`, `Guid`...). This class is inherited to a `SessionNodeContainer`, which additionally allows plugin to register their own primitive types and metadata. - -#### Node builders - -The `NodeContainer` contains an `INodeBuilder` member and provides a default implementation for it. So far we didn't had the need to make a custom implementation, since the structure of the graphs themselves is pretty stable. - -However, the `INodeBuilder` interface presents an `INodeFactory` member which we override. This factory allows to customize the nodes to be constructed. - -The `INodeBuilder` also contains a list of types to be considered as *primitive types*, which means that even if the type contains members or is a reference type, it will be, in term of graph, considered as a primitive value and won't be expanded. - -#### Nodes - -There are 3 types of nodes in Quantum: - -* `ObjectNode` are node corresponding to an object that is a reference type. They can contain members (properties, fields...), and items (collection). -* `BoxedNode` are a special case of `ObjectNode` that handles `struct`. They are able to write back the value of the struct in other nodes that reference them -* `MemberNode` are node corresponding to the members of an object. If the value of the member is a class or a struct, the member will also contain a reference to the corresponding `ObjectNode`. -* `ObjectNode` that are representing a collection of class/struct items will also have a collection of reference to target nodes via the `ItemReferences` property. - -Each node has some methods that allow to manipulate the value it's wrapping. `Retrieve` returns the current value, `Update` changes it. Collections can be manipulated with the `Add` and `Remove` methods (and a single item can be modified also with `Update`). - -#### Events - -Each node presents events that can be registered to: -* `PrepareChange` and `FinalizeChange` are raised at the very beginning and the very end of a change of the node value. These events are internal to Quantum. -* `MemberNode`s have the `ValueChanging` and `ValueChanged` events that are raised when the value is being modified. -* `ObjectNode` have `ItemChanging` and `ItemChanged` events that are raised when the wrapped object is a collection, and this collection is modified. - -The arguments of these events all inherits from `INodeChangeEventArgs`, which allows to share the handlers between collection changes and member changes. - -Finally, Quantum nodes are specialized for assets, where the implementation of the support of override and base is. These specialized classes also present `OverrideChanging` and `OverrideChanged` event to handle changes in the override state. - -## AssetPropertyGraph - -### Concept - -We use Quantum nodes mainly to represent and save the properties of an asset. The AssetPropertyGraph is a container of all the nodes related to an asset, and describes certain rules such as which node is an object reference, etc. - -### Asset references - -When an asset needs to reference another asset, it should never contains a member that is of the type of the referenced asset. Rather, the type of the member should be the type of the *Content* corresponding to the referenced asset. - -### Node listener - -A node listener is an object that can listen to changes in a graph of node (rather than an individual nodes). The base class is `GraphNodeChangeListener`, and this class must define a visitor that can visit the graph of nodes to register, and stop at the boundaries of that graph. - -### Object references - -In many scenarios of serialization (in YAML, but also in the property grid where objects are represented by a tree rather than a graph), we need a way to represent multiple referencers of the same object such a way that the object is actually expanded at one unique location, and shown/serialized as a reference to all other locations. We introduce the concept of **Object references** to solve this issue. - -By design, only objects implementing the `IIdentifiable` interface can be referenced from multiple locations from the same root object. But right now they can only be referenced from the same unique root object (usually an `Asset`). Later on we might support *cross-asset references* but this would require to change how we serialize them. - -There are two methods to implement to define if a node must be considered as an object reference or not: - -* one for members of an object: `IsMemberTargetObjectReference` -* one for items of a collection: `IsTargetItemObjectReference` - -## Node presenters - -Node presenters are objects used to present the properties of an object to a view system, such as a property grid. -They transform a graph of nodes to a tree of nodes, and contains metadata to be consumed by the view. -The resulting tree is slightly different from the graph. When an object A contains a member that is an object B that contains a property C, the graph will look like this: - -`ObjectNode A --(members)--> MemberNode B --(target)--> ObjectNode B --(members)--> MemberNode C` - -the corresponding tree of node presenters will be: - -`RootNodePresenter A --> MemberNodePresenter B --> MemberNodePresenter C` - -There is also a `ItemNodePresenter` for collection. On the example above, if B is instead a collection that contains a single item C, the graph would be: - -`ObjectNode A --(members)--> MemberNode B --(target)--> ObjectNode B --(items)--> ObjectNode C` - -the corresponding tree of node presenters will be: - -`RootNodePresenter A --> ItemNodePresenter B --> MemberNodePresenter C` - -Node presenter are constructed by a `INodePresenterFactory` in which `INodePresenterUpdater` can be registered. A `INodePresenterUpdater` allows to attach metadata to nodes, and re-organize the hierarchy in case it want to be presented differently from the actual structures (by inserting nodes to create category, bypassing a class object to inline its members, etc.). -`INodePresenterUpdater` have two methods to update node: - -* `void UpdateNode(INodePresenter node)` is called on **each** node, after its children have been created. But it's not guaranteed that its siblings, or the siblings of its parents, will be constructed. -* `void FinalizeTree(INodePresenter root)` is called once, at the end of the creation of the tree, and only on the root. Here it's guaranteed that every node is constructed, but you have to visit manually the tree to find the node that you want to customize. - -Node presenters listens to changes in the graph node they are wrapping. In case of an update, the children of the modified node are discarded and reconstructed. `UpdateNode` is called again on all new children, and `FinalizeTree` is also called again at the end on the root of the tree. Therefore, you have to be aware that an updater can run multiple time on the same nodes/trees. - -Metadata can be attached to node presenters via the `NodePresenterBase.AttachedProperties` property containers. These metadata are exposed to the view models as described in the section below. - -Commands can also be attached to node presenters. A command does special actions on a node, in order to update it. Node presenter commands implements the `INodePresenterCommand` interface. A command is divided in three steps, in order to handle multi-selection: -* `PreExecute` and `PostExecute` are run only once, for a selection of similar node presenters, before and after `Execute` respectively. -* `Execute` is run once per selected node presenter. - -### Node view models - -The view models are created on top of node presenters. Each node presenter has a corresponding `NodeViewModel`. In case of multi-selection, a `NodeViewModel` can actually wrap a collection of node presenters, rather than a single one. - -Metadata (ie. attached properties) are also exposed from the node presenter to the view via the view model, assuming they are common to all wrapped node presenter, if not, it is possible to add a `PropertyCombinerMetadata` to the property key to define the rule to combine the metadata. The default behavior for combining is to set the value to `DifferentValues` (a special object representing different values) if the values are not equals. - -Commands are also exposed. They are added to the view model, combined depending on their `CombineMode` property. They are transformed into WPF commands by being wrapped into a `NodePresenterCommandWrapper`. - -All members, attached properties, and commands of node view models are exposed as `dynamic` properties, and can therefore be used in databinding. - -All node view models are contained in an instance of `GraphViewModel`. A `GraphViewModelService` is passed in this object that acts as a registry for the node presenter commands and updaters that are available during the construction of the tree. - -### Template selector - -In order to be presented to the property grid, a proper template must be selected for each NodeViewModel. The `TemplateProviderSelector` object picks the proper template by finding the first registered one that accept the given node. Templates are defined in various XAML resource dictionaries, the base one being `DefaultPropertyTemplateProviders.xaml`. There is a priority mechanism that uses an `OverrideRule` enum with four values: `All`, `Most`, `Some`, `None`. One template can also explicitly override the other with the `OverriddenProviderNames` collection. The algorithm that picks the best match is in the `CompareTo` method of `TemplateProviderBase`. - -There is actually 3 levels of templates for each property. `PropertyHeader` and `PropertyFooter` represent the section above and the section below the expander that contains the children properties. In the default implementation (`DefaultPropertyHeaderTemplate` and most of its specializations), the header presents the left part of the property (the name, sometimes a checkbox...), and use the third template category, `PropertyEditor`, for the right side of the property grid. - -## Bases - -The base-derived concept and the override are stored in specialized Quantum nodes that implements `IAssetNode`. Properties (as well are items of collections) are automatically overridden when `Update`/`Add`/`Remove` methods are called. Some methods are also provided to manually interact with overrides, but it should not be used directly by users of Quantum. - -### Node linker - -`GraphNodeLinker` is an object that link a given node to another node. It has two main usages: it links objects that are game-side in the scene editor to their counterpart asset-side, and they also link a node to its base if it has one. - -The `AssetToBaseNodeLinker` is used to do that. It is invoked at initialization, as well as each time a property changes. It has a `FindTarget` method and `FindTargetReference`, which basically resolve, when visiting the derived graph, which equivalent node of the base graph corresponds to it. - -This linker is run from the `AssetPropertyGraph` that can then call `SetBaseNode` to actually link the nodes together. - -### Reconciliation with base - -Each time a change occurs in an asset, all nodes that have the modified nodes as base will call `ReconcileWithBase`. This method visits the graph, starting from the modified properties, and "reconcile" the change. The method is a bit long but well commented. The principle is, for each node, to detect first if something should be reconciled, and if yes, find the proper value (either cloning the value from the base, or find a corresponding existing object in the derived) and set it. - -`ReconcileWithBase` is also called at initialization to make sure that any desynchronization that could happen offline is fixed. - -## Future - -### Undo/redo - -The undo/redo system currently records only the change on the modified object, and rely on `ReconcileWithBase` to undo/redo the changes on the derived object. This is not an ideal design because there are a lot of consideration to take, and a lot of special cases. - -What we would like to do is: -* record everything that changes, both in derived and in base nodes -* disbranch totally automatic propagation during an undo/redo - -This design was not possible initially, and I'm not sure it is possible to do now - it's possible to hit a blocker when implementing it, or that it requires a lot of refactoring here and there before being doable. - -### Dynamic nodes - -Currently we still expose the real asset object in `AssetViewModel`, which it should never, in the editor, be modified out of Quantum node. Also, manipulating Quantum node is quite difficult sometimes due to indirection with target nodes, and access to members. - -``` -var partsNode = RootNode[nameof(AssetCompositeHierarchy.Hierarchy)].Target[nameof(AssetCompositeHierarchyData, IIdentifiable>.Parts)].Target; -partsNode.Add(newPart); -``` - -Ideally, we would like to use the `DynamicNode` objects (currently broken) to manipulate quantum nodes: - -``` -dynamic root = DynamicNode.Get(RootNode); -root.Hierarchy.Parts.Add(newPart) -``` - -If this is done properly, `AssetViewModel.Asset` could be turned private, and `AssetViewModel` could just expose the root dynamic node, which would allow to seemlessly manipulate the asset through a `dynamic` object. diff --git a/docs/technical/build-pipeline.md b/docs/technical/build-pipeline.md deleted file mode 100644 index 8e78d508f0..0000000000 --- a/docs/technical/build-pipeline.md +++ /dev/null @@ -1,153 +0,0 @@ -# Build pipeline - -This document describes the Build pipeline in Stride, its current implementation (and legacy), and the work that should be done to improve it. - -## Terminology - -* An **Asset** is a design-time object containing information to generate **Content** that can be loaded at runtime. For example, a **Model asset** contains the path to a source FBX file, and additional information such as an offset for the pivot point of the model, a scale factor, a list of materials to use for this model. A **Sprite font asset** contains a path to a source font, multiple parameters such as the size, kerning, etc. and information describing in which form it should be compiled (such as pre-rasterized, or using distance field...). **Asset** are serialized on disk using the YAML format, and are part of the data that a team developing a game should be sharing on a source control system. - -* **Content** is the name given to compiled data (usually generated from **Asset**s) that can be loaded at runtime. This means that in term of format, **Content** is optimized for performance and size (using binary serialization, and data structured in a way so that the runtime can consume it without re-transforming it). Therefore **Content** is the platform-specific optimized version of your game data. - -## Design - -Stride uses *Content-addressable storage* to store the data generated by the compilation. The main concept is that the actual name of each generated file is the hash of the file. So if, after a change, the resulting content built from the asset is different, then the file name will be different. An index map file contains the mapping between the content *URL* and the actual hash of the corresponding file. Parameters of each compilation commands are also hashed and stored in this database, so if a command is ran again with the same parameters, the build engine can easily recover the hashes of the corresponding generated files. - -### Build Engine - -The build engine is the part of the infrastructure that transforms data from the **assets** into actual **content** and save it to the database. It was originally designed to build content from input similar to a makefile. (eg. "compile all files in `MyModels/*.fbx` into Stride models). It has then been changed to work with individual assets when the asset layer has been implemented. Due to this legacy, this library is still not perfectly suited or optimal to build assets in an efficient way (dependencies of build steps, management of a queue for live-compiling in the Game Studio, etc.). - -#### Builder - -The `Builder` class is the entry point of the build engine. A `Builder` will spawn a given number of threads, each one running a `Microthread` scheduler (see `RunUntilEnd` method). - -#### Build Steps - -The `Builder` takes a root `BuildStep` as input. We currently have two types of `BuildStep`s: -* A `ListBuildStep` contains a sequence of `BuildStep` (Formerly we had an additional parent class called `EnumerableBuildStep`, but it has been merged into `ListBuildStep`). A `ListBuildStep` will schedule all the build steps it contains at the same time, to be run in parallel. Formerly we had a synchronization mechanism using a special `WaitBuildStep` but it has been removed. We now use `PrerequisiteSteps` with `LinkBuildSteps` to manage dependencies. -* A `CommandBuildStep` contains a single `Command` to run, which does actual work to compile asset. - -> **TODO:** Currently, when compiling a graph of build steps, we need to have all steps to compile in the root `ListBuildStep`. More especially, if we have a `ListBuildStep` container in which we want to put a step A that depends on a step B and C, we need to put A, B, C in the `ListBuildStep` container. This is cumbersome and error-prone. What we would like to do is to rely only on the `PrerequisiteSteps` of a given step to find what we have to compile. If we do so, we wouldn't need to return a `ListBuildStep` in `AssetCompilerResult`, but just the final build step for the asset, the graph of dependent build steps being described by recursive `PrerequisiteSteps`. The `ListBuildStep` container could be removed. We would still need to have lists of build steps when we compile multiple asset (eg. when compiling the full game), but it would be nothing that the build engine should be aware of. - -#### Commands - -Most command inherits from `IndexFileCommand`, which automatically register the output of the command into the command context. - -Basically, at the beginning of the command (in the `PreCommand` method), a `BuildTransaction` object is created. This transaction contains a subset of the database of objects that have been already compiled, provided by the `ICommandContext.GetOutputObjectsGroups()`. In term of implementation, this method returns all the objects that where written by prerequisite build steps, and all the objects that are already written in any of the parent `ListBuildStep`s, recursively. The objects coming from the parent `ListBuildStep` are a legacy of when we were using `WaitBuildStep` to synchronize the build steps. This hopefully should be implemented differently, relying only on prerequisite (since no synchronization can happen in the `ListBuildStep itself, everything is run in parallel). - -> **TODO:** Rewrite how OutputObjects are transfered from `BuildStep`s to other `BuildStep`s. Only the output from prerequisite `BuildStep` should be transfered. A lot of legacy makes this code very convoluted and hard to maintain. - -The `BuildTransaction` created during this step is mounted as a *Microthread-local database*, which is accessible only from the current microthread (which is basically the current command). - -At the end of the command (in the `PostCommand` method), every object that has been written in the database by the command are extracted from the `BuildTransaction` and registered to the current `ICommandContext` (which is how the `ICommandContext` can "flow" objects from one command to the other. - -It's important to keep in mind that objects accessible in a given command (in the `DoCommandOverride`) using a `ContentManager` are those provided during the `PreCommand` step, and therefore it is important that dependencies between commands (what other commmands a command needs to be completed to start) are properly set. - -### Compilers - -Compilers are classes that generate a set of `BuildStep`s to compile a given `Asset` in a specific context. This list could grow in the future if we have other needs, but the current different contexts are: -- compiling the asset for the game -- compiling the asset for the scene editor -- compiling the asset to display in the preview -- compiling the asset to generate a thumbnail - -#### IAssetCompiler - -This is the base interface for compiler. The entry point is the `Prepare` method, which takes an `AssetItem` and returns a `AssetCompilerResult`, which is a mix of a `LoggerResult` and a `ListBuildStep`. Usually there are two implementations per asset types, one to compile asset for the game and one to compile asset for its thumbnails. Some asset types such as animations might have an additional implementation for the preview. - -Each implementation of `IAssetCompiler` must have the `AssetCompilerAttribute` attached to the class, in order to be registered (compilers are registered via the `AssetCompilerRegistry`. - -> **TODO:** The `AssetCompilerRegistry` could be merged into the `AssetRegistry` to have a single location where asset-related types and meta-information are registered. - -Each compiler provides a set of methods to help discover the dependencies between assets and compilers. They will be covered later in this document. - -#### ICompilationContext - -> Not to be mistaken with `CompilerContext` and `AssetCompilerContext`. - -Contexts of compilation are defined by *types*, which allow to use inheritance mechanism to fallback on a default compiler when there is no specific compiler for a given context. Each compilation context type must implement `ICompilationContext`. Currently we have: - -* `AssetCompilationContext` is the context used when we compile an asset for the runtime (ie. the game). -* `EditorGameCompilationContext` is the context used when we compile an asset for the scene editor, which is a specific runtime. Therefore, it inherits from `AssetCompilationContext`. -* `PreviewCompilationContext` is the context used when we compile an asset for the preview, which is a specific runtime. Therefore, it inherits from `AssetCompilationContext`. -* `ThumbnailCompilationContext` is the context used when we compile an asset to generate a thumbnail. Generally, for thumbnails, we compile one or several assets for the runtime, and use additional steps to generate the thumbnail with the `ThumbnailCompilationContext` (see below). - -> **TODO:** Currently thumbnail compilation is in a poor state. In `ThumbnailListCompiler.Compile`, we first generate the steps to compile the asset in `PreviewCompilationContext`, then generate the steps to compile the asset in `ThumbnailCompilationContext`, and finally we like the first with the latter. Dependencies from thumbnail compilers (which load a scene and take screenshots) to the runtime compiler (which compile the asset) is **not** expressed at all. It just works now because in all current cases, the `PreviewCompilationContext` does what we need for thumbnails (for example, the `AnimationAssetPreviewCompiler` adds the preview model to the normal compilation of the animation, which is needed for both preview and thumbnail). - -### Dependency managers - -We currently have two mechanisms that handle dependencies. - -> **TODO:** Merge the `AssetDependencyManager` and the `BuildDependencyManager` together into a single dependency manager object. There is a lot of redundancy between both, one rely on the other, some code is duplicated. See `XK-4862` - -#### AssetDependencyManager - -The `AssetDependencyManager` was the first implementation of an mechanism to manage dependencies between assets. It works independently of the build, which is one of the main issue it had and the reason why we started to develop a new infrastructure. - -It is based essentially on visiting assets with a `DataVisitorBase` to find references to other assets. There are two ways of referencing an asset: - -- Having a property whose type is an implementation of `IReference`. More explicitely the only case we have currently is `AssetReference`. This type contains an `AssetId` and a `Location` corresponding to the referenced asset. -- Having a property whose type correspond to a *Content* type, ie. a type registered as being the compiled version of an asset type (for example, `Texture` is the Content type of `TextureAsset`). - -The problem of that design was that once all the references are collected, there is no way to know of the referenced assets are actually consumed, which could be one of the three following way: - -- the referenced asset is not needed to compile this asset, but it's needed at runtime to use the compiled content (eg. Models need Materials, who need Textures. But you can compile Models, Materials and Textures independently). -- the referenced asset needs to be compiled before this asset, and the compiler of this asset needs to load the corresponding content generated from the referenced asset (eg. A prefab model, which aggregates multiple models together, needs the compiled version of each model it's referencing to be able to merge them). -- the referenced asset is read when compiling this asset because it depends on some of its parameter, but the referenced asset itself doesn't need to be compiled first (eg. Navigation Meshes need to read the scene asset they are related to in order to gather static colliders it contains, but they don't need to compile the scene itself). - -#### BuildDependencyManager - -The `BuildDependencyManager` has been introduced recently to solve the problems of the `AssetDependencyManager`. It is currently not complete, and the ultimate goal is to merge it totally with the `AssetDependencyManager`. - -The approach is a bit different. Rather than extracting dependencies from the asset itself, we extract them from the compilers of the assets, which are better suited to know what they exactly need to compile the asset and what will be needed to load the asset at runtime. - -But one asset type can have multiple compilers associated to it (for the game, for the thumbnail, for the preview...). So the `BuildDependencyManager` works in the context of a specific compiler. - -Currently there is one `BuildDependencyManager` for each type of compiler. - -> **TODO:** Have a single global instance of `BuildDependencyManager` that contains all types of dependencies for all context of compilers. For example, we have thumbnail compilers that requires *game* version of assets, which means that the `BuildDependencyManager` for thumbnails will also contain a large part of the `BuildDependencyManager` to build the game. Merging everything into a single graph would reduce redundancy and risk to trigger the same operation multiple times simultaneously. - -#### AssetDependenciesCompiler - -The `AssetDependenciesCompiler` is the object that computes the dependencies with the `BuildDependencyManager`, and then generates the build steps for a given asset, including the runtime dependencies. It's the main entry point of compilation for the CompilerApp, the scene editor, and the preview. Thumbnails also use it, via the `ThumbnailListCompiler` class. - -> **TODO:** This class should be removed, and its content moved into the `BuildDependencyManager` class. By doing so, it should be possible to make `BuildAssetNode` and `BuildAssetLink` internal - those classes are just the data of the dependency graph, they should not be exposed publicly. To do that, a method to retrieve the dependencies in a given context must be implemented in `BuildDependencyManager` in order to fix the usage of `BuildAssetNode` in `EditorContentLoader`. - -### In the Game Studio - -The Game Studio compiles assets in various versions all the time. It has some specific way of managing database and content depending on the context. - -Remark: the Game Studio never saves index file on the disk, it keeps the url -> hash mapping in memory, always. - -#### Databases - -Before accessing content to load, a Microthread-local database must be mounted. Depending on the context, it can be a database containing a scene and its dependencies (scene editor), the assets needed to create a thumbnail, an asset to display in the preview... - -For the scene editor, this is handled by the `GameStudioDatabase` class. Thumbnails and preview also handle database mounting internally (in `ThumbnailGenerator` for example). - -> **TODO:** See if it could be possible/useful to wrap all database-mounting in the Game Studio into the GameStudioDatabase class. - -#### Builder service - -All compilations that occur in the Game Studio is done through the `GameStudioBuilderService`. This class creates an instance of `Builder`, a `DynamicBuilder` which allows to feed the Builder with build steps at any time. Having a single builder for the whole Game Studio allows to control the number of threads and concurrent tasks more easily. - -The `DynamicBuilder` class simply creates a thread to run the Builder on, and set a special build step, `DynamicBuildStep`, as root step of this builder. This step is permanently waiting for other child build step to be posted, and execute them. - -> **TODO:** Currently the dynamic build step waits arbitratly with the `CompleteOneBuildStep` method when more than 8 assets compiling. This is a poor design because if the 8 assets are for example prefabs who contains a lot of models, materials, textures, it will block until all are done, although we could complete the thumbnails of these models/materials/textures individually. Ideally, this `await` should be removed, and a way to make sure thumbnails of assets which are compiled are created as soon as possible should be implemented. - -The builder service uses `AssetBuildUnit`s as unit of compilation. A build unit corresponds to a single asset, and encapsulates the compiler and the generated build step of this asset. - -#### EditorContentLoader - -The scene editor needs a special behavior in term of asset loading. The main issue is that any type of asset can be modified by the user (for example a texture), and then need to be reloaded. Stride use the `ContentManager` to handle reference counting of loaded assets. With a few exception (Materials, maybe Textures), it does not support hot-swapping an asset. Therefore, when an asset needs to be reloaded, we actually need to unload and reload the *first-referencer* of this asset. - -The *first-referencer* is the first asset referenced by an entity, that contains a way (in term of reference) to the asset to reload. For example, in case of a texture, we will have to reload all models that use materials that use the texture to reload. - -This is done by the `EditorContentLoader` class. At initialization, this class collects all *first-referencer* assets and build them. Each time an asset is built, it is then loaded into the scene editor game, and the references (from the entity to the asset) are updated. This means that this class needs to track all first-referencers on its own and update them. This is done specifically by the `LoaderReferenceManager` object. The reference are collected from the `GameEditorChangePropagator`, an object that takes the responsibility to push synchronization of changes between the assets and the game (for all properties, including non-references). There is one instance of it per entity. When a property of an entity that contains a reference to an asset (a *first-referencer*) is modified, the propagator will trigger the work to compile and update the entity. In case of a referenced asset modified by the user, `EditorContentLoader.AssetPropertiesChanged` takes the responsibility to gather, build, unload and reload what needs to be reloaded. - -## Additional Todos - -> **TODO:** `GetInputFiles` exists both in `Command` and in `IAssetCompiler`. It has the same signature in both case, so it's returning information using `ObjectUrl` and `UrlType` in the compiler, where we are trying to describe dependency. That signature should be changed, so it returns information using `BuildDependencyType` and `AssetCompilationContext`, just like the GetInputTypes method. Also, the method is passed to the command via the `InputFilesGetter` which is not very nice and has to be done manually (super error-prone, we had multiple commands that were missing it!). An automated way should be provided. - -> **TODO:** The current design of the build steps and list build steps is a *tree*. For this reason, same build steps are often generated multiple times and appears in multiple trees. It could be possible to cache and share the build step if the structure was a *graph* rather than a *tree*. Do to that, the `Parent` property of build steps should be removed. The main difficulty is that the way output objects of build steps flow between steps has to be rewritten. - - diff --git a/docs/technical/copy-paste.md b/docs/technical/copy-paste.md deleted file mode 100644 index 7014f03cbf..0000000000 --- a/docs/technical/copy-paste.md +++ /dev/null @@ -1,218 +0,0 @@ -# Copy and paste - -## Introduction - -### Rationale -Any good editing software has some kind of copy/paste system and Stride is no exception. Copy/paste should be intuitive and work in a lot of cases: any situation that make sense for a user. - -### Goals -From a usability point of view, the capabilities of the copy/paste system should be: -* Copy anything. -* Paste anywhere. - -### Scope -The copy/paste system should at term support all those cases: -* copy/paste of assets -* copy/paste of properties of assets -* copy/paste of parts of assets (e.g. entities in a scene or prefab) -* copy/paste of settings -* support for copy/paste between different instances of the GameStudio - -### Current state (October 2017) - -* [x] copy/paste of assets -* [-] copy/paste of properties of assets - * [x] support for primitives - * [x] support for collections - * [-] partial support for dictionaries - * [x] support for asset references and asset part references - * [x] support for structures and class instances - * [ ] no support for virtual properties (copy works in some cases, paste doesn't) -* [-] copy/paste of parts of assets - * [x] support for entities in scene or prefab - * [x] support for UI elements, but need more testing, especially regarding attached properties - * [x] support for sprites in spritesheet -* [ ] copy/paste of settings (should be easy to add) -* [-] support for copy/paste between different instances of the GameStudio - * there is no technical obstacle as copying use the clipboard - * already working, but need more testing, especially regarding identifiers and references - * might need to introduce a unique Guid per project (or even per GameStudio instance) to detect and solve potential conflicts - -## Workflow -From the user point of view, the entry points in the GameStudio are context menus (property grid, assets in asset view, entities in scene and prefab editors, etc.). Keyboard shortcuts ("Ctrl+C" and "Ctrl+V") also work in the same location. - -### Copy -The order of events when the user copies "something" are: -1. keyboard or context menu -2. copy command in the corresponding editor or viewmodel -3. eventually, some preparation code specific to the editor or asset -4. call to one of `ICopyPasteService` copy methods - 1. encapsulation into a `CopyPasteData` container - 2. collection of necessary metadata - 3. serialization to `string` -5. save to clipboard - -### Paste -The order of events when the user pastes "something" are: -1. get text from clipboard and check that data is valid -2. call `ICopyPasteService.DeserializeCopiedData()` method - 1. deserialization from `string` - 2. find a valid `IPasteProcessor` for the data - 3. call `IPasteProcessor.ProcessDeserializedData()` method - 4. apply metadata overrides -3. actual paste - * either use the result directly (simple case) - * or call `IPasteProcessor.Paste()` (more complex scenario such as entities) - -## Implementation details -The copy/paste API is exposed by the `ICopyPasteService` interface. It is available by consumers through the `ServiceProvider` (see `ViewModelBase` class). - -Implementation details are hidden from the API as only interfaces are exposed: `ICopyPasteService`, `IPasteResult`, `IPasteItem`, `IPasteProcessor`. This makes integration easier and allows extensibility for the future. - -### Service - -#### `ICopyPasteService` interface -This interface is the main entry point for the copy/paste API. It exposes the copy and paste method as well as the registration (or unregistration) of processors. - -##### `CopyFromAsset()` and `CopyFromAssets()` methods -Those methods create a serialized version of an asset or part of an asset that can then be put into the clipboard. - -##### `CopyMultipleAssets()` method -This is a legacy method that is only used to copy a collection of `AssetItem`. Ideally it should be reworked so that `CopyFromAssets()` could be used instead. That implies modifying the call-site of this method (see `AssetCollectionViewModel.CopySelection()`) as well as the corresponding paste process (see `AssetItemPasteProcessor` and `AssetCollectionViewModel.Paste()`). - -##### `CanPaste()` method -This method allows to quickly check if the serialized data can be pasted given the expected types of the target. - -##### `DeserializeCopiedData()` method -This method attempts to deserialize the string data into a object compatible with the target. The object returned (`IPasteResult` see below) contains the data (if the process was successful) and a reference to the paste processors that were used. - -#### `CopyPasteService` class -Internal implementation of the `ICopyPasteService` interface. it doesn't expose more functionalities than the interface. - -### Data and serialization -When the copy service is asked to copy some objects, it first put them in a container before serialization. The container has some additional properties and metadata that gives some context to the copied objects. These metadata will then be used when pasting to help resolve some situations. - -#### `CopyPasteData` class -It is the top container of copied data. In the serialized YAML it is the root of the document. - -##### `ItemType` property -This string property contains the type of the copied items, serialized as a YAML tag. Having the type available as a top property allows before pasting to quickly check the type of the data without deserializing the whole document. - -##### `Items` property -The copy/paste feature supports copying more than one object at a time, provided that the object types are all compatible (either same type or share a common base type). This property holds the list of copied items. - -##### `Overrides` property -Objects that are copied from the property grid can override their base (e.g. in case of an archetype or prefab). Before serialization, the overrides metadata are collected for the copied objects and put into this property. - -#### `CopyPasteItem` class -Each item is also put inside a container in order to attach per-item contextual metadata. - -##### `Data` property -The copied data itself. - -##### `SourceId` property -Identifier to the asset from which the data was copied. This will be used later by the paste processors to determine whether the pasted data must be cloned or used as-is depending on some conditions. - -##### `IsRootObjectReference` property -Indicates if the copied data is a reference to another object. - -#### `PasteResult` class -(implements `IPasteResult` interface) -Similarly to the copy step, pasted data (i.e. copied data that has been deserialized and processed by a paste processor) is returned by the service inside a container. The paste result is itself a collection of items as each `CopyPasteItem` from the copied data is processed separately. - -#### `PasteItem` class -(implements `IPasteItem` interface) -Represents one item of the resulting paste data. It also contains a reference to the processor that was used to process the deserialized data. - -### Copy processors -(implement `ICopyProcessor`) - -A copy processor processes the data before it is serialized. At the moment there is only one such processor. - -Remark: copy processors are registered as plugins (see `AssetsPlugin.RegisterCopyProcessors()`). - -#### `EntityComponentCopyProcessor` -This copy processor is applied when copying a `TransformComponent` or an `EntityComponentCollection` containing one or more `TransformComponent`. In such cases, the list of children of the transform is cleared so that only the transform properties (position, rotation and scale) are copied. - -### Paste processors -(implement `IPasteProcessor`) - -A paste processor has two roles: -* first, it processed the data just after it has been deserialized. That step prepares the data before it can be applies to the target. This usually involves converting to match certain types and resolving references. -* if the data could be processed, it then paste it into the final target object. Only during that step is an actual asset modified. - -Paste processors are registered as plugins (see `AssetsPlugin.RegisterPasteProcessors()`). The order of registration matters: when looking for a matching processor, the service will iterate through the list of registered processors in reverse order (last registered first) and return the first one than can process the data (i.e. the first one which `Accept()` method returns `true`). At the moment it is working fine but when plugins will be more widely supported it might cause some conflicts. An explicit priority order could be given to each processor. - -Currently the registration order is: -1. `AssetPropertyPasteProcessor` -2. `AssetItemPasteProcessor` -3. `EntityComponentPasteProcessor` -4. `EntityHierarchyPasteProcessor` -5. `UIHierarchyPasteProcessor` - -#### `AssetPropertyPasteProcessor` class -This is the default paste processor with the lowest priority (registered first, see above). It supports the following features: -* pasting a value into a target property -* pasting a single item into a target collection (appending or adding one item depending on the index) -* pasting a collection into a target collection (appending or inserting items depending on the index) -* replacing a target collection with a single item or a collection - -It will also try to convert the pasted value into the type or the target (see `TypeConverterHelper` helper class). - -#### `AssetItemPasteProcessor` class -This processor only accepts single object or collection of `AssetItem`. It is used when copying and pasting assets in the asset view. - -#### `EntityComponentPasteProcessor` class -(inherits `AssetPropertyPasteProcessor`) - -This processor extends the behavior of `AssetPropertyPasteProcessor` in the case of `EntityComponent`. It adds some special rules specific to components: -* the `TransformComponent` cannot be removed from an `EntityComponentCollection` -* the `TransformComponent` cannot be replaced by a different type of component -* when replacing the `TransformComponent`, instead manually replace its properties (position, rotation and scale) -* multiple instances of component are allowed only if the component class is decorated with a `AllowMultipleComponentAttribute`. - -#### `AssetCompositeHierarchyPasteProcessor` class -This processor supports pasting hierarchical data (`AssetCompositeHierarchyData`) into a hierarchical asset composite (`AssetCompositeHierarchy`). Typically used for prefab, scene or UI assets. - -The tricky part is actually handling all the part references (hierarchy) and the inheritance from the base (composite). There is a lot of cloning and remapping of identifiers involved in that process. - -#### `EntityHierarchyPasteProcessor` class -(inherits `AssetCompositeHierarchyPasteProcessor`) - -This processor is dedicated to hierarchy of entities (i.e. scene or prefab assets). It handles the actual pasting into the target asset. - -#### `UIHierarchyPasteProcessor` class -(inherits `AssetCompositeHierarchyPasteProcessor`) - -This processor is dedicated to hierarchy of UI elements (i.e. UI page or library assets). It handles the actual pasting into the target asset. - -### Post-paste processors -(implement `IAssetPostPasteProcessor`) - -Small hack to apply special case when a scene asset is copied/pasted in the asset view. This should be reworked to allow more general cases. - -Remark: post-paste processors are registered as plugins (see `AssetsPlugin.RegisterPostPasteProcessors()`). - -#### `ScenePostPasteProcessor` class -Because scene asset are also hierarchical (a scene can contain child scenes), when creating a copy of a scene those relationship must be cleared. - -### Editor commands -In the property grid, the copy, paste and replace capabilities are available through the context menu of the properties and keyboard shortcuts. There are implemented by node commands. - -#### `CopyPropertyCommand` class -This command assumes that data can always be copied and thus is available on all asset nodes. It basically asks the `ICopyService` to serialize the node value and then sets the clipboard. - -#### `PastePropertyCommandBase` class -This command implements the paste capability in the property grid. It is always attached to all asset nodes. However it is disabled, when pasting is not possible: readonly property, incompatible data. - -When pasting, the command automatically creates a transaction to enable undo and redo. - -This abstract class is inherited by `PastePropertyCommand` and `ReplacePropertyCommand` where the only difference is that the latter will set the `AssetPropertyPasteProcessor.IsReplaceKey` property key to `true`. Depending on the value, paste processors will either paste or replace. It is only meaningful in the context of collection, as pasting a value to a single property is the same as replacing it. - -### Others - -#### `SafeClipboard` class -The `System.Windows.Clipboard` can sometimes throw `COMException` when the clipboard is not available (only one process can access the clipboard at a given time). This class is a tiny wrapper that silently ignores (catches) those exceptions. - -## Documentation and references -The only user documentation currently existing can be found in one blog post (https://stride3d.net/blog/copy-paste/) and the release notes of the 1.9-beta version (http://doc.stride3d.net/latest/en/ReleaseNotes/ReleaseNotes-1.9.html). diff --git a/docs/technical/editor-localization.md b/docs/technical/editor-localization.md deleted file mode 100644 index 47c069433c..0000000000 --- a/docs/technical/editor-localization.md +++ /dev/null @@ -1,657 +0,0 @@ -# Editor localization - -## Introduction - -### Rationale -Currently (Stride 2.1) the editor is mostly available in English, although there is very partial support for Japanese. Ideally Stride should be available in a range of languages, starting with English and Japanese. Other languages will probably be be easy to add later if needed. - -Supporting multiple languages not only covers every text or tooltip that appear in the UI of the editor, but also error messages, logs and the documentation (we have plan to integrate part of the documentation directly in the Game Studio). - -We want also to simplify the workflow of translating the application so that future updates and fixes can be easily integrated. The translation itself will probably be done by an external contractor that doesn't necessarily have technical knowledge of Stride. - -And finally, we should have a solution that is flexible enough so that translations can be added/updated without recompiling. This could allow third-party or community-based translation for languages that we won't officially support but that might add value for some people. - -### Goals -To summarize, the design goals of the localization system are: - -* Easy for developers to add, change or remove text that should be localized. -* Easy for translators to understand the context of the text to be localized, so they can provide the best translation. - * This also means that they should be provided with a unified document format independent of the underlying technology used. -* Support for versioning. - * i.e. text format. -* No need to recompile the GameStudio to update a language, an application restart should be able to pickup the latest version. - * Consider delivery of translation updates outside of the normal release cycle. - -### Scope -The localization system should at term support all those cases: - -* static UI (mostly XAML) - * essentially text, but that may also include images or icons -* messages defined in code - * error messages, logs, etc. -* property grid attributes and types - * `[Display]`, `[Category]`, enums, types, etc. -* assets - * game templates that contain a description - -### Current state (October 2017) -* [x] static UI -* [x] messages in code (but not all translated) -* [ ] property grid - * [x] support for enum tooltips -* [ ] assets - -## Workflow - -### Basic workflow -First localized text need to be identified and "marked". Then they can be extracted to an independent text file (template) which is given to translators. For each supported language translators create or update a file matching the template. Those translated text file can then be imported by the Game Studio and used to display texts and messages in the UI. - -In short, development -> extraction -> translation -> import. - -A special care should be taken in case of update of existing (and possibly already translated) strings. - -### Development -To ease the work of the developer, the API should be minimal and text to translate should be extractable by a tool. - -#### XAML -Traditionally, when working with resource files (**.resx**) and satellite assemblies, the developer must lookup the correct key or create a new one if necessary. This is both time-consuming and error-prone. - -The current solution, based on a gettext-like technology, use a markup extension (`LocalizeExtension`) for most cases and a value converter (`Translate`) for more advanced cases. The main advantage of a gettext approach is that localization context and plurals are supported out of the box without much trouble. Plurals especially can be complex as the rules differ depending on the language: Japanese doesn't have plurals, Latin-based languages usually have two forms: singular and plural, Arabic has 6 forms, etc. - -Examples of use of `LocalizeExtension`: -```xml - - - - - - -``` - -Examples of use of `Translate`: -```xml - - - - -``` - -Notes: -* Since the converter is used for bindings, the text to localize cannot be extracted from the XAML (same case as when using a static string reference with the markup extension). So the developer must ensure that the related entries are available. -* Plurals and context are not supported as binding (context is supported as a literal attribute), but could be added if necessary using a kind of multi-binding (not implemented yet). A typical use could look like: -```xml - -``` -* Also one difference between the markup extension (Localize) and the converter (Translate) is that the latter converts dynamically. So if the value changes, it will look for the translation of that new value. On the other hand the markup extension works statically: the value is only provided once. - -#### C# Code -The main entry point is the `ITranslationManager` interface which is accessed through the singleton `TranslationManager.Instance` (lazy initialized). It is agnostic of the underlying technology (though inspired by Gettext) used for providing the translation and define a minimal interface. Several providers can be registered to the manager (typically one per localized assembly). Through the provider interface (`ITranslationProvider`), developer can query for translated text. For convenience `ITranslationManager` itself implements the `ITranslationProvider` interface. - -Initialization (typically in the `Module` class of an assembly): -```csharp -TranslationManager.Instance.RegisterProvider(new GettextTranslationProvider()); -``` - -Change current language: -```csharp -TranslationManager.Instance.CurrentLanguage = new CultureInfo("en-US"); -``` - -Examples of use: -```csharp -// Get a simple string -var str = TranslationManager.Instance.GetString("Some text."); -// Get a string supporting plurals -var plural = TranslationManager.Instance.GetPluralString("{0} fox", "{0} foxes", 42); -// Get a string with a context -var context = TranslationManager.Instance.GetParticularString("Some text.", "some context"); -// Get a string with a context and supporting plurals -var contextPlural = TranslationManager.Instance.GetParticularPluralString("{0} fox", "{0} foxes", 42, "some other context"); -``` - -Notes: -* When a translation is not available in the current language, the methods from the provider return the original string. - -Like the `ResourceSet` and `ResourceManager` (**.resx** files) that it mimics, Gettext supports language inheritance, i.e. if the current locale is "fr-FR" it will first look for translation for "fr-FR", then fallback to "fr" if not found, then fallback to neutral if not found (then fallback to returning the string as-is as a last resort). - -```csharp -TranslationManager.Instance.RegisterProvider(new GettextTranslationProvider()); -// Change current culture to French (neutral) -TranslationManager.Instance.CurrentLanguage = new CultureInfo("fr"); -// if a French translation exists, returns it. Otherwise returns the same text -Console.WriteLine(provider.GetString("Hello, World!")); -// Change current culture to French (France) -TranslationManager.Instance.CurrentLanguage = new CultureInfo("fr-FR"); -// if a French (France) translation exists, returns it. Otherwise looks for French (neutral). Otherwise returns the same text -Console.WriteLine(provider.GetString("Hello, World!")); -``` - -To localize C# constructs such as class, enum or property, the `TranslationAttribute` can be used. Typical use of this feature includes decorating static strings and enums. - -Declarations: -```csharp -public static class Strings -{ - [Translation("Some text")] - public static readonly string SomeText = "Some text"; -} - -public enum Groups -{ - [Translation("First group")] - Group0, - [Translation("Second group")] - Group1, -} -``` - -Uses (C#): -```csharp -var group = Groups.Group0; -Console.WriteLine(TranslationManager.Instance.GetString(group.ToString())); -``` - -Uses (XAML): -```xml - - -``` - -### Extraction -Instead of manually creating the resources files, a tool is responsible to extract all localizable strings from the source code (both **.cs** and **.xaml** files). - -Notes: -* For convenience, a batch script (**extract_strings.bat**) is provided in **sources\localization**. - -#### Export formats -The extractor too can export the strings into a gettext-compatible format (**.pot**). Other formats could be added later (e.g. CSV, XLIFF). - -#### Example of exported file -```t -msgid "" -msgstr "" -"Project-Id-Version: \n" -"POT-Creation-Date: 2017-05-26 14:41:20+0900\n" -"PO-Revision-Date: 2017-05-26 14:41:20+0900\n" -"Last-Translator: \n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=utf-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: MonoDevelop Gettext addin\n" - -#: ViewModels/MainWindowViewModel.cs:17 -msgid "Some translated text" -msgstr "" - -#: ViewModels/MainWindowViewModel.cs:14 -msgctxt "UI" -msgid "Main Window" -msgstr "" - -#: ViewModels/MainWindowViewModel.cs:55 -msgid "{0} fox" -msgid_plural "{0} foxes" -msgstr[0] "" -msgstr[1] "" -``` - -Each entry consists of a few elements: -* `msgid` is the original (untranslated) `text`. It corresponds to the text parameter in the `ITranslationProvider` methods. -* `msgid_plural` is the original (untranslated) plural version of the text and is an optional parameter. It corresponds to the `textPlural` parameter in the `ITranslationProvider` methods. -* `msgstr` will be the language-specific translation(s). In a template (.pot) file, it is empty. It will be filled by the translator when creating the dedicated **.po** file for a given language. - * when a plural form is expected, this become an indexed array of translations. The .pot contains two indexed entries (0 and 1), since it is the default in most Latin-based language (e.g English). -* `msgctxt` is the context of the text and is an optional parameter. It corresponds to the context parameter in the `ITranslationProvider` methods. -* Comments are supported and indicated with a starting **#** character. The character immediately following indicates the type of comment: - * a whitespace indicates a manual comment, usually added by the translator. - * a colon (**:**) indicates a source file reference where occurrences of the text appear. If there is more than one occurrence, multiple comments can be used. - * a point (**.**) indicates a comment added by the developers. - * other types are supported. See https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html#PO-Files. - -#### Merging -The `Catalog` class in the `GNU.Gettext` library already supports some kind of merging with an existing **.pot** file. Additionally, some tools (such as Poedit, see below) support updating an existing translation with a template (**.pot**) file. - -When the `--merge` option is used, the existing file is read and new entries found by the extractor tool are added. For the moment, existing entries that are not found again are not deleted but this could be added as an option. - -Additionally, the standard distribution of Gettext includes a set of utilities that can be used to automatize the manipulation of existing .po files. This includes: - -* ``msgmerge`` to merge two existing .po files together, or to update a **.po** files from a more recent **.pot** files (see https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html#msgmerge-Invocation) * Note that the "merge with POT" option in Poedit is probably based on this utility. -* all kind of manipulations such as comparing, appending, filtering. See complete list here: https://www.gnu.org/software/gettext/manual/html_node/Manipulating.html#Manipulating - -##### Text added -New text entries are added to the newly extracted **.pot** template compared to the one that was used to create the existing **.po** files. - -In that case, the merge is easy and non conflicting: after using msgmerge (or Poedit), the new entries will be added to the **.po** files with empty translations. - -previous **MyApp.fr.po**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" -``` - -new **MyApp.pot**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "" -msgstr[1] "" - -msgid "{0} fox" -msgid_plural "{0} foxes" -msgstr[0] "" -msgstr[1] "" -``` - -new (merged) **MyApp.fr.po**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" - -msgid "{0} fox" -msgid_plural "{0} foxes" -msgstr[0] "" -msgstr[1] "" -``` - -##### Text removed -Text entries are missing in the newly extracted **.pot** template compared to the one that was used to create the existing **.po** files. - -When using `msgmerge` (or Poedit), the missing entries that have already been translated will be mark as obsolete in the **.po** files (commented out with `#~`). They won't appear in Poedit UI anymore, until they are restored in the **.pot** later. Missing entries that were not translated yet are completely removed to keep the file clean. - -previous **MyApp.fr.po**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" - -msgid "{0} fox" -msgid_plural "{0} foxes" -msgstr[0] "renard" -msgstr[1] "renards" -``` - -new **MyApp.pot**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "" -msgstr[1] "" -``` - -new (merged) **MyApp.fr.po**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" - -#~ msgid "{0} fox" -#~ msgid_plural "{0} foxes" -#~ msgstr[0] "renard" -#~ msgstr[1] "renards" -``` - -##### Text changed -When an original text in an entry has changed, `msgmerge` (or Poedit) will attempt to find and match the previous text and will mark the translation as fuzzy (i.e. need work). When used with the `--previous` option (which seem to be the case in Poedit), the previous matched text will be preserved (commented out with `#|`). - -previous **MyApp.fr.po**: -```t -msgid "{0} horse" -msgid_plural "{0} horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" -``` - -new **MyApp.pot**: -```t -msgid "{0} big horse" -msgid_plural "{0} big horses" -msgstr[0] "" -msgstr[1] "" -``` - -new (merged) **MyApp.fr.po**: -```t -#, fuzzy -#| msgid "{0} horse" -#| msgid_plural "{0} horses" -msgid "{0} big horse" -msgid_plural "{0} big horses" -msgstr[0] "{0} cheval" -msgstr[1] "{0} chevaux" -``` -Note that if `msgmerge` (or Poedit) cannot find and match a previous text, it will fallback to the add/remove cases. - -Of course, after the merge the new file must be transmitted to the translators for update. - -### Translation -After having extracted the localizable strings, the next step is to actually translate them into the supported languages. - -The **.pot** file is a textual format that can theoretically be edited manually. However, this can be a difficult especially for non-technical people. Fortunately a number of tools exist than can recognize this format and work with it smoothly. - -#### Using Poedit -Poedit ([website](https://poedit.net/), [source code (MIT)](https://github.com/vslavik/poedit/)) allows to create and manage gettext catalog file (**.po**). It also support creating and updating an existing catalog from a template (**.pot**) applying the correct plural rules corresponding to the selected language. - -Poedit application is translated into several language including Japanese (100% according to the [Crowdin project](https://crowdin.com/project/poedit/ja)). - -##### Open a file (**.pot** or **.po**) -Poedit supports opening both **.po** (catalog) and **.pot** (template) files. The main difference is that a template is not bound to a particular localization while a catalog is. Templates contains extracted strings and ce be seen as a read-only input in the translation process. Catalogs can be modified and are the output of this process. - -After opening a template file, the translator can create a new translation by clicking on the "Create new translation" button at the bottom. It opens a dialog where the target language can be selected. This is important in order to setup the correct plural rules for the chosen language. - -![](media/poedit-open-pot-file.png) - -##### Editing a catalog -The process of editing in poedit is straightforward. A list of source strings is visible at the top. After selecting an entry, the translation (or translations incase of plurals) can be edited at the bottom. Poedit also offers translation suggestions based on some external database, but is not always accurate and is a limited feature in the free version. - -Comments (added by developers and by the extraction tool) are also visible at the bottom right. Translators can also add their own comments. This can be use for example to communicate between the translators team and the developers. - -![](media/poedit-edit-po-file.png) - -It also supports a "Need work" flag (that is translated into the fuzzy flag in the **.po** file) to indicate when a translation might be incorrect or need more work. In that case it will appear in orange in the list. The flag is usually set by the extraction tool, but translators can also set or unset it at will. - -##### Update from a more recent template -After a translation has been created by a translator, changes can still happen as development continues. To deal with this case, Poedit has an option to update a catalog (**.po**) from a template (**.pot**). To do so go to **Menu-->Catalog-->Update from POT files...** and select the corresponding file. - -What happens is that any new entry is added while previously existing one are kept as-is. There is also a very neat option when a source text has slightly changed, Poedit will detect it and display the entry has fuzzy while also indicated that it detected a change. - -On the other hand, if an entry is completely removed, it will disappear from the Poedit interface but will stay in the **.po** file (commented out using the `#~` prefix) until it is purged. One advantage of not removing it completely is that if it is reintroduced, the existing translation will be restored. - -Notes: -* Internally Poedit seem to be using the `msgmerge` tool (see "Merging" section above). - -##### Saving -By convention the name of the catalog file should match the name of the template with the addition of the target language (in [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) format). It should also be saved in a folder corresponding to that language. - -For example the Japanese catalog for `Stride.GameStudio` should be named **Stride.GameStudio.ja.po** and saved in a **ja/** folder. - -### Import - -#### Supported import formats -For the moment we only support **.po** and **.resx** files compiled into satellite assemblies. However the `Stride.Core.Translation` library is flexible and can be extended to support additional providers. This could include CSV (not necessarily compiled), XLIFF, **.mo** files (which is another kind of compiled **.po**). This could also be considered for asset translation (e.g. **.sdtpl**) where we externalize the translations into files that can be distributed separately and loaded/discovered using a dedicated provider. - -#### Compilation to satellite assemblies -**.po** files can be compiled into a satellite assemblies that the `GettextTranslationProvider` (and under the hood the `GNU.Gettext` library) will use to retrieve translations for a given language. It it a similar mechanism to the satellite assemblies generated from the **.resx files**. In fact `GettextResourceManager` inherits from `ResourceManager` with additional support for capabilities provided by Gettext such as context and plurals. - -To create such assembly, the GNU.Gettext.Msgfmt.exe command line must be used. Usage is show below: - -``` -Msgfmt (Gettext.NET tools) -Custom message formatter from *.po to satellite assembly DLL or to *.resources files - -Usage: - GNU.Gettext.Msgfmt[.exe] [OPTIONS] filename.po ... - -r resource, --resource=resource Base name for resources catalog i.e. 'Solution1.App2.Module3' - Note that '.Messages' suffix will be added for using by GettextResourceManager - - -o file, --output-file=file Output file name for .NET resources file. - Ignored when -d is specified - - -d directory Output directory for satellite assemblies. - Subdirectory for specified locale will be created - - -l locale, --locale=locale .NET locale (culture) name i.e. "en-US", "en" etc. - - -L path, --lib-dir=path Path to directory where GNU.Gettext.dll is located (need to compile DLL) - - --compiler-name=name C# compiler name. - Defaults are "mcs" for Mono and "csc" for Windows.NET. - On Windows you should check if compiler directory is in PATH environment variable - - --check-format Verify C# format strings and raise error if invalid format is detected - - --csharp-resources Convert a PO file to a .resources file instead of satellite assembly - - -v, --verbose Verbose output - - -h, --help Display this help and exit -``` - -The command needs to find a C# compiler in the path (in our case csc that can be found in the Roslyn folder under the *MSBuild* installation). - -In Stride's projects file, a command line similar to the following one is used: - -``` -Path=$(MSBuildBinPath)\Roslyn;$(Path) -IF EXIST "$(SolutionDir)..\sources\localization\ja\$(TargetName).ja.po" "$(SolutionDir)..\sources\common\deps\Gettext.Net\GNU.Gettext.Msgfmt.exe" --lib-dir "$(SolutionDir)..\sources\common\deps\Gettext.Net" --resource $(TargetName) -d "$(TargetDir)." --locale ja "$(SolutionDir)..\sources\localization\ja\$(TargetName).ja.po" --verbose -``` - -Remarks: - -* By convention, the **.po** filenames are suffixed by the [IETF language tag](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g. "en" for English, "fr" for French, "ja" for Japanese). Note that the same tags are recognized by the .Net `CultureInfo` class. -* The generated satellite assemblies must be located into a dedicated subfolder relative to where the executable is (same rule as with assemblies generated from **.resx** files). The command line already takes care of it through the `-d` argument. -* The generated satellite assemblies are named after the corresponding assembly that is localized, suffixed by ".Messages" (then by ".resources" as is the convention for satellites). For example for **Stride.GameStudio.exe** the satellite assembly is named **Stride.GameStudio.Messages.resources.dll** - -### Update -When developers add or remove the strings that can be localized, the same workflow as described above must be run. Because it includes changes in both the original assemblies and the satellite assemblies, it implies that a new version of the product must be released. - -There are some cases though were we might just want to correct typos, without adding or removing localizable strings. Normally developers would fix the typos in code. But that means that they "keys" of the gettext catalog would change which will require updating all translations as well. - -If only a few corrections are needed, there is another alternative. The localizable strings present in the code (**.cs** or **.xaml** file) are considered as *neutral* language, not as *English*. So there is a way to provide an English translation to them. While that may sound a bit silly (those strings are already in English), it is a nice hack to make quick fixes. - -Consider this: instead of fixing in code, rebuilding the whole product and updating all translations and then releasing the whole as a brand new Stride version, developers just need to follow the same workflow as with any other language but by creating an English translation instead (e.g. **Stride.GameStudio.en.po**). Then just fix the entries that need fixing and generate a satellite assembly (e.g. **Stride.GameStudio.Messages.resources.dll**) and distribute it. Tell the users to copy it into a **en/** folder in **Bin/Windows** of Stride installation and voilà! At runtime when the English language is selected (which is the default), the translation system will pick-up this satellite assembly and uses its entries as a translation for English. - -## Implementation details - -### `GNU.Gettext` assembly -This assembly contains a port of GNU gettext to .Net. It is part of the [Gettext for .NET/Mono project](https://sourceforge.net/projects/gettextnet/). - -#### `GettextResourceManager` class -This class inherits from `System.Resources.ResourceManager`. Instances of this class are used by the `GettextTranslationProvider` to retrieve translations from localized strings. A resource manager handles one or several resource sets corresponding to a language and its derivatives (e.g. "en" and "en-US"). - -#### `GettextResourceSet` class -This class inherits from `System.Resources.ResourceSet`. Instances of this class will be generated from **.po** files and compiled into satellite assemblies. A resource set is a big hashtable of strings with the original localized string as key and the translated string as value. - -### `Stride.Core.Translation` assembly -This assembly contains the translation API that developers will use. - -#### `ITranslationProvider` class -Interface defining the API of a translation provider. The methods signatures imitate the API provided by `GettextResourceManager`. - -#### `GettextTranslationProvider` class -Basically a wrapper around a `GettextResourceManager`. - -#### `ResxTranslationProvider` class -Basically a wrapper around a `GettextResourceManager`, provided as a convenience class in case we need to also include satellite assemblies generated from **.resx** files. - -It doesn't support context and plurals (as this feature is only provided by gettext) but returns nicely either a normal translation of the string (i.e. singular and without context) or the string itself. It is preferable to throwing an exception, although it would be better to also log that behavior. - -#### `TranslationManager` class -Main entry point of the API, its implementation is hidden and a single instance (singleton) is available through the `TranslationManager.Instance` static property. - -Providers can be registered to it (this usually happens in the `Module` class of its localized assembly). When the user requests the translation of a string, the manager will select the correct provider based on the calling assembly name. - -The manager itself implements the `ITranslationProvider`. This is convenient and enables scenario where nested manager could be used (register a manager as a provider of another manager). - -##### `GetString(string text)` -| Parameter name | Description | -|----------------|-------------------------| -| text | The string to translate | - -Example: -```csharp -Console.WriteLine(TranslationManager.Instance.GetString("Hello World!")); -``` - -##### `GetPluralString(string text, string textPlural, int count)` -| Parameter name | Description | -|----------------|----------------------------------------------| -| text | The string to translate | -| pluralText | The plural version of the text to translate | -| count | An integer used to determine the plural form | - -Example: -```csharp -long count = 2; -Console.WriteLine(TranslationManager.Instance.GetPluralString("Hello World!", "Hello Worlds!", count)); -``` - -##### `GetParticularString(string context, string text)` -| Parameter name | Description | -|----------------|---------------------------------| -| context | The context for the translation | -| text | The string to translate | - -Example: -```csharp -Console.WriteLine(TranslationManager.Instance.GetParticularString("Messages", "Hello World!")); -``` - -##### `GetParticularPluralString(string context, string text, string textPlural, int count)` -| Parameter name | Description | -|----------------|----------------------------------------------| -| context | The context for the translation | -| text | The string to translate | -| pluralText | The plural version of the text to translate | -| count | An integer used to determine the plural form | - -Example: -```csharp -long count = 2; -Console.WriteLine(TranslationManager.Instance.GetParticularPluralString("Messages", "Hello World!", "Hello Worlds!", count)); -``` - -#### `TranslationAttribute` class -Sometimes, we need to localize certain C# constructs such as enum values, especially when they are displayed to the end-user. For that purpose, the `TranslationAttribute` can be used. - -Example: -```csharp -public enum Hoyle -{ - [Translation("Big")] - Big, - [Translation("Bang")] - Bang, -} -``` - -#### `Tr` helper class -Writing `TranslationManager.Instance.GetString()` for every call to the translation API is a bit long. For that reason convenient shortcuts are provided in the `Tr` helper class. The following table describes the relation between the shortcut method and the corresponding API: - -| `Tr` | `TranslationManager.Instance` | -|-------------------------------------|-----------------------------------------------------------| -| `_(text)` | `GetString(text)` | -| `_n(text, textPlural, count)` | `GetPluralString(text, textPlural, count)` | -| `_p(context, text)` | `GetParticularString(context, text)` | -| `_pn(context, text, textPlural, count)` | `GetParticularPluralString(context, text, textPlural, count)` | - -### `Stride.Core.Translation.Presentation` assembly - -This assembly enables the support of localization in **.xaml** files. - -#### `LocalizeExtension` class - -This markup extension has a double use. It is used by the extractor to detect localizable strings. At runtime it uses the localization API to provide the correct string depending on the language. - -Examples of use in XAML: -```xml - - - - - - - - - - - - - - -``` - -#### `LocalizeConverter` class -Base class for markup extensions/value converters that support some kind of localization. The base class retrieve the current local assembly where this markup extension/value converter is used. Inheriting classes can then pass this assembly as parameter to the `TranslationManager` methods to get the corresponding translation. Currently three converters inherits from this class: `EnumToTooltip`, `ContentReferenceToUrl` and `Translate`. The first two already existed and were adapted to support localization. - -#### `Translate` class -The `LocalizeExtension` described above can only localize static strings and can't be used in bindings. For that scenario the `Translate` markup extension/value converter can be used. It will dynamically query the translation manager with the bound value converted to a `string`. - -Note that for the localization to work, the bound value must match one of the localized string. - -### `Stride.Core.Translation.Extractor` standalone -The extractor is a standalone command line that can be used to retrieve all *localizable* strings from **.cs** and **.xaml** source file and generate a template **.pot** file. - -The usage of the command line is -``` -Stride.Core.Translation.Extractor[.exe] [options] [inputfile | filemask] ... -``` - - with the following options: -* `-D directory` or `--directory=directory`: Look for files in the given directory. This option can be added more than once. -* `-r` or `--recursive`: Look for files in sub-directories. -* `-x` or `--exclude=filemask`: Exclude a file or filemask from the search. -* `-d` or `--domain-name=name`: Output 'name.pot' instead of default 'messages.pot' -* `-b` or `--backup`: Create a backup file (.bak) in case the output file already exists -* `-o file` or `--output=file`: Write output to specified file (instead of 'name.pot or 'messages.pot'). -* `-m` or `--merge`: Attempt to merge extracted strings with an existing file. -* `-C` or `--preserve-comments`: Attempt to preserve comments on existing entries. -* `-v` or `--verbose`: More verbose message in the command prompt. -* `-h` or `--help`: Display usage and exit. - -For example to extract the strings for the `Stride.GameStudio` project, the command line is: - -``` -Stride.Core.Translation.Extractor -D ..\editor\Stride.GameStudio -d Stride.GameStudio -r -C -x *.Designer.cs *.xaml *.cs -``` - -It will look into all **.xaml** and **.cs** files in the whole project (*recursive* option) except the file matching the **\*.Designer.cs** pattern and output the extracted strings into `Stride.GameStudio.pot` (*domain-name* option). Existing comments will be preserved. - -Notes: -* Internally it uses the C#-port of the Gettext library, retrieved from the seemingly non-longer maintained [Gettext for .NET/Mono](https://sourceforge.net/projects/gettextnet/) (last update 2016-05-08). Note that the source code is provided under the LGPL v2 license so if we make modifications we need to publish it under the same license. Maybe we should fork it (and publish it on GitHub) to be on the safe side. -* For the moment I didn't have to do any modification as I rewrote the extractor tool, instead of using/modifying the one that cam with the code (GNU.Gettext.Xgettext), so using the compiled binaries of the `GNU.Gettext.dll` (use at design time and runtime) and `GNU.Getopt.dll` (used only at design time) is fine. - -#### `CSharpExtractor` class -This class parses **.cs** files and extracts the localizable strings by matching regular expressions with the methods from `ITranslationProvider` interface and `Tr` helper class. - -Note: using regular expressions is not perfect and ideally we should parse the **.cs** file properly (using Roslyn?) to make sure we don't get false positives. - -#### `XamlExtractor` class -This class parses **.xaml** files and extracts strings that are localized with the `{sskk:Localize}` extension. It uses a `XamlReader` to parse the nodes, which is more robust than regular expressions. - -#### `POExporter` class -This class exports the extracted strings in a template **.pot** file that can be then used by translators. It uses the capabilities from the `GNU.Gettext.Catalog` class to manage and save the **.pot** file. - -#### `ResxExporter` class -Not yet implemented. The idea is to be able to export the extracted strings in a regular **.resx** file, in case this format is to be used. - -## Documentation and references -https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization IStringLocalizer (Asp.Net Core) - -https://docs.microsoft.com/en-us/windows/uwp/globalizing/prepare-your-app-for-localization using resw files - -https://en.wikipedia.org/wiki/XLIFF standardized format for localization files. See https://github.com/Microsoft/XLIFF2-Object-Model for a possible implementation. - -### Tools and frameworks -Gettext references: https://www.gnu.org/software/gettext/manual/index.html, for C# https://www.gnu.org/software/gettext/manual/html_node/C_0023.html - -Alternative implementation of gettext for .Net (https://github.com/neris/NGettext) - -Another .Net implementation (https://sourceforge.net/projects/gettextnet/) - -Could be combined with https://github.com/pdfforge/translatable - -#### Tools -ResX editor: https://github.com/UweKeim/ZetaResourceEditor - -Poedit (for gettext .po files): https://poedit.net/. Source code here: https://github.com/vslavik/poedit/ - -Babylon.Net: http://www.redpin.eu/ - -Pootle: http://pootle.translatehouse.org/. Source code here: https://github.com/translate/pootle - -### Misc. -http://wp12674741.server-he.de/tiki/tiki-index.php - -http://www.tbs-apps.com/lsacreator/ - -https://crowdin.net/ crowd-source localization (nice community, but only for the texts, still need a tool to collect and a tool to build) - -https://weblate.org/en/ free web-based translation software. The company also offers hosting plan and support for a price, but self hosting is possible. - - diff --git a/docs/technical/media/poedit-edit-po-file.png b/docs/technical/media/poedit-edit-po-file.png deleted file mode 100644 index e0248b47ed..0000000000 --- a/docs/technical/media/poedit-edit-po-file.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:08bf4890d05f049064610915de66e25bb717965ad27b92eef96c8972d9cf9d86 -size 52882 diff --git a/docs/technical/media/poedit-open-pot-file.png b/docs/technical/media/poedit-open-pot-file.png deleted file mode 100644 index 407fef6d7e..0000000000 --- a/docs/technical/media/poedit-open-pot-file.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:624de8c115f2d5ed6cd72550c280dc96e874ace7f86847fa170cfeb26930e9ef -size 39017 From b6275f24d54033ba7e1530ee51b7ee309f64781d Mon Sep 17 00:00:00 2001 From: Virgile Bello Date: Fri, 5 Jan 2024 10:30:04 +0900 Subject: [PATCH 002/247] [VSPackage] For older version of Stride (up to 4.1), use a custom BinaryFormatter serializer and skip compression to be compatible with ServiceWire library used at the time. --- .../ServiceWireBinaryFormatterSerializer.cs | 61 +++++++++++++++++++ .../ServiceWireDoNothingCompressor.cs | 17 ++++++ .../Commands/StrideCommandsProxy.cs | 7 ++- 3 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireBinaryFormatterSerializer.cs create mode 100644 sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireDoNothingCompressor.cs diff --git a/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireBinaryFormatterSerializer.cs b/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireBinaryFormatterSerializer.cs new file mode 100644 index 0000000000..41eea944b7 --- /dev/null +++ b/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireBinaryFormatterSerializer.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.Serialization.Formatters.Binary; +using System.Runtime.Serialization; +using System.Text; +using System.Threading.Tasks; +using ServiceWire; + +namespace Stride.VisualStudio.Commands +{ + + internal class ServiceWireBinaryFormatterSerializer : ISerializer + { + private readonly IFormatter _formatter = new BinaryFormatter(); + + public byte[] Serialize(T obj) + { + if (null == obj) return null; + using (var ms = new MemoryStream()) + { + _formatter.Serialize(ms, obj); + return ms.ToArray(); + } + } + + public byte[] Serialize(object obj, string typeConfigName) + { + if (null == obj) return null; + using (var ms = new MemoryStream()) + { + var type = typeConfigName.ToType(); + var objT = Convert.ChangeType(obj, type); + _formatter.Serialize(ms, objT); + return ms.ToArray(); + } + } + + public T Deserialize(byte[] bytes) + { + if (null == bytes || bytes.Length == 0) return default(T); + using (var ms = new MemoryStream(bytes)) + { + return (T)_formatter.Deserialize(ms); + } + } + + public object Deserialize(byte[] bytes, string typeConfigName) + { + if (null == typeConfigName) throw new ArgumentNullException(nameof(typeConfigName)); + var type = typeConfigName.ToType(); + if (null == typeConfigName || null == bytes || bytes.Length == 0) return type.GetDefault(); + using (var ms = new MemoryStream(bytes)) + { + var obj = _formatter.Deserialize(ms); + return Convert.ChangeType(obj, type); + } + } + } +} diff --git a/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireDoNothingCompressor.cs b/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireDoNothingCompressor.cs new file mode 100644 index 0000000000..e6bddc99d8 --- /dev/null +++ b/sources/tools/Stride.VisualStudio.Package/Commands/ServiceWireDoNothingCompressor.cs @@ -0,0 +1,17 @@ +using ServiceWire; + +namespace Stride.VisualStudio.Commands +{ + internal class ServiceWireDoNothingCompressor : ICompressor + { + public byte[] Compress(byte[] data) + { + return data; + } + + public byte[] DeCompress(byte[] compressedBytes) + { + return compressedBytes; + } + } +} diff --git a/sources/tools/Stride.VisualStudio.Package/Commands/StrideCommandsProxy.cs b/sources/tools/Stride.VisualStudio.Package/Commands/StrideCommandsProxy.cs index d9bf12fb17..310190ddb6 100644 --- a/sources/tools/Stride.VisualStudio.Package/Commands/StrideCommandsProxy.cs +++ b/sources/tools/Stride.VisualStudio.Package/Commands/StrideCommandsProxy.cs @@ -114,7 +114,12 @@ public static IStrideCommands GetProxy() { try { - strideCommands = new NpClient(new NpEndPoint(address + "/IStrideCommands")); + strideCommands = stridePackageInfo.LoadedVersion >= new PackageVersion("4.2.0") + ? new NpClient(new NpEndPoint(address + "/IStrideCommands")) + // For Stride 4.1, we were using ServiceWire 5.3.4, which didn't have compressor and used BinaryFormatter serialization + // BinaryFormatter was removed from .NET 8.0, ServiceWire 5.5.4 and Stride 4.2, but we still need to be able to communicate with previous version of Stride + // Luckily, since VS still work with .NET 4.7, we can access BinaryFormatter and implement a custom ServiceWire.ISerializer for it. + : new NpClient(new NpEndPoint(address + "/IStrideCommands"), new ServiceWireBinaryFormatterSerializer(), new ServiceWireDoNothingCompressor()); break; } catch From 8f20f77323360e70cbfaf4b89f668d8736ab8e22 Mon Sep 17 00:00:00 2001 From: Eideren Date: Sun, 7 Jan 2024 13:41:39 +0100 Subject: [PATCH 003/247] [Physics] Fix inconsistent box2D collision, see #1707 and #2019 (#2092) --- sources/engine/Stride.Physics/Shapes/BoxColliderShape.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sources/engine/Stride.Physics/Shapes/BoxColliderShape.cs b/sources/engine/Stride.Physics/Shapes/BoxColliderShape.cs index a810191e20..10d705bd71 100644 --- a/sources/engine/Stride.Physics/Shapes/BoxColliderShape.cs +++ b/sources/engine/Stride.Physics/Shapes/BoxColliderShape.cs @@ -30,11 +30,11 @@ public BoxColliderShape(bool is2D, Vector3 size) if (is2D) size.Z = 0.001f; - // Note: Creating Convex 2D Shape from (3D) BoxShape, causes weird behaviour, - // better to instantiate Box2DShape directly (see issue #1707) if (Is2D) { - InternalShape = new BulletSharp.Box2DShape(size / 2) { LocalScaling = cachedScaling }; + // Note that encapsulating a 2D Box goes against bullet's 2D collision example. + // This was found through trial and error as the most stable solution, see issue #1707 and #2019 + InternalShape = new BulletSharp.Convex2DShape(new BulletSharp.Box2DShape(size / 2) { LocalScaling = Vector3.One }) { LocalScaling = cachedScaling }; } else { From 81f0f6cf9713fbd152e66837f9d73575f528cd3d Mon Sep 17 00:00:00 2001 From: Eideren Date: Sun, 7 Jan 2024 13:42:07 +0100 Subject: [PATCH 004/247] [Dispatcher] improve api, reduce overhead, improve performances for items > 1k (#2083) --- .../core/Stride.Core/Threading/Dispatcher.cs | 647 +++++++----------- .../Threading/ThreadPool.SemaphoreW.cs | 8 +- .../core/Stride.Core/Threading/ThreadPool.cs | 132 +++- .../Engine/Processors/TransformProcessor.cs | 17 +- .../Materials/MaterialRenderFeature.cs | 42 +- .../Rendering/SkinningRenderFeature.cs | 34 +- .../Rendering/TransformRenderFeature.cs | 72 +- 7 files changed, 461 insertions(+), 491 deletions(-) diff --git a/sources/core/Stride.Core/Threading/Dispatcher.cs b/sources/core/Stride.Core/Threading/Dispatcher.cs index fc61b4683c..bdc4a1a2b0 100644 --- a/sources/core/Stride.Core/Threading/Dispatcher.cs +++ b/sources/core/Stride.Core/Threading/Dispatcher.cs @@ -16,7 +16,7 @@ namespace Stride.Core.Threading { - public class Dispatcher + public static class Dispatcher { #if STRIDE_PLATFORM_IOS || STRIDE_PLATFORM_ANDROID public static int MaxDegreeOfParallelism = 1; @@ -25,497 +25,319 @@ public class Dispatcher #endif private static readonly ProfilingKey DispatcherSortKey = new ProfilingKey("Dispatcher.Sort"); + private static readonly ProfilingKey DispatcherBatched = new ProfilingKey("Dispatcher.Batched"); public delegate void ValueAction(ref T obj); - public static void For(int fromInclusive, int toExclusive, [Pooled] Action action) - { - using (Profile(action)) + /// + /// The call producing the least amount of overhead, other methods are built on top of this one. + /// + /// + /// The amount of items to process, + /// this total will be split into multiple batches, + /// each batch runs . with the range of items for that batch + /// + /// + /// An object shared across all threads running this job, if TJob is a struct each threads will work off of a unique copy of it + /// + /// If any of the threads executing this job threw an exception, it will be re-thrown in the caller's scope + public static unsafe void ForBatched(int items, TJob batchJob) where TJob : IBatchJob + { + using var _ = Profiler.Begin(DispatcherBatched); + + // This scope's JIT performance is VERY fragile, be careful when tweaking it + + if (items == 0) + return; + + if (MaxDegreeOfParallelism <= 1 || items == 1) { - if (fromInclusive > toExclusive) - { - var temp = fromInclusive; - fromInclusive = toExclusive + 1; - toExclusive = temp + 1; - } + batchJob.Process(0, items); + return; + } - var count = toExclusive - fromInclusive; - if (count == 0) - return; + int batchCount = Math.Min(MaxDegreeOfParallelism, items); + uint itemsPerBatch = (uint)((items + (batchCount - 1)) / batchCount); - if (MaxDegreeOfParallelism <= 1 || count == 1) - { - ExecuteBatch(fromInclusive, toExclusive, action); - } - else - { - var state = BatchState.Acquire(); - state.WorkDone = state.StartInclusive = fromInclusive; + // Performs 1/8 to 1/4 better in most cases, performs up to 1/8 worse when the ratio between + // the duration each individual item takes and the amount of items per batch hits a very narrow sweet-spot. + // Not entirely sure why yet. +#if FALSE + if (items / MaxDegreeOfParallelism > 8) + itemsPerBatch /= 4; // Batches of 2 instead of 8 to allow faster threads to steal more of the work + else if (items / MaxDegreeOfParallelism > 4) + itemsPerBatch /= 2; // Batches of 2 instead of 4 to allow faster threads to steal more of the work +#endif - try - { - var batchCount = Math.Min(MaxDegreeOfParallelism, count); - var batchSize = (count + (batchCount - 1)) / batchCount; + var batch = BatchState.Borrow(itemsPerBatch: itemsPerBatch, endExclusive: (uint)items, references: batchCount, batchJob); + try + { + ThreadPool.Instance.QueueUnsafeWorkItem(batch, &TypeAdapter, batchCount - 1); - // Kick off a worker, then perform work synchronously - state.AddReference(); - Fork(toExclusive, batchSize, MaxDegreeOfParallelism, action, state); + ProcessBatch(batchJob, batch); - // Wait for all workers to finish - state.WaitCompletion(toExclusive); + // Might as well steal some work instead of just waiting, + // also helps prevent potential deadlocks from badly threaded code + while (Volatile.Read(ref batch.ItemsDone) < batch.Total && batch.Finished.WaitOne(0) == false) + ThreadPool.Instance.TryCooperate(); - var ex = Interlocked.Exchange(ref state.ExceptionThrown, null); - if (ex != null) - throw ex; - } - finally - { - state.Release(); - } - } + var ex = Interlocked.Exchange(ref batch.ExceptionThrown, null); + if (ex != null) + throw ex; + } + finally + { + batch.Release(); } } - public static void For(int fromInclusive, int toExclusive, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal = null) + private static void TypeAdapter(object obj) where TJob : IBatchJob { - using (Profile(action)) + var batch = obj as BatchState; // 'as' and assert instead of direct cast to improve performance + Debug.Assert(batch is not null); + try { - if (fromInclusive > toExclusive) - { - var temp = fromInclusive; - fromInclusive = toExclusive + 1; - toExclusive = temp + 1; - } - - var count = toExclusive - fromInclusive; - if (count == 0) - return; + ProcessBatch(batch.Job, batch); + } + finally + { + batch.Release(); + } + } - if (MaxDegreeOfParallelism <= 1 || count == 1) - { - ExecuteBatch(fromInclusive, toExclusive, initializeLocal, action, finalizeLocal); - } - else + private static void ProcessBatch(TJob job, [NotNull] BatchState state) where TJob : IBatchJob + { + try + { + for (uint start; (start = Interlocked.Add(ref state.Index, state.ItemsPerBatch) - state.ItemsPerBatch) < state.Total;) { - var state = BatchState.Acquire(); - state.WorkDone = state.StartInclusive = fromInclusive; - - try - { - var batchCount = Math.Min(MaxDegreeOfParallelism, count); - var batchSize = (count + (batchCount - 1)) / batchCount; - - // Kick off a worker, then perform work synchronously - state.AddReference(); - Fork(toExclusive, batchSize, MaxDegreeOfParallelism, initializeLocal, action, finalizeLocal, state); + uint end = Math.Min(start + state.ItemsPerBatch, state.Total); - // Wait for all workers to finish - state.WaitCompletion(toExclusive); + job.Process((int)start, (int)end); - var ex = Interlocked.Exchange(ref state.ExceptionThrown, null); - if (ex != null) - throw ex; - } - finally + if (Interlocked.Add(ref state.ItemsDone, state.ItemsPerBatch) >= state.Total) { - state.Release(); + state.Finished.Set(); + break; } } } - } - - public static void ForEach([NotNull] IReadOnlyList collection, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal = null) - { - For(0, collection.Count, initializeLocal, (i, local) => action(collection[i], local), finalizeLocal); + catch (Exception e) + { + Interlocked.Exchange(ref state.ExceptionThrown, e); + throw; + } } - public static void ForEach([NotNull] IReadOnlyList collection, [Pooled] Action action) + public static unsafe void ForBatched(int items, in T parameter, delegate* executeBatch) { - For(0, collection.Count, i => action(collection[i])); + var batchedDelegate = new BatchedDelegate + { + Param = parameter, + Delegate = executeBatch, + }; + ForBatched(items, batchedDelegate); } - public static void ForEach([NotNull] List collection, [Pooled] Action action) + public static unsafe void ForBatched(int items, ref T parameter, delegate* executeBatch) { - For(0, collection.Count, i => action(collection[i])); + var batchedDelegate = new BatchedDelegateRef + { + Param = parameter, + Delegate = executeBatch, + }; + ForBatched(items, batchedDelegate); } - public static void ForEach([NotNull] Dictionary collection, [Pooled] Action> action) + public static unsafe void ForBatched(int items, [Pooled] Action executeBatch) { - if (MaxDegreeOfParallelism <= 1 || collection.Count <= 1) - { - ExecuteBatch(collection, 0, collection.Count, action); - } - else + var batchedDelegate = new BatchedDelegate> { - var state = BatchState.Acquire(); + Param = executeBatch, + Delegate = &ForBatchedAction, + }; + ForBatched(items, batchedDelegate); - try - { - var batchCount = Math.Min(MaxDegreeOfParallelism, collection.Count); - var batchSize = (collection.Count + (batchCount - 1)) / batchCount; - - // Kick off a worker, then perform work synchronously - state.AddReference(); - Fork(collection, batchSize, MaxDegreeOfParallelism, action, state); + static void ForBatchedAction(Action parameter, int from, int toExclusive) + { + parameter(from, toExclusive); + } + } - // Wait for all workers to finish - state.WaitCompletion(collection.Count); + public static unsafe void For(int fromInclusive, int toExclusive, [Pooled] Action action) + { + var parameters = (action, fromInclusive); + ForBatched(toExclusive - fromInclusive, parameters, &ForWrapped); - var ex = Interlocked.Exchange(ref state.ExceptionThrown, null); - if (ex != null) - throw ex; - } - finally + static void ForWrapped((Action action, int start) parameters, int from, int toExclusive) + { + for (int i = from; i < toExclusive; i++) { - state.Release(); + parameters.action(parameters.start + i); } } } - public static void ForEach([NotNull] Dictionary collection, [Pooled] Func initializeLocal, [Pooled] Action, TLocal> action, [Pooled] Action finalizeLocal = null) + public static unsafe void For(int fromInclusive, int toExclusive, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal = null) { - if (MaxDegreeOfParallelism <= 1 || collection.Count <= 1) - { - ExecuteBatch(collection, 0, collection.Count, initializeLocal, action, finalizeLocal); - } - else - { - var state = BatchState.Acquire(); + var parameters = (initializeLocal, action, finalizeLocal, fromInclusive); + ForBatched(toExclusive - fromInclusive, parameters, &ForWrapped); + static void ForWrapped((Func initializeLocal, Action action, Action finalizeLocal, int start) parameters, int from, int toExclusive) + { + TLocal local = default(TLocal); try { - var batchCount = Math.Min(MaxDegreeOfParallelism, collection.Count); - var batchSize = (collection.Count + (batchCount - 1)) / batchCount; - - // Kick off a worker, then perform work synchronously - state.AddReference(); - Fork(collection, batchSize, MaxDegreeOfParallelism, initializeLocal, action, finalizeLocal, state); - - // Wait for all workers to finish - state.WaitCompletion(collection.Count); + if (parameters.initializeLocal != null) + { + local = parameters.initializeLocal.Invoke(); + } - var ex = Interlocked.Exchange(ref state.ExceptionThrown, null); - if (ex != null) - throw ex; + for (int i = from; i < toExclusive; i++) + { + parameters.action(parameters.start + i, local); + } } finally { - state.Release(); + parameters.finalizeLocal?.Invoke(local); } } } - public static void ForEach([NotNull] FastCollection collection, [Pooled] Action action) - { - For(0, collection.Count, i => action(collection[i])); - } - - public static void ForEach([NotNull] FastList collection, [Pooled] Action action) + public static void ForEach([NotNull] T[] collection, [Pooled] Action action) { - For(0, collection.Count, i => action(collection.Items[i])); + ForEach(collection, action); } public static void ForEach([NotNull] ConcurrentCollector collection, [Pooled] Action action) { - For(0, collection.Count, i => action(collection.Items[i])); + ForEach>(collection, action); } - public static void ForEach([NotNull] FastList collection, [Pooled] ValueAction action) + public static void ForEach([NotNull] List collection, [Pooled] Action action) { - For(0, collection.Count, i => action(ref collection.Items[i])); + ForEach>(collection, action); } - public static void ForEach([NotNull] ConcurrentCollector collection, [Pooled] ValueAction action) + public static void ForEach([NotNull] ConcurrentCollector collection, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal = null) { - For(0, collection.Count, i => action(ref collection.Items[i])); + ForEach>(collection, initializeLocal, action, finalizeLocal); } - private static void Fork([NotNull] Dictionary collection, int batchSize, int maxDegreeOfParallelism, [Pooled] Action> action, [NotNull] BatchState state) + public static void ForEach([NotNull] FastCollection collection, [Pooled] Action action) { - // Other threads already processed all work before this one started. - if (state.StartInclusive >= collection.Count) - { - state.Release(); - return; - } - - // Kick off another worker if there's any work left - if (maxDegreeOfParallelism > 1 && state.StartInclusive + batchSize < collection.Count) - { - int workToSchedule = maxDegreeOfParallelism - 1; - for (int i = 0; i < workToSchedule; i++) - { - state.AddReference(); - } - ThreadPool.Instance.QueueWorkItem(() => Fork(collection, batchSize, 0, action, state), workToSchedule); - } - - try - { - // Process batches synchronously as long as there are any - int newStart; - while ((newStart = Interlocked.Add(ref state.StartInclusive, batchSize)) - batchSize < collection.Count) - { - try - { - // TODO: Reuse enumerator when processing multiple batches synchronously - var start = newStart - batchSize; - ExecuteBatch(collection, newStart - batchSize, Math.Min(collection.Count, newStart) - start, action); - } - finally - { - if (Interlocked.Add(ref state.WorkDone, batchSize) >= collection.Count) - { - // Don't wait for other threads to wake up and signal the BatchState, release as soon as work is finished - state.Finished.Set(); - } - } - } - } - catch (Exception e) - { - Interlocked.Exchange(ref state.ExceptionThrown, e); - throw; - } - finally - { - state.Release(); - } + ForEach>(collection, action); } - private static void Fork([NotNull] Dictionary collection, int batchSize, int maxDegreeOfParallelism, [Pooled] Func initializeLocal, [Pooled] Action, TLocal> action, [Pooled] Action finalizeLocal, [NotNull] BatchState state) + public static unsafe void ForEach([NotNull] ConcurrentCollector collection, [Pooled] ValueAction action) { - // Other threads already processed all work before this one started. - if (state.StartInclusive >= collection.Count) - { - state.Release(); - return; - } + var parameters = (action, collection); + ForBatched(collection.Count, parameters, &ForEachList); - // Kick off another worker if there's any work left - if (maxDegreeOfParallelism > 1 && state.StartInclusive + batchSize < collection.Count) + static void ForEachList((ValueAction action, ConcurrentCollector collection) parameters, int from, int toExclusive) { - int workToSchedule = maxDegreeOfParallelism - 1; - for (int i = 0; i < workToSchedule; i++) + for (int i = from; i < toExclusive; i++) { - state.AddReference(); + parameters.action(ref parameters.collection.Items[i]); } - ThreadPool.Instance.QueueWorkItem(() => Fork(collection, batchSize, 0, initializeLocal, action, finalizeLocal, state), workToSchedule); - } - - try - { - // Process batches synchronously as long as there are any - int newStart; - while ((newStart = Interlocked.Add(ref state.StartInclusive, batchSize)) - batchSize < collection.Count) - { - try - { - // TODO: Reuse enumerator when processing multiple batches synchronously - var start = newStart - batchSize; - ExecuteBatch(collection, newStart - batchSize, Math.Min(collection.Count, newStart) - start, initializeLocal, action, finalizeLocal); - } - finally - { - if (Interlocked.Add(ref state.WorkDone, batchSize) >= collection.Count) - { - // Don't wait for other threads to wake up and signal the BatchState, release as soon as work is finished - state.Finished.Set(); - } - } - } - } - catch (Exception e) - { - Interlocked.Exchange(ref state.ExceptionThrown, e); - throw; - } - finally - { - state.Release(); } } - private static void ExecuteBatch(int fromInclusive, int toExclusive, [Pooled] Action action) + public static unsafe void ForEach([NotNull] TList collection, [Pooled] Action action) where TList : IReadOnlyList { - for (var i = fromInclusive; i < toExclusive; i++) - { - action(i); - } - } + var parameters = (action, collection); + ForBatched(collection.Count, parameters, &ForEachList); - private static void ExecuteBatch(int fromInclusive, int toExclusive, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal) - { - var local = default(TLocal); - try + static void ForEachList((Action action, TList collection) parameters, int from, int toExclusive) { - if (initializeLocal != null) - { - local = initializeLocal(); - } - - for (var i = fromInclusive; i < toExclusive; i++) + for (int i = from; i < toExclusive; i++) { - action(i, local); + parameters.action(parameters.collection[i]); } } - finally - { - finalizeLocal?.Invoke(local); - } } - private static void Fork(int endExclusive, int batchSize, int maxDegreeOfParallelism, [Pooled] Action action, [NotNull] BatchState state) + public static unsafe void ForEach([NotNull] TList collection, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal = null) where TList : IReadOnlyList { - // Other threads already processed all work before this one started. - if (state.StartInclusive >= endExclusive) - { - state.Release(); - return; - } + var parameters = (initializeLocal, action, finalizeLocal, collection); + ForBatched(collection.Count, parameters, &ForEachList); - // Kick off another worker if there's any work left - if (maxDegreeOfParallelism > 1 && state.StartInclusive + batchSize < endExclusive) + static void ForEachList((Func initializeLocal, Action action, Action finalizeLocal, TList collection) parameters, int from, int toExclusive) { - int workToSchedule = maxDegreeOfParallelism - 1; - for (int i = 0; i < workToSchedule; i++) - { - state.AddReference(); - } - ThreadPool.Instance.QueueWorkItem(() => Fork(endExclusive, batchSize, 0, action, state), workToSchedule); - } - - try - { - // Process batches synchronously as long as there are any - int newStart; - while ((newStart = Interlocked.Add(ref state.StartInclusive, batchSize)) - batchSize < endExclusive) + TLocal local = default(TLocal); + try { - try + if (parameters.initializeLocal != null) { - ExecuteBatch(newStart - batchSize, Math.Min(endExclusive, newStart), action); + local = parameters.initializeLocal.Invoke(); } - finally + + for (int i = from; i < toExclusive; i++) { - if (Interlocked.Add(ref state.WorkDone, batchSize) >= endExclusive) - { - // Don't wait for other threads to wake up and signal the BatchState, release as soon as work is finished - state.Finished.Set(); - } + parameters.action(parameters.collection[i], local); } } - } - catch (Exception e) - { - Interlocked.Exchange(ref state.ExceptionThrown, e); - throw; - } - finally - { - state.Release(); + finally + { + parameters.finalizeLocal?.Invoke(local); + } } } - private static void Fork(int endExclusive, int batchSize, int maxDegreeOfParallelism, [Pooled] Func initializeLocal, [Pooled] Action action, [Pooled] Action finalizeLocal, [NotNull] BatchState state) + public static unsafe void ForEach([NotNull] Dictionary collection, [Pooled] Action> action) { - // Other threads already processed all work before this one started. - if (state.StartInclusive >= endExclusive) - { - state.Release(); - return; - } + var parameters = (action, collection); + ForBatched(collection.Count, parameters, &ForEachDict); - // Kick off another worker if there's any work left - if (maxDegreeOfParallelism > 1 && state.StartInclusive + batchSize < endExclusive) + static void ForEachDict((Action> action, Dictionary collection) parameters, int from, int toExclusive) { - int workToSchedule = maxDegreeOfParallelism - 1; - for (int i = 0; i < workToSchedule; i++) + using var enumerator = parameters.collection.GetEnumerator(); + + // Skip to offset + for (int i = 0; i < from; i++) { - state.AddReference(); + enumerator.MoveNext(); } - ThreadPool.Instance.QueueWorkItem(() => Fork(endExclusive, batchSize, 0, initializeLocal, action, finalizeLocal, state), workToSchedule); - } - try - { - // Process batches synchronously as long as there are any - int newStart; - while ((newStart = Interlocked.Add(ref state.StartInclusive, batchSize)) - batchSize < endExclusive) + // Process batch + for (int i = from; i < toExclusive && enumerator.MoveNext(); i++) { - try - { - ExecuteBatch(newStart - batchSize, Math.Min(endExclusive, newStart), initializeLocal, action, finalizeLocal); - } - finally - { - if (Interlocked.Add(ref state.WorkDone, batchSize) >= endExclusive) - { - // Don't wait for other threads to wake up and signal the BatchState, release as soon as work is finished - state.Finished.Set(); - } - } + parameters.action(enumerator.Current); } } - catch (Exception e) - { - Interlocked.Exchange(ref state.ExceptionThrown, e); - throw; - } - finally - { - state.Release(); - } } - private static void ExecuteBatch([NotNull] Dictionary dictionary, int offset, int count, [Pooled] Action> action) + public static unsafe void ForEach([NotNull] Dictionary collection, [Pooled] Func initializeLocal, [Pooled] Action, TLocal> action, [Pooled] Action finalizeLocal = null) { - var enumerator = dictionary.GetEnumerator(); - var index = 0; + var parameters = (initializeLocal, action, finalizeLocal, collection); + ForBatched(collection.Count, parameters, &ForEachDict); - // Skip to offset - while (index < offset && enumerator.MoveNext()) + static void ForEachDict((Func initializeLocal, Action, TLocal> action, Action finalizeLocal, Dictionary collection) parameters, int from, int toExclusive) { - index++; - } + using var enumerator = parameters.collection.GetEnumerator(); - // Process batch - while (index < offset + count && enumerator.MoveNext()) - { - action(enumerator.Current); - index++; - } - } - - private static void ExecuteBatch([NotNull] Dictionary dictionary, int offset, int count, [Pooled] Func initializeLocal, [Pooled] Action, TLocal> action, [Pooled] Action finalizeLocal) - { - var local = default(TLocal); - try - { - if (initializeLocal != null) + for (int i = 0; i < from; i++) // Skip to the start of our batch { - local = initializeLocal(); + enumerator.MoveNext(); } - var enumerator = dictionary.GetEnumerator(); - var index = 0; - - // Skip to offset - while (index < offset && enumerator.MoveNext()) + TLocal local = default; + try { - index++; - } + if (parameters.initializeLocal != null) + local = parameters.initializeLocal.Invoke(); - // Process batch - while (index < offset + count && enumerator.MoveNext()) + for (int i = from; i < toExclusive && enumerator.MoveNext(); i++) + { + parameters.action(enumerator.Current, local); + } + } + finally { - action(enumerator.Current, local); - index++; + parameters.finalizeLocal?.Invoke(local); } } - finally - { - finalizeLocal?.Invoke(local); - } } public static void Sort([NotNull] ConcurrentCollector collection, IComparer comparer) @@ -665,51 +487,84 @@ private static void Swap([NotNull] T[] collection, int i, int j) collection[j] = temp; } - private class BatchState + /// + /// An implementation of a job running in batches. + /// Implementing this as a struct improves performance as the JIT would have an easier time inlining the call. + /// Implementing this as a class would provide more utility as this object would be shared across all threads, + /// allowing for interlocked operations and other communication between threads. + /// + public interface IBatchJob + { + /// + /// Execute this job over a range of items + /// + /// the start of the range + /// the end of the range, iterate as long as i < endExclusive + void Process(int start, int endExclusive); + } + + private sealed class BatchState where TJob : IBatchJob { - private static readonly ConcurrentPool Pool = new ConcurrentPool(() => new BatchState()); + private static readonly ConcurrentStack> Pool = new(); private int referenceCount; - public readonly ManualResetEvent Finished = new ManualResetEvent(false); + public readonly ManualResetEvent Finished = new(false); - public int StartInclusive; + public uint Index, Total, ItemsPerBatch, ItemsDone; - public int WorkDone; + public TJob Job; public Exception ExceptionThrown; [NotNull] - public static BatchState Acquire() + public static BatchState Borrow(uint itemsPerBatch, uint endExclusive, int references, TJob job) { - var state = Pool.Acquire(); - state.referenceCount = 1; - state.StartInclusive = 0; - state.WorkDone = 0; + if (Pool.TryPop(out var state) == false) + state = new(); + + state.Index = 0; + state.Total = endExclusive; + state.ItemsPerBatch = itemsPerBatch; + state.ItemsDone = 0; state.ExceptionThrown = null; - state.Finished.Reset(); + state.referenceCount = references; + state.Job = job; return state; } - public void AddReference() - { - Interlocked.Increment(ref referenceCount); - } - public void Release() { - if (Interlocked.Decrement(ref referenceCount) == 0) + var refCount = Interlocked.Decrement(ref referenceCount); + if (refCount == 0) { - Pool.Release(this); + Job = default; // Clear any references it may hold onto + Finished.Reset(); + Pool.Push(this); } + Debug.Assert(refCount >= 0); } - - public void WaitCompletion(int end) + } + + struct BatchedDelegateRef : IBatchJob + { + public T Param; + public unsafe delegate* Delegate; + + public unsafe void Process(int start, int endExclusive) { - // Might as well steal some work instead of just waiting, - // also helps prevent potential deadlocks from badly threaded code - while(WorkDone < end && Finished.WaitOne(0) == false) - ThreadPool.Instance.TryCooperate(); + Delegate(ref Param, start, endExclusive); + } + } + + struct BatchedDelegate : IBatchJob + { + public T Param; + public unsafe delegate* Delegate; + + public unsafe void Process(int start, int endExclusive) + { + Delegate(Param, start, endExclusive); } } diff --git a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs index 2ee9908462..93bf23f472 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.SemaphoreW.cs @@ -15,7 +15,7 @@ public sealed partial class ThreadPool /// /// Mostly lifted from dotnet's LowLevelLifoSemaphore /// - private class SemaphoreW + private class SemaphoreW : ISemaphore { private const int SpinSleep0Threshold = 10; @@ -60,7 +60,9 @@ public SemaphoreW(int spinCountParam) public void Wait(int timeout = -1) => internals.Wait(spinCount, lifoSemaphore, timeout); - public void Release(int releaseCount) => internals.Release(releaseCount, lifoSemaphore); + public void Release(int count) => internals.Release(count, lifoSemaphore); + + public void Dispose() => lifoSemaphore?.Dispose(); [StructLayout(LayoutKind.Explicit)] private struct Counts @@ -367,4 +369,4 @@ private struct PaddingFalseSharing #endif } } -} \ No newline at end of file +} diff --git a/sources/core/Stride.Core/Threading/ThreadPool.cs b/sources/core/Stride.Core/Threading/ThreadPool.cs index 3951ea39a6..8424455f84 100644 --- a/sources/core/Stride.Core/Threading/ThreadPool.cs +++ b/sources/core/Stride.Core/Threading/ThreadPool.cs @@ -5,6 +5,8 @@ using Stride.Core.Diagnostics; using System; using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; namespace Stride.Core.Threading @@ -15,6 +17,8 @@ namespace Stride.Core.Threading /// public sealed partial class ThreadPool : IDisposable { + private static readonly Logger Logger = GlobalLogger.GetLogger(nameof(ThreadPool)); + /// /// The default instance that the whole process shares, use this one to avoid wasting process memory. /// @@ -28,9 +32,9 @@ public sealed partial class ThreadPool : IDisposable private static readonly ProfilingKey ProcessWorkItemKey = new ProfilingKey($"{nameof(ThreadPool)}.ProcessWorkItem"); - private readonly ConcurrentQueue workItems = new ConcurrentQueue(); - private readonly SemaphoreW semaphore; - + private readonly ConcurrentQueue workItems = new ConcurrentQueue(); + private readonly ISemaphore semaphore; + private long completionCounter; private int workScheduled, threadsBusy; private int disposing; @@ -47,8 +51,30 @@ public sealed partial class ThreadPool : IDisposable public ThreadPool(int? threadCount = null) { - semaphore = new SemaphoreW(spinCountParam:70); - + int spinCount = 70; + + if(RuntimeInformation.ProcessArchitecture is Architecture.Arm or Architecture.Arm64) + { + // Dotnet: + // On systems with ARM processors, more spin-waiting seems to be necessary to avoid perf regressions from incurring + // the full wait when work becomes available soon enough. This is more noticeable after reducing the number of + // thread requests made to the thread pool because otherwise the extra thread requests cause threads to do more + // busy-waiting instead and adding to contention in trying to look for work items, which is less preferable. + spinCount *= 4; + } + try + { + semaphore = new DotnetLifoSemaphore(spinCount); + } + catch(Exception e) + { + // For net6+ this should not happen, logging instead of throwing as this is just a performance regression + if(Environment.Version.Major >= 6) + Logger.Warning($"Could not bind to dotnet's Lifo Semaphore, falling back to suboptimal semaphore:\n{e}"); + + semaphore = new SemaphoreW(spinCountParam:70); + } + WorkerThreadsCount = threadCount ?? (Environment.ProcessorCount == 1 ? 1 : Environment.ProcessorCount - 1); leftToDispose = WorkerThreadsCount; for (int i = 0; i < WorkerThreadsCount; i++) @@ -66,7 +92,7 @@ static ThreadPool() /// Queue an action to run on one of the available threads, /// it is strongly recommended that the action takes less than a millisecond. /// - public void QueueWorkItem([NotNull, Pooled] Action workItem, int amount = 1) + public unsafe void QueueWorkItem([NotNull, Pooled] Action workItem, int amount = 1) { // Throw right here to help debugging if (workItem == null) @@ -85,10 +111,55 @@ public void QueueWorkItem([NotNull, Pooled] Action workItem, int amount = 1) } Interlocked.Add(ref workScheduled, amount); + var work = new Work { WorkHandler = &ActionHandler, Data = workItem }; for (int i = 0; i < amount; i++) { PooledDelegateHelper.AddReference(workItem); - workItems.Enqueue(workItem); + workItems.Enqueue(work); + } + semaphore.Release(amount); + } + + static void ActionHandler(object param) + { + Action action = (Action)param; + try + { + action(); + } + finally + { + PooledDelegateHelper.Release(action); + } + } + + /// + /// Queue some work item to run on one of the available threads, + /// it is strongly recommended that the action takes less than a millisecond. + /// Additionally, the parameter provided must be fixed from this call onward until the action has finished executing + /// + public unsafe void QueueUnsafeWorkItem(object parameter, delegate* obj, int amount = 1) + { + if (parameter == null) + { + throw new NullReferenceException(nameof(parameter)); + } + + if (amount < 1) + { + throw new ArgumentOutOfRangeException(nameof(amount)); + } + + if (disposing > 0) + { + throw new ObjectDisposedException(ToString()); + } + + Interlocked.Add(ref workScheduled, amount); + var work = new Work { WorkHandler = obj, Data = parameter }; + for (int i = 0; i < amount; i++) + { + workItems.Enqueue(work); } semaphore.Release(amount); } @@ -98,7 +169,7 @@ public void QueueWorkItem([NotNull, Pooled] Action workItem, int amount = 1) /// If you absolutely have to block inside one of the threadpool's thread for whatever /// reason do a busy loop over this function. /// - public bool TryCooperate() + public unsafe bool TryCooperate() { if (workItems.TryDequeue(out var workItem)) { @@ -106,12 +177,11 @@ public bool TryCooperate() Interlocked.Decrement(ref workScheduled); try { - using var _ = Profiler.Begin(ProcessWorkItemKey); - workItem.Invoke(); + using (Profiler.Begin(ProcessWorkItemKey)) + workItem.WorkHandler(workItem.Data); } finally { - PooledDelegateHelper.Release(workItem); Interlocked.Decrement(ref threadsBusy); Interlocked.Increment(ref completionCounter); } @@ -172,14 +242,11 @@ public void Dispose() { return; } - + semaphore.Release(WorkerThreadsCount); + semaphore.Dispose(); while (Volatile.Read(ref leftToDispose) != 0) { - if (semaphore.SignalCount == 0) - { - semaphore.Release(1); - } Thread.Yield(); } @@ -189,5 +256,38 @@ public void Dispose() } } + + unsafe struct Work + { + public object Data; + public delegate* WorkHandler; + } + + private interface ISemaphore : IDisposable + { + public void Release(int count); + public void Wait(int timeout = -1); + } + + private sealed class DotnetLifoSemaphore : ISemaphore + { + private readonly IDisposable semaphore; + private readonly Func wait; + private readonly Action release; + + public DotnetLifoSemaphore(int spinCount) + { + // The semaphore Dotnet uses for its own threadpool is more efficient than what's publicly available, + // but sadly it is internal - we'll hijack it through reflection + Type lifoType = Type.GetType("System.Threading.LowLevelLifoSemaphore"); + semaphore = Activator.CreateInstance(lifoType, new object[]{ 0, short.MaxValue, spinCount, new Action( () => {} ) }) as IDisposable; + wait = lifoType.GetMethod("Wait", BindingFlags.Instance | BindingFlags.Public).CreateDelegate>(semaphore); + release = lifoType.GetMethod("Release", BindingFlags.Instance | BindingFlags.Public).CreateDelegate>(semaphore); + } + + public void Dispose() => semaphore.Dispose(); + public void Release(int count) => release(count); + public void Wait(int timeout = -1) => wait(timeout, true); + } } } diff --git a/sources/engine/Stride.Engine/Engine/Processors/TransformProcessor.cs b/sources/engine/Stride.Engine/Engine/Processors/TransformProcessor.cs index fab91cf612..3ce996a3ec 100644 --- a/sources/engine/Stride.Engine/Engine/Processors/TransformProcessor.cs +++ b/sources/engine/Stride.Engine/Engine/Processors/TransformProcessor.cs @@ -93,9 +93,9 @@ protected override void OnEntityComponentRemoved(Entity entity, TransformCompone } } - internal void UpdateTransformations(FastCollection transformationComponents) + internal unsafe void UpdateTransformations(FastCollection transformationComponents) { - Dispatcher.ForEach(transformationComponents, UpdateTransformationsRecursive); + Dispatcher.ForBatched(transformationComponents.Count, transformationComponents, &UpdateTransformationsRecursive); // Re-update model node links to avoid one frame delay compared reference model (ideally entity should be sorted to avoid this in future). if (ModelNodeLinkProcessor != null) @@ -105,17 +105,18 @@ internal void UpdateTransformations(FastCollection transform { modelNodeLinkComponents.Add(modelNodeLink.Entity.Transform); } - Dispatcher.ForEach(modelNodeLinkComponents, UpdateTransformationsRecursive); + Dispatcher.ForBatched(modelNodeLinkComponents.Count, modelNodeLinkComponents, &UpdateTransformationsRecursive); } } - private static void UpdateTransformationsRecursive(TransformComponent transform) + private static void UpdateTransformationsRecursive(FastCollection transforms, int from, int toExclusive) { - transform.UpdateLocalMatrix(); - transform.UpdateWorldMatrixInternal(false); - foreach (var child in transform.Children) + for (int i = from; i < toExclusive; i++) { - UpdateTransformationsRecursive(child); + var transform = transforms[i]; + transform.UpdateLocalMatrix(); + transform.UpdateWorldMatrixInternal(false); + UpdateTransformationsRecursive(transform.Children, 0, transform.Children.Count); } } diff --git a/sources/engine/Stride.Rendering/Rendering/Materials/MaterialRenderFeature.cs b/sources/engine/Stride.Rendering/Rendering/Materials/MaterialRenderFeature.cs index 89e4da1de0..533b2c1c39 100644 --- a/sources/engine/Stride.Rendering/Rendering/Materials/MaterialRenderFeature.cs +++ b/sources/engine/Stride.Rendering/Rendering/Materials/MaterialRenderFeature.cs @@ -280,31 +280,35 @@ public override void Prepare(RenderDrawContext context) // Assign descriptor sets to each render node var resourceGroupPool = ((RootEffectRenderFeature)RootRenderFeature).ResourceGroupPool; - Dispatcher.For(0, RootRenderFeature.RenderNodes.Count, () => context.RenderContext.GetThreadContext(), (renderNodeIndex, threadContext) => + Dispatcher.ForBatched(RootRenderFeature.RenderNodes.Count, (from, toExclusive) => { - var renderNodeReference = new RenderNodeReference(renderNodeIndex); - var renderNode = RootRenderFeature.RenderNodes[renderNodeIndex]; - var renderMesh = (RenderMesh)renderNode.RenderObject; + var threadContext = context.RenderContext.GetThreadContext(); + for (int i = from; i < toExclusive; i++) + { + var renderNodeReference = new RenderNodeReference(i); + var renderNode = RootRenderFeature.RenderNodes[i]; + var renderMesh = (RenderMesh)renderNode.RenderObject; - // Ignore fallback effects - if (renderNode.RenderEffect.State != RenderEffectState.Normal) - return; + // Ignore fallback effects + if (renderNode.RenderEffect.State != RenderEffectState.Normal) + continue; - // Collect materials and create associated MaterialInfo (includes reflection) first time - // TODO: We assume same material will generate same ResourceGroup (i.e. same resources declared in same order) - // Need to offer some protection if this invariant is violated (or support it if it can actually happen in real scenario) - var material = renderMesh.MaterialPass; - var materialInfo = renderMesh.MaterialInfo; - var materialParameters = material.Parameters; + // Collect materials and create associated MaterialInfo (includes reflection) first time + // TODO: We assume same material will generate same ResourceGroup (i.e. same resources declared in same order) + // Need to offer some protection if this invariant is violated (or support it if it can actually happen in real scenario) + var material = renderMesh.MaterialPass; + var materialInfo = renderMesh.MaterialInfo; + var materialParameters = material.Parameters; - // Register resources usage - Context.StreamingManager?.StreamResources(materialParameters); + // Register resources usage + Context.StreamingManager?.StreamResources(materialParameters); - if (!UpdateMaterial(RenderSystem, threadContext, materialInfo, perMaterialDescriptorSetSlot.Index, renderNode.RenderEffect, materialParameters)) - return; + if (!UpdateMaterial(RenderSystem, threadContext, materialInfo, perMaterialDescriptorSetSlot.Index, renderNode.RenderEffect, materialParameters)) + continue; - var descriptorSetPoolOffset = ((RootEffectRenderFeature)RootRenderFeature).ComputeResourceGroupOffset(renderNodeReference); - resourceGroupPool[descriptorSetPoolOffset + perMaterialDescriptorSetSlot.Index] = materialInfo.Resources; + var descriptorSetPoolOffset = ((RootEffectRenderFeature)RootRenderFeature).ComputeResourceGroupOffset(renderNodeReference); + resourceGroupPool[descriptorSetPoolOffset + perMaterialDescriptorSetSlot.Index] = materialInfo.Resources; + } }); } diff --git a/sources/engine/Stride.Rendering/Rendering/SkinningRenderFeature.cs b/sources/engine/Stride.Rendering/Rendering/SkinningRenderFeature.cs index e05af8ed36..c453dfb92c 100644 --- a/sources/engine/Stride.Rendering/Rendering/SkinningRenderFeature.cs +++ b/sources/engine/Stride.Rendering/Rendering/SkinningRenderFeature.cs @@ -56,7 +56,7 @@ public override void PrepareEffectPermutations(RenderDrawContext context) int effectSlotCount = ((RootEffectRenderFeature)RootRenderFeature).EffectPermutationSlotCount; //foreach (var objectNodeReference in RootRenderFeature.ObjectNodeReferences) - Dispatcher.ForEach(((RootEffectRenderFeature)RootRenderFeature).ObjectNodeReferences, objectNodeReference => + Dispatcher.ForEach(RootRenderFeature.ObjectNodeReferences, objectNodeReference => { var objectNode = RootRenderFeature.GetObjectNode(objectNodeReference); var renderMesh = (RenderMesh)objectNode.RenderObject; @@ -116,25 +116,29 @@ public override unsafe void Prepare(RenderDrawContext context) { var renderModelObjectInfoData = RootRenderFeature.RenderData.GetData(renderModelObjectInfoKey); - Dispatcher.ForEach(((RootEffectRenderFeature)RootRenderFeature).RenderNodes, (ref RenderNode renderNode) => + Dispatcher.ForBatched(RootRenderFeature.RenderNodes.Count, (from, toExclusive) => { - var perDrawLayout = renderNode.RenderEffect.Reflection?.PerDrawLayout; - if (perDrawLayout == null) - return; + for (int i = from; i < toExclusive; i++) + { + var renderNode = RootRenderFeature.RenderNodes[i]; + var perDrawLayout = renderNode.RenderEffect.Reflection?.PerDrawLayout; + if (perDrawLayout == null) + continue; - var blendMatricesOffset = perDrawLayout.GetConstantBufferOffset(blendMatrices); - if (blendMatricesOffset == -1) - return; + var blendMatricesOffset = perDrawLayout.GetConstantBufferOffset(blendMatrices); + if (blendMatricesOffset == -1) + continue; - var renderModelObjectInfo = renderModelObjectInfoData[renderNode.RenderObject.ObjectNode]; - if (renderModelObjectInfo == null) - return; + var renderModelObjectInfo = renderModelObjectInfoData[renderNode.RenderObject.ObjectNode]; + if (renderModelObjectInfo == null) + continue; - var mappedCB = (byte*)renderNode.Resources.ConstantBuffer.Data + blendMatricesOffset; + var mappedCB = (byte*)renderNode.Resources.ConstantBuffer.Data + blendMatricesOffset; - fixed (Matrix* blendMatricesPtr = renderModelObjectInfo) - { - Unsafe.CopyBlockUnaligned(mappedCB, blendMatricesPtr, (uint)renderModelObjectInfo.Length * (uint)sizeof(Matrix)); + fixed (Matrix* blendMatricesPtr = renderModelObjectInfo) + { + Unsafe.CopyBlockUnaligned(mappedCB, blendMatricesPtr, (uint)renderModelObjectInfo.Length * (uint)sizeof(Matrix)); + } } }); } diff --git a/sources/engine/Stride.Rendering/Rendering/TransformRenderFeature.cs b/sources/engine/Stride.Rendering/Rendering/TransformRenderFeature.cs index 79d229ef24..766284d84e 100644 --- a/sources/engine/Stride.Rendering/Rendering/TransformRenderFeature.cs +++ b/sources/engine/Stride.Rendering/Rendering/TransformRenderFeature.cs @@ -160,51 +160,55 @@ public override unsafe void Prepare(RenderDrawContext context) // Update PerDraw (World, WorldViewProj, etc...) // Copy Entity.World to PerDraw cbuffer // TODO: Have a PerObject cbuffer? - Dispatcher.ForEach(((RootEffectRenderFeature)RootRenderFeature).RenderNodes, (ref RenderNode renderNode) => + Dispatcher.ForBatched(RootRenderFeature.RenderNodes.Count, (from, toExclusive) => { - var perDrawLayout = renderNode.RenderEffect.Reflection?.PerDrawLayout; - if (perDrawLayout == null) - return; + for (int i = from; i < toExclusive; i++) + { + var renderNode = RootRenderFeature.RenderNodes[i]; + var perDrawLayout = renderNode.RenderEffect.Reflection?.PerDrawLayout; + if (perDrawLayout == null) + continue; - var worldOffset = perDrawLayout.GetConstantBufferOffset(this.world); - var worldInverseOffset = perDrawLayout.GetConstantBufferOffset(this.worldInverse); + var worldOffset = perDrawLayout.GetConstantBufferOffset(this.world); + var worldInverseOffset = perDrawLayout.GetConstantBufferOffset(this.worldInverse); - if (worldOffset == -1 && worldInverseOffset == -1) - return; + if (worldOffset == -1 && worldInverseOffset == -1) + continue; - ref var renderModelObjectInfo = ref renderModelObjectInfoData[renderNode.RenderObject.ObjectNode]; - ref var renderModelViewInfo = ref renderModelViewInfoData[renderNode.ViewObjectNode]; + ref var renderModelObjectInfo = ref renderModelObjectInfoData[renderNode.RenderObject.ObjectNode]; + ref var renderModelViewInfo = ref renderModelViewInfoData[renderNode.ViewObjectNode]; - var mappedCB = renderNode.Resources.ConstantBuffer.Data; - if (worldOffset != -1) - { - var world = (Matrix*)((byte*)mappedCB + worldOffset); - *world = renderModelObjectInfo.World; - } - - if (worldInverseOffset != -1) - { - var perDraw = (PerDrawExtra*)((byte*)mappedCB + worldInverseOffset); + var mappedCB = renderNode.Resources.ConstantBuffer.Data; + if (worldOffset != -1) + { + var world = (Matrix*)((byte*)mappedCB + worldOffset); + *world = renderModelObjectInfo.World; + } - // Fill PerDraw - var perDrawData = new PerDrawExtra + if (worldInverseOffset != -1) { - WorldView = renderModelViewInfo.WorldView, - WorldViewProjection = renderModelViewInfo.WorldViewProjection, - }; + var perDraw = (PerDrawExtra*)((byte*)mappedCB + worldInverseOffset); + + // Fill PerDraw + var perDrawData = new PerDrawExtra + { + WorldView = renderModelViewInfo.WorldView, + WorldViewProjection = renderModelViewInfo.WorldViewProjection, + }; - Matrix.Invert(ref renderModelObjectInfo.World, out perDrawData.WorldInverse); - Matrix.Transpose(ref perDrawData.WorldInverse, out perDrawData.WorldInverseTranspose); - Matrix.Invert(ref renderModelViewInfo.WorldView, out perDrawData.WorldViewInverse); + Matrix.Invert(ref renderModelObjectInfo.World, out perDrawData.WorldInverse); + Matrix.Transpose(ref perDrawData.WorldInverse, out perDrawData.WorldInverseTranspose); + Matrix.Invert(ref renderModelViewInfo.WorldView, out perDrawData.WorldViewInverse); - perDrawData.WorldScale = new Vector3( - ((Vector3)renderModelObjectInfo.World.Row1).Length(), - ((Vector3)renderModelObjectInfo.World.Row2).Length(), - ((Vector3)renderModelObjectInfo.World.Row3).Length()); + perDrawData.WorldScale = new Vector3( + ((Vector3)renderModelObjectInfo.World.Row1).Length(), + ((Vector3)renderModelObjectInfo.World.Row2).Length(), + ((Vector3)renderModelObjectInfo.World.Row3).Length()); - perDrawData.EyeMS = new Vector4(perDrawData.WorldInverse.M41, perDrawData.WorldInverse.M42, perDrawData.WorldInverse.M43, 1.0f); + perDrawData.EyeMS = new Vector4(perDrawData.WorldInverse.M41, perDrawData.WorldInverse.M42, perDrawData.WorldInverse.M43, 1.0f); - *perDraw = perDrawData; + *perDraw = perDrawData; + } } }); } From 0327957b312afd27f5bf3963f33902e8b8463425 Mon Sep 17 00:00:00 2001 From: Eideren Date: Sun, 7 Jan 2024 19:23:01 +0100 Subject: [PATCH 005/247] [Build] Fix misc warnings (#2063) --- .../Stride.Assets.Tests2.csproj | 1 - .../engine/Stride.Graphics/SDL/Application.cs | 4 +- sources/engine/Stride.Graphics/SDL/Window.cs | 50 +++++++++---------- .../Stride.UI.Tests/Layering/EditTextTests.cs | 2 +- .../Layering/TextBlockTests.cs | 2 +- .../Stride.VirtualReality/OpenVR/OpenVR.cs | 24 +++------ .../Stride.ProjectGenerator.csproj | 6 --- 7 files changed, 35 insertions(+), 54 deletions(-) diff --git a/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj b/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj index 9763d041b5..e3dffa4c0c 100644 --- a/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj +++ b/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj @@ -12,7 +12,6 @@ - diff --git a/sources/engine/Stride.Graphics/SDL/Application.cs b/sources/engine/Stride.Graphics/SDL/Application.cs index 25c1bcc461..0e44e0c3ec 100644 --- a/sources/engine/Stride.Graphics/SDL/Application.cs +++ b/sources/engine/Stride.Graphics/SDL/Application.cs @@ -175,11 +175,11 @@ public static void ProcessEvent(Event e) ctrl = WindowFromSdlHandle(SDL.GetWindowFromID(e.Window.WindowID)); switch ((WindowEventID)e.Window.Event) { - case WindowEventID.WindoweventFocusGained: + case WindowEventID.FocusGained: WindowWithFocus = ctrl; break; - case WindowEventID.WindoweventFocusLost: + case WindowEventID.FocusLost: WindowWithFocus = null; break; } diff --git a/sources/engine/Stride.Graphics/SDL/Window.cs b/sources/engine/Stride.Graphics/SDL/Window.cs index 78a3ed9f71..484c1484d8 100644 --- a/sources/engine/Stride.Graphics/SDL/Window.cs +++ b/sources/engine/Stride.Graphics/SDL/Window.cs @@ -39,16 +39,16 @@ static Window() /// Title of the window, see Text property. public unsafe Window(string title) { - WindowFlags flags = WindowFlags.WindowAllowHighdpi; + WindowFlags flags = WindowFlags.AllowHighdpi; #if STRIDE_GRAPHICS_API_OPENGL - flags |= WindowFlags.WindowOpengl; + flags |= WindowFlags.Opengl; #elif STRIDE_GRAPHICS_API_VULKAN - flags |= WindowFlags.WindowVulkan; + flags |= WindowFlags.Vulkan; #endif #if STRIDE_PLATFORM_ANDROID || STRIDE_PLATFORM_IOS - flags |= WindowFlags.WindowBorderless | WindowFlags.WindowFullscreen | WindowFlags.WindowShown; + flags |= WindowFlags.Borderless | WindowFlags.Fullscreen | WindowFlags.Shown; #else - flags |= WindowFlags.WindowHidden | WindowFlags.WindowResizable; + flags |= WindowFlags.Hidden | WindowFlags.Resizable; #endif // Create the SDL window and then extract the native handle. sdlHandle = SDL.CreateWindow(title, Sdl.WindowposUndefined, Sdl.WindowposUndefined, 640, 480, (uint)flags); @@ -191,12 +191,12 @@ public bool IsFullScreen private WindowFlags GetFullscreenFlag() { - return FullscreenIsBorderlessWindow ? WindowFlags.WindowFullscreenDesktop : WindowFlags.WindowFullscreen; + return FullscreenIsBorderlessWindow ? WindowFlags.FullscreenDesktop : WindowFlags.Fullscreen; } private static bool CheckFullscreenFlag(uint flags) { - return ((flags & (uint)WindowFlags.WindowFullscreen) != 0) || ((flags & (uint)WindowFlags.WindowFullscreenDesktop) != 0); + return ((flags & (uint)WindowFlags.Fullscreen) != 0) || ((flags & (uint)WindowFlags.FullscreenDesktop) != 0); } /// @@ -206,7 +206,7 @@ public bool Visible { get { - return (SDL.GetWindowFlags(sdlHandle) & (uint)WindowFlags.WindowShown) != 0; + return (SDL.GetWindowFlags(sdlHandle) & (uint)WindowFlags.Shown) != 0; } set { @@ -233,11 +233,11 @@ public FormWindowState WindowState { return FormWindowState.Fullscreen; } - if ((flags & (uint)WindowFlags.WindowMaximized) != 0) + if ((flags & (uint)WindowFlags.Maximized) != 0) { return FormWindowState.Maximized; } - else if ((flags & (uint)WindowFlags.WindowMinimized) != 0) + else if ((flags & (uint)WindowFlags.Minimized) != 0) { return FormWindowState.Minimized; } @@ -273,7 +273,7 @@ public bool Focused { get { - return (SDL.GetWindowFlags(sdlHandle) & (uint)WindowFlags.WindowInputFocus) != 0; + return (SDL.GetWindowFlags(sdlHandle) & (uint)WindowFlags.InputFocus) != 0; } } @@ -388,8 +388,8 @@ public FormBorderStyle FormBorderStyle get { uint flags = SDL.GetWindowFlags(sdlHandle); - var isResizeable = (flags & (uint)WindowFlags.WindowResizable) != 0; - var isBorderless = (flags & (uint)WindowFlags.WindowBorderless) != 0; + var isResizeable = (flags & (uint)WindowFlags.Resizable) != 0; + var isBorderless = (flags & (uint)WindowFlags.Borderless) != 0; if (isBorderless) { return FormBorderStyle.None; @@ -523,51 +523,51 @@ public virtual void ProcessEvent(Event e) { switch ((WindowEventID)e.Window.Event) { - case WindowEventID.WindoweventSizeChanged: + case WindowEventID.SizeChanged: ResizeBeginActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventResized: + case WindowEventID.Resized: ResizeEndActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventClose: + case WindowEventID.Close: CloseActions?.Invoke(); break; - case WindowEventID.WindoweventShown: + case WindowEventID.Shown: ActivateActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventHidden: + case WindowEventID.Hidden: DeActivateActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventMinimized: + case WindowEventID.Minimized: MinimizedActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventMaximized: + case WindowEventID.Maximized: MaximizedActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventRestored: + case WindowEventID.Restored: RestoredActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventEnter: + case WindowEventID.Enter: MouseEnterActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventLeave: + case WindowEventID.Leave: MouseLeaveActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventFocusGained: + case WindowEventID.FocusGained: FocusGainedActions?.Invoke(e.Window); break; - case WindowEventID.WindoweventFocusLost: + case WindowEventID.FocusLost: FocusLostActions?.Invoke(e.Window); break; } diff --git a/sources/engine/Stride.UI.Tests/Layering/EditTextTests.cs b/sources/engine/Stride.UI.Tests/Layering/EditTextTests.cs index ed9ec26627..c7db2420b8 100644 --- a/sources/engine/Stride.UI.Tests/Layering/EditTextTests.cs +++ b/sources/engine/Stride.UI.Tests/Layering/EditTextTests.cs @@ -17,7 +17,7 @@ namespace Stride.UI.Tests.Layering [System.ComponentModel.Description("Tests for EditText layering")] public class EditTextTests { - class DummyFont : SpriteFont { } + public class DummyFont : SpriteFont { } /// /// Test the invalidations generated object property changes. diff --git a/sources/engine/Stride.UI.Tests/Layering/TextBlockTests.cs b/sources/engine/Stride.UI.Tests/Layering/TextBlockTests.cs index 6f39bf66e1..d808c9b04a 100644 --- a/sources/engine/Stride.UI.Tests/Layering/TextBlockTests.cs +++ b/sources/engine/Stride.UI.Tests/Layering/TextBlockTests.cs @@ -14,7 +14,7 @@ namespace Stride.UI.Tests.Layering [System.ComponentModel.Description("Tests for TextBlock layering")] public class TextBlockTests : TextBlock { - private class DummyFont : SpriteFont { } + public class DummyFont : SpriteFont { } /// /// Test the invalidations generated object property changes. diff --git a/sources/engine/Stride.VirtualReality/OpenVR/OpenVR.cs b/sources/engine/Stride.VirtualReality/OpenVR/OpenVR.cs index e9929ff4f7..9da9de784e 100644 --- a/sources/engine/Stride.VirtualReality/OpenVR/OpenVR.cs +++ b/sources/engine/Stride.VirtualReality/OpenVR/OpenVR.cs @@ -16,18 +16,6 @@ namespace Stride.VirtualReality { internal static class OpenVR { - /// Bypasses definite assignment rules for a given reference. - /// A thin wrapper around for the sole purpose of making it usable in an expression. - /// The type of the reference. - /// The reference whose initialization should be skipped. - /// The reference to . - /// Take care to ensure that the struct has been initialized appropriately, otherwise the struct's fields could contain uninitialized data from the stack. - private static unsafe ref T SkipInit(out T value) - { - Unsafe.SkipInit(out value); - return ref value; - } - public class Controller { // This helper can be used in a variety of ways. Beware that indices may change @@ -308,8 +296,8 @@ private static unsafe DeviceState GetControllerPoseUnsafe(int controllerIndex, o { ref var devicePose = ref DevicePoses[index]; Unsafe.As(ref pose) = devicePose.mDeviceToAbsoluteTracking; - Unsafe.As(ref SkipInit(out velocity)) = devicePose.vVelocity; - Unsafe.As(ref SkipInit(out angVelocity)) = devicePose.vAngularVelocity; + velocity = Unsafe.As(ref devicePose.vVelocity); + angVelocity = Unsafe.As(ref devicePose.vAngularVelocity); var state = DeviceState.Invalid; if (devicePose.bDeviceIsConnected && devicePose.bPoseIsValid) @@ -344,8 +332,8 @@ private static unsafe DeviceState GetTrackerPoseUnsafe(int trackerIndex, out Mat ref var devicePose = ref DevicePoses[trackerIndex]; Unsafe.As(ref pose) = devicePose.mDeviceToAbsoluteTracking; - Unsafe.As(ref SkipInit(out velocity)) = devicePose.vVelocity; - Unsafe.As(ref SkipInit(out angVelocity)) = devicePose.vAngularVelocity; + velocity = Unsafe.As(ref devicePose.vVelocity); + angVelocity = Unsafe.As(ref devicePose.vAngularVelocity); var state = DeviceState.Invalid; if (devicePose.bDeviceIsConnected && devicePose.bPoseIsValid) @@ -376,8 +364,8 @@ private static unsafe DeviceState GetHeadPoseUnsafe(out Matrix pose, out Vector3 if (Valve.VR.OpenVR.System.GetTrackedDeviceClass(index) == ETrackedDeviceClass.HMD) { Unsafe.As(ref pose) = devicePose.mDeviceToAbsoluteTracking; - Unsafe.As(ref SkipInit(out linearVelocity)) = devicePose.vVelocity; - Unsafe.As(ref SkipInit(out angularVelocity)) = devicePose.vAngularVelocity; + linearVelocity = Unsafe.As(ref devicePose.vVelocity); + angularVelocity = Unsafe.As(ref devicePose.vAngularVelocity); var state = DeviceState.Invalid; if (DevicePoses[index].bDeviceIsConnected && DevicePoses[index].bPoseIsValid) diff --git a/sources/tools/Stride.ProjectGenerator/Stride.ProjectGenerator.csproj b/sources/tools/Stride.ProjectGenerator/Stride.ProjectGenerator.csproj index 856acdb142..9adc4ee191 100644 --- a/sources/tools/Stride.ProjectGenerator/Stride.ProjectGenerator.csproj +++ b/sources/tools/Stride.ProjectGenerator/Stride.ProjectGenerator.csproj @@ -8,12 +8,6 @@ WindowsTools false - - true - - - true - From f6d95f42cca7221fa146887862a2feb631ec51c7 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Thu, 11 Jan 2024 07:43:17 +0100 Subject: [PATCH 006/247] [Build] Restore rules on stride analysers (#2089) --- sources/targets/Stride.Core.targets | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sources/targets/Stride.Core.targets b/sources/targets/Stride.Core.targets index 81b90a66e1..c7bd8f0e5a 100644 --- a/sources/targets/Stride.Core.targets +++ b/sources/targets/Stride.Core.targets @@ -46,7 +46,10 @@ Condition="'$(StrideEnableCodeAnalysis)' != 'true'"> - + + + + From 0f6f4eeef435dd2f25cb1760580dda80bf7ea9f2 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Thu, 11 Jan 2024 17:46:51 +0100 Subject: [PATCH 007/247] Update dependencies (#2046) --- samples/Tests/Stride.Samples.Tests.csproj | 9 +- sources/Directory.Packages.props | 118 +++++------ .../Stride.Core.Assets.Quantum.Tests.csproj | 5 +- ...estAssetCompositeHierarchySerialization.cs | 2 + .../TestObjectReferenceSerialization.cs | 2 + .../TestOverrideSerialization.cs | 2 + .../Stride.Core.Assets.Tests.csproj | 5 +- .../Stride.Core.BuildEngine.Tests.csproj | 5 +- ...Stride.Core.AssemblyProcessor.Tests.csproj | 6 +- .../Stride.Core.AssemblyProcessor.csproj | 4 +- .../Stride.Core.CompilerServices.Tests.csproj | 5 +- .../Stride.Core.Design.Tests.csproj | 5 +- .../Stride.Core.Design.Tests/TestFileLock.cs | 2 +- .../Stride.Core.Mathematics.Tests.csproj | 5 +- .../Stride.Core.Tests.csproj | 5 +- .../Stride.Core.Yaml.Tests.csproj | 5 +- .../Stride.Core.Assets.Editor.Tests.csproj | 5 +- .../Stride.GameStudio.Tests.csproj | 5 +- .../Stride.Assets.Tests.csproj | 5 +- .../Stride.Assets.Tests2.csproj | 5 +- .../Stride.Engine.Tests/EventSystemTests.cs | 2 +- .../Stride.Graphics.Regression.csproj | 5 +- .../DynamicBarrierTest.cs | 2 +- .../Stride.Navigation.Tests/StaticTest.cs | 2 +- .../Stride.Physics.Tests/CharacterTest.cs | 2 +- .../Stride.Shaders.Tests.Windows.csproj | 5 +- ...ide.Core.Presentation.Quantum.Tests.csproj | 5 +- .../Stride.Core.Presentation.Tests.csproj | 5 +- .../Stride.Core.Quantum.Tests.csproj | 5 +- .../Stride.TextureConverter.Tests.csproj | 5 +- sources/tests/xunit.runner.stride/App.axaml | 8 + .../tests/xunit.runner.stride/App.axaml.cs | 65 ++++++ sources/tests/xunit.runner.stride/App.xaml | 13 -- sources/tests/xunit.runner.stride/App.xaml.cs | 13 -- .../xunit.runner.stride/StrideXunitRunner.cs | 56 +++--- .../tests/xunit.runner.stride/ViewLocator.cs | 32 --- .../ViewModels/MainViewModel.cs | 14 ++ .../ViewModels/MainWindowViewModel.cs | 15 -- .../ViewModels/TestCaseViewModel.cs | 56 +++--- .../ViewModels/TestGroupViewModel.cs | 59 +++--- .../ViewModels/TestNodeViewModel.cs | 11 +- .../ViewModels/TestsViewModel.cs | 185 +++++++++--------- .../ViewModels/ViewModelBase.cs | 10 +- .../xunit.runner.stride/ViewModels/XSink.cs | 84 ++++---- .../xunit.runner.stride/Views/MainView.axaml | 50 +++++ .../Views/MainView.axaml.cs | 14 ++ .../Views/MainWindow.axaml | 11 ++ .../Views/MainWindow.axaml.cs | 14 ++ .../xunit.runner.stride/Views/MainWindow.xaml | 41 ---- .../Views/MainWindow.xaml.cs | 19 -- .../tests/xunit.runner.stride/app.manifest | 18 ++ .../xunit.runner.stride.csproj | 20 +- .../Stride.Code.Tests.csproj | 5 +- ...Stride.Core.ProjectTemplating.Tests.csproj | 5 +- .../Stride.VisualStudio.Package.Tests.csproj | 45 ++--- 55 files changed, 601 insertions(+), 505 deletions(-) create mode 100644 sources/tests/xunit.runner.stride/App.axaml create mode 100644 sources/tests/xunit.runner.stride/App.axaml.cs delete mode 100644 sources/tests/xunit.runner.stride/App.xaml delete mode 100644 sources/tests/xunit.runner.stride/App.xaml.cs delete mode 100644 sources/tests/xunit.runner.stride/ViewLocator.cs create mode 100644 sources/tests/xunit.runner.stride/ViewModels/MainViewModel.cs delete mode 100644 sources/tests/xunit.runner.stride/ViewModels/MainWindowViewModel.cs create mode 100644 sources/tests/xunit.runner.stride/Views/MainView.axaml create mode 100644 sources/tests/xunit.runner.stride/Views/MainView.axaml.cs create mode 100644 sources/tests/xunit.runner.stride/Views/MainWindow.axaml create mode 100644 sources/tests/xunit.runner.stride/Views/MainWindow.axaml.cs delete mode 100644 sources/tests/xunit.runner.stride/Views/MainWindow.xaml delete mode 100644 sources/tests/xunit.runner.stride/Views/MainWindow.xaml.cs create mode 100644 sources/tests/xunit.runner.stride/app.manifest diff --git a/samples/Tests/Stride.Samples.Tests.csproj b/samples/Tests/Stride.Samples.Tests.csproj index 46bbd96d2c..b9c9893bd6 100644 --- a/samples/Tests/Stride.Samples.Tests.csproj +++ b/samples/Tests/Stride.Samples.Tests.csproj @@ -11,8 +11,8 @@ true - - + + @@ -29,4 +29,9 @@ + + + + + \ No newline at end of file diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props index 96804ebbf8..2daa2be23d 100644 --- a/sources/Directory.Packages.props +++ b/sources/Directory.Packages.props @@ -5,9 +5,10 @@ - + - + + @@ -18,49 +19,47 @@ - - - - - - + + + + + + - + - + - - - - + + + - - - + + - - + + - + - + - + @@ -69,13 +68,13 @@ - - - + + + - - + + @@ -83,65 +82,43 @@ - - + + - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - + + + + + - - - - - + + + + + - - + - - - + + + @@ -149,11 +126,10 @@ - - - + + - + \ No newline at end of file diff --git a/sources/assets/Stride.Core.Assets.Quantum.Tests/Stride.Core.Assets.Quantum.Tests.csproj b/sources/assets/Stride.Core.Assets.Quantum.Tests/Stride.Core.Assets.Quantum.Tests.csproj index cc9aa1942a..523d2b77cf 100644 --- a/sources/assets/Stride.Core.Assets.Quantum.Tests/Stride.Core.Assets.Quantum.Tests.csproj +++ b/sources/assets/Stride.Core.Assets.Quantum.Tests/Stride.Core.Assets.Quantum.Tests.csproj @@ -7,7 +7,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestAssetCompositeHierarchySerialization.cs b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestAssetCompositeHierarchySerialization.cs index 3534c91bb1..58494708d5 100644 --- a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestAssetCompositeHierarchySerialization.cs +++ b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestAssetCompositeHierarchySerialization.cs @@ -4,6 +4,8 @@ namespace Stride.Core.Assets.Quantum.Tests { + using SerializationHelper = Helpers.SerializationHelper; + public class TestAssetCompositeHierarchySerialization { const string SimpleHierarchyYaml = @"!MyAssetHierarchy diff --git a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestObjectReferenceSerialization.cs b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestObjectReferenceSerialization.cs index 18e776fa07..e4903a63a4 100644 --- a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestObjectReferenceSerialization.cs +++ b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestObjectReferenceSerialization.cs @@ -10,6 +10,8 @@ namespace Stride.Core.Assets.Quantum.Tests { + using SerializationHelper = Helpers.SerializationHelper; + public class TestObjectReferenceSerialization { private const string SimpleReferenceYaml = @"!Stride.Core.Assets.Quantum.Tests.Helpers.Types+MyAssetWithRef,Stride.Core.Assets.Quantum.Tests diff --git a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestOverrideSerialization.cs b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestOverrideSerialization.cs index c84ff7f5c8..b87152aed3 100644 --- a/sources/assets/Stride.Core.Assets.Quantum.Tests/TestOverrideSerialization.cs +++ b/sources/assets/Stride.Core.Assets.Quantum.Tests/TestOverrideSerialization.cs @@ -13,6 +13,8 @@ namespace Stride.Core.Assets.Quantum.Tests { + using SerializationHelper = Helpers.SerializationHelper; + public class TestOverrideSerialization { /* test TODO: diff --git a/sources/assets/Stride.Core.Assets.Tests/Stride.Core.Assets.Tests.csproj b/sources/assets/Stride.Core.Assets.Tests/Stride.Core.Assets.Tests.csproj index acfb37588e..711ca07f66 100644 --- a/sources/assets/Stride.Core.Assets.Tests/Stride.Core.Assets.Tests.csproj +++ b/sources/assets/Stride.Core.Assets.Tests/Stride.Core.Assets.Tests.csproj @@ -11,7 +11,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/buildengine/Stride.Core.BuildEngine.Tests/Stride.Core.BuildEngine.Tests.csproj b/sources/buildengine/Stride.Core.BuildEngine.Tests/Stride.Core.BuildEngine.Tests.csproj index 502eea2172..5122c74bc3 100644 --- a/sources/buildengine/Stride.Core.BuildEngine.Tests/Stride.Core.BuildEngine.Tests.csproj +++ b/sources/buildengine/Stride.Core.BuildEngine.Tests/Stride.Core.BuildEngine.Tests.csproj @@ -11,7 +11,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj b/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj index f2d86144c5..8721724ee6 100644 --- a/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj +++ b/sources/core/Stride.Core.AssemblyProcessor.Tests/Stride.Core.AssemblyProcessor.Tests.csproj @@ -18,9 +18,9 @@ ASSEMBLY_PROCESSOR;STRIDE_PLATFORM_DESKTOP;TRACE - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/sources/core/Stride.Core.AssemblyProcessor/Stride.Core.AssemblyProcessor.csproj b/sources/core/Stride.Core.AssemblyProcessor/Stride.Core.AssemblyProcessor.csproj index c6fda13073..94888a59b8 100644 --- a/sources/core/Stride.Core.AssemblyProcessor/Stride.Core.AssemblyProcessor.csproj +++ b/sources/core/Stride.Core.AssemblyProcessor/Stride.Core.AssemblyProcessor.csproj @@ -35,8 +35,8 @@ - - + + diff --git a/sources/core/Stride.Core.CompilerServices.Tests/Stride.Core.CompilerServices.Tests.csproj b/sources/core/Stride.Core.CompilerServices.Tests/Stride.Core.CompilerServices.Tests.csproj index 297a912dba..fc29f3e11d 100644 --- a/sources/core/Stride.Core.CompilerServices.Tests/Stride.Core.CompilerServices.Tests.csproj +++ b/sources/core/Stride.Core.CompilerServices.Tests/Stride.Core.CompilerServices.Tests.csproj @@ -10,7 +10,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/core/Stride.Core.Design.Tests/Stride.Core.Design.Tests.csproj b/sources/core/Stride.Core.Design.Tests/Stride.Core.Design.Tests.csproj index 53fa573f90..3423cb1b14 100644 --- a/sources/core/Stride.Core.Design.Tests/Stride.Core.Design.Tests.csproj +++ b/sources/core/Stride.Core.Design.Tests/Stride.Core.Design.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/core/Stride.Core.Design.Tests/TestFileLock.cs b/sources/core/Stride.Core.Design.Tests/TestFileLock.cs index 205ceedc1e..afc62243bd 100644 --- a/sources/core/Stride.Core.Design.Tests/TestFileLock.cs +++ b/sources/core/Stride.Core.Design.Tests/TestFileLock.cs @@ -36,7 +36,7 @@ public void TestFilelockWait() { // This should never happen. So throw an exception and make sure it is not caught by our catch below. flag = true; - Assert.True(false, "Cannot create a file lock if parent directory does not exist."); + Assert.Fail("Cannot create a file lock if parent directory does not exist."); } } catch (Exception) diff --git a/sources/core/Stride.Core.Mathematics.Tests/Stride.Core.Mathematics.Tests.csproj b/sources/core/Stride.Core.Mathematics.Tests/Stride.Core.Mathematics.Tests.csproj index c8e4b40880..7d4205a593 100644 --- a/sources/core/Stride.Core.Mathematics.Tests/Stride.Core.Mathematics.Tests.csproj +++ b/sources/core/Stride.Core.Mathematics.Tests/Stride.Core.Mathematics.Tests.csproj @@ -12,7 +12,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/core/Stride.Core.Tests/Stride.Core.Tests.csproj b/sources/core/Stride.Core.Tests/Stride.Core.Tests.csproj index b4cd54d9a5..e9ae07deee 100644 --- a/sources/core/Stride.Core.Tests/Stride.Core.Tests.csproj +++ b/sources/core/Stride.Core.Tests/Stride.Core.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/core/Stride.Core.Yaml.Tests/Stride.Core.Yaml.Tests.csproj b/sources/core/Stride.Core.Yaml.Tests/Stride.Core.Yaml.Tests.csproj index 997555b9df..41247c9701 100644 --- a/sources/core/Stride.Core.Yaml.Tests/Stride.Core.Yaml.Tests.csproj +++ b/sources/core/Stride.Core.Yaml.Tests/Stride.Core.Yaml.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/editor/Stride.Core.Assets.Editor.Tests/Stride.Core.Assets.Editor.Tests.csproj b/sources/editor/Stride.Core.Assets.Editor.Tests/Stride.Core.Assets.Editor.Tests.csproj index 94303b6f2e..824598e2df 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Tests/Stride.Core.Assets.Editor.Tests.csproj +++ b/sources/editor/Stride.Core.Assets.Editor.Tests/Stride.Core.Assets.Editor.Tests.csproj @@ -7,7 +7,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/editor/Stride.GameStudio.Tests/Stride.GameStudio.Tests.csproj b/sources/editor/Stride.GameStudio.Tests/Stride.GameStudio.Tests.csproj index 04f882fbda..ec9fd00f6d 100644 --- a/sources/editor/Stride.GameStudio.Tests/Stride.GameStudio.Tests.csproj +++ b/sources/editor/Stride.GameStudio.Tests/Stride.GameStudio.Tests.csproj @@ -7,7 +7,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj b/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj index 73dbd15cc3..ddc4c9155c 100644 --- a/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj +++ b/sources/engine/Stride.Assets.Tests/Stride.Assets.Tests.csproj @@ -12,7 +12,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj b/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj index e3dffa4c0c..bb0af6039b 100644 --- a/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj +++ b/sources/engine/Stride.Assets.Tests2/Stride.Assets.Tests2.csproj @@ -11,7 +11,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/engine/Stride.Engine.Tests/EventSystemTests.cs b/sources/engine/Stride.Engine.Tests/EventSystemTests.cs index 2b1916bc2d..6cdd055131 100644 --- a/sources/engine/Stride.Engine.Tests/EventSystemTests.cs +++ b/sources/engine/Stride.Engine.Tests/EventSystemTests.cs @@ -446,7 +446,7 @@ public void ReceiveFirstCheck() await game.NextFrame(); } - Assert.True(false, "t2 should be completed"); + Assert.Fail("t2 should be completed"); }); } diff --git a/sources/engine/Stride.Graphics.Regression/Stride.Graphics.Regression.csproj b/sources/engine/Stride.Graphics.Regression/Stride.Graphics.Regression.csproj index fc5de905e1..3d6a3049da 100644 --- a/sources/engine/Stride.Graphics.Regression/Stride.Graphics.Regression.csproj +++ b/sources/engine/Stride.Graphics.Regression/Stride.Graphics.Regression.csproj @@ -22,7 +22,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/engine/Stride.Navigation.Tests/DynamicBarrierTest.cs b/sources/engine/Stride.Navigation.Tests/DynamicBarrierTest.cs index 235e85e0c9..4d3088a34c 100644 --- a/sources/engine/Stride.Navigation.Tests/DynamicBarrierTest.cs +++ b/sources/engine/Stride.Navigation.Tests/DynamicBarrierTest.cs @@ -64,7 +64,7 @@ protected override void Update(GameTime gameTime) base.Update(gameTime); if (gameTime.Total > TimeSpan.FromSeconds(6)) { - Assert.True(false, "Test timed out"); + Assert.Fail("Test timed out"); } } diff --git a/sources/engine/Stride.Navigation.Tests/StaticTest.cs b/sources/engine/Stride.Navigation.Tests/StaticTest.cs index 7f975caedd..23fd6ca9a8 100644 --- a/sources/engine/Stride.Navigation.Tests/StaticTest.cs +++ b/sources/engine/Stride.Navigation.Tests/StaticTest.cs @@ -58,7 +58,7 @@ protected override void Update(GameTime gameTime) base.Update(gameTime); if (gameTime.Total > TimeSpan.FromSeconds(6)) { - Assert.True(false, "Test timed out"); + Assert.Fail("Test timed out"); } } diff --git a/sources/engine/Stride.Physics.Tests/CharacterTest.cs b/sources/engine/Stride.Physics.Tests/CharacterTest.cs index eb96251018..04df8b453f 100644 --- a/sources/engine/Stride.Physics.Tests/CharacterTest.cs +++ b/sources/engine/Stride.Physics.Tests/CharacterTest.cs @@ -123,7 +123,7 @@ public void CharacterTest1() { await game.Script.NextFrame(); } - Assert.True(false, "Character controller never collided with test collider."); + Assert.Fail("Character controller never collided with test collider."); }); controller.SetVelocity(Vector3.UnitX * 2.5f); diff --git a/sources/engine/Stride.Shaders.Tests/Stride.Shaders.Tests.Windows.csproj b/sources/engine/Stride.Shaders.Tests/Stride.Shaders.Tests.Windows.csproj index c7c7c35973..53052129d3 100644 --- a/sources/engine/Stride.Shaders.Tests/Stride.Shaders.Tests.Windows.csproj +++ b/sources/engine/Stride.Shaders.Tests/Stride.Shaders.Tests.Windows.csproj @@ -20,7 +20,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/presentation/Stride.Core.Presentation.Quantum.Tests/Stride.Core.Presentation.Quantum.Tests.csproj b/sources/presentation/Stride.Core.Presentation.Quantum.Tests/Stride.Core.Presentation.Quantum.Tests.csproj index ea6e8304eb..54dee18729 100644 --- a/sources/presentation/Stride.Core.Presentation.Quantum.Tests/Stride.Core.Presentation.Quantum.Tests.csproj +++ b/sources/presentation/Stride.Core.Presentation.Quantum.Tests/Stride.Core.Presentation.Quantum.Tests.csproj @@ -8,7 +8,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/presentation/Stride.Core.Presentation.Tests/Stride.Core.Presentation.Tests.csproj b/sources/presentation/Stride.Core.Presentation.Tests/Stride.Core.Presentation.Tests.csproj index 9b6289a2b4..5884b94f83 100644 --- a/sources/presentation/Stride.Core.Presentation.Tests/Stride.Core.Presentation.Tests.csproj +++ b/sources/presentation/Stride.Core.Presentation.Tests/Stride.Core.Presentation.Tests.csproj @@ -8,7 +8,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/presentation/Stride.Core.Quantum.Tests/Stride.Core.Quantum.Tests.csproj b/sources/presentation/Stride.Core.Quantum.Tests/Stride.Core.Quantum.Tests.csproj index 4adfb016cb..6a0e45d544 100644 --- a/sources/presentation/Stride.Core.Quantum.Tests/Stride.Core.Quantum.Tests.csproj +++ b/sources/presentation/Stride.Core.Quantum.Tests/Stride.Core.Quantum.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/tests/tools/Stride.TextureConverter.Tests/Stride.TextureConverter.Tests.csproj b/sources/tests/tools/Stride.TextureConverter.Tests/Stride.TextureConverter.Tests.csproj index 4bd72d76fc..c8e9597a95 100644 --- a/sources/tests/tools/Stride.TextureConverter.Tests/Stride.TextureConverter.Tests.csproj +++ b/sources/tests/tools/Stride.TextureConverter.Tests/Stride.TextureConverter.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/tests/xunit.runner.stride/App.axaml b/sources/tests/xunit.runner.stride/App.axaml new file mode 100644 index 0000000000..7cc4125567 --- /dev/null +++ b/sources/tests/xunit.runner.stride/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/sources/tests/xunit.runner.stride/App.axaml.cs b/sources/tests/xunit.runner.stride/App.axaml.cs new file mode 100644 index 0000000000..60d3718985 --- /dev/null +++ b/sources/tests/xunit.runner.stride/App.axaml.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System; +using System.Threading; +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using xunit.runner.stride.ViewModels; +using xunit.runner.stride.Views; + +namespace xunit.runner.stride; + +public partial class App : Application +{ + internal readonly CancellationTokenSource cts = new(); + internal Action setInteractiveMode; + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + BindingPlugins.DataValidators.RemoveAt(0); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainViewModel + { + Tests = + { + SetInteractiveMode = setInteractiveMode, + IsInteractiveMode = true, + } + } + }; + desktop.MainWindow.Closed += (_, __) => cts.Cancel(); + desktop.MainWindow.Show(); + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + // don't remove; also used by visual designer. + singleViewPlatform.MainView = new MainView + { + DataContext = new MainViewModel + { + Tests = + { + SetInteractiveMode = setInteractiveMode, + IsInteractiveMode = true, + } + } + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/sources/tests/xunit.runner.stride/App.xaml b/sources/tests/xunit.runner.stride/App.xaml deleted file mode 100644 index c10d222088..0000000000 --- a/sources/tests/xunit.runner.stride/App.xaml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - diff --git a/sources/tests/xunit.runner.stride/App.xaml.cs b/sources/tests/xunit.runner.stride/App.xaml.cs deleted file mode 100644 index 011c7bcd90..0000000000 --- a/sources/tests/xunit.runner.stride/App.xaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia; -using Avalonia.Markup.Xaml; - -namespace xunit.runner.stride -{ - public class App : Application - { - public override void Initialize() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/sources/tests/xunit.runner.stride/StrideXunitRunner.cs b/sources/tests/xunit.runner.stride/StrideXunitRunner.cs index 730730e0f6..f982607aa7 100644 --- a/sources/tests/xunit.runner.stride/StrideXunitRunner.cs +++ b/sources/tests/xunit.runner.stride/StrideXunitRunner.cs @@ -1,43 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + using System; using Avalonia; using Avalonia.Controls; -using Avalonia.ReactiveUI; -using xunit.runner.stride.ViewModels; -using xunit.runner.stride.Views; +using Avalonia.Controls.ApplicationLifetimes; + +namespace xunit.runner.stride; -namespace xunit.runner.stride +public class StrideXunitRunner { - public class StrideXunitRunner + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + public static void Main(string[] args, Action setInteractiveMode = null) { - // Initialization code. Don't use any Avalonia, third-party APIs or any - // SynchronizationContext-reliant code before AppMain is called: things aren't initialized - // yet and stuff might break. - public static void Main(string[] args, Action setInteractiveMode = null) => BuildAvaloniaApp().Start((app, args2) => AppMain(app, args2, setInteractiveMode), args); - - // Avalonia configuration, don't remove; also used by visual designer. - public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace() - .UseReactiveUI(); - - // Your application's entry point. Here you can initialize your MVVM framework, DI - // container, etc. - private static void AppMain(Application app, string[] args, Action setInteractiveMode) + var builder = BuildAvaloniaApp() + .SetupWithLifetime(new ClassicDesktopStyleApplicationLifetime()); + if (builder.Instance is App app) { - var window = new MainWindow - { - DataContext = new MainWindowViewModel - { - Tests = - { - SetInteractiveMode = setInteractiveMode, - IsInteractiveMode = true, - } - } - }; - - app.Run(window); + app.setInteractiveMode = setInteractiveMode; + app.Run(app.cts.Token); } } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); } diff --git a/sources/tests/xunit.runner.stride/ViewLocator.cs b/sources/tests/xunit.runner.stride/ViewLocator.cs deleted file mode 100644 index 17f2b7bba7..0000000000 --- a/sources/tests/xunit.runner.stride/ViewLocator.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using Avalonia.Controls; -using Avalonia.Controls.Templates; -using xunit.runner.stride.ViewModels; - -namespace xunit.runner.stride -{ - public class ViewLocator : IDataTemplate - { - public bool SupportsRecycling => false; - - public IControl Build(object data) - { - var name = data.GetType().FullName.Replace("ViewModel", "View"); - var type = Type.GetType(name); - - if (type != null) - { - return (Control)Activator.CreateInstance(type); - } - else - { - return new TextBlock { Text = "Not Found: " + name }; - } - } - - public bool Match(object data) - { - return data is ViewModelBase; - } - } -} \ No newline at end of file diff --git a/sources/tests/xunit.runner.stride/ViewModels/MainViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/MainViewModel.cs new file mode 100644 index 0000000000..1b93b3bbef --- /dev/null +++ b/sources/tests/xunit.runner.stride/ViewModels/MainViewModel.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace xunit.runner.stride.ViewModels; + +public class MainViewModel : ViewModelBase +{ + public MainViewModel() + { + + } + + public TestsViewModel Tests { get; } = new TestsViewModel(); +} diff --git a/sources/tests/xunit.runner.stride/ViewModels/MainWindowViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/MainWindowViewModel.cs deleted file mode 100644 index 2f4f46f6be..0000000000 --- a/sources/tests/xunit.runner.stride/ViewModels/MainWindowViewModel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Text; - -namespace xunit.runner.stride.ViewModels -{ - public class MainWindowViewModel : ViewModelBase - { - public MainWindowViewModel() - { - - } - - public TestsViewModel Tests { get; } = new TestsViewModel(); - } -} diff --git a/sources/tests/xunit.runner.stride/ViewModels/TestCaseViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/TestCaseViewModel.cs index 2396bf78ee..9ca872f353 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/TestCaseViewModel.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/TestCaseViewModel.cs @@ -1,39 +1,37 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ReactiveUI; -using Xunit; +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Generic; using Xunit.Abstractions; -namespace xunit.runner.stride.ViewModels -{ - public class TestCaseViewModel : TestNodeViewModel - { - private readonly TestsViewModel tests; +namespace xunit.runner.stride.ViewModels; - public ITestCase TestCase { get; } +public class TestCaseViewModel : TestNodeViewModel +{ + private readonly TestsViewModel tests; - public TestCaseViewModel(TestsViewModel tests, ITestCase testCase) - { - this.tests = tests; - TestCase = testCase; - } + public ITestCase TestCase { get; } - public void RunTest() - { - tests.RunTests(this); - } + public TestCaseViewModel(TestsViewModel tests, ITestCase testCase) + { + this.tests = tests; + TestCase = testCase; + } - public override IEnumerable EnumerateTestCases() - { - yield return this; - } + public void RunTest() + { + tests.RunTests(this); + } - public override TestCaseViewModel LocateTestCase(ITestCase testCase) - { - return (testCase == this.TestCase) ? this : null; - } + public override IEnumerable EnumerateTestCases() + { + yield return this; + } - public override string DisplayName => TestCase.DisplayName; + public override TestCaseViewModel LocateTestCase(ITestCase testCase) + { + return (testCase == this.TestCase) ? this : null; } + + public override string DisplayName => TestCase.DisplayName; } diff --git a/sources/tests/xunit.runner.stride/ViewModels/TestGroupViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/TestGroupViewModel.cs index ccb3c4de9d..dfd18dd9ce 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/TestGroupViewModel.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/TestGroupViewModel.cs @@ -1,43 +1,42 @@ -using System.Collections.Generic; +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using ReactiveUI; -using Xunit; using Xunit.Abstractions; -namespace xunit.runner.stride.ViewModels +namespace xunit.runner.stride.ViewModels; + +public class TestGroupViewModel : TestNodeViewModel { - public class TestGroupViewModel : TestNodeViewModel - { - private readonly TestsViewModel tests; - private readonly string displayName; + private readonly TestsViewModel tests; + private readonly string displayName; - public List Children { get; } = new List(); + public List Children { get; } = []; - public TestGroupViewModel(TestsViewModel tests, string displayName) - { - this.tests = tests; - this.displayName = displayName; - } + public TestGroupViewModel(TestsViewModel tests, string displayName) + { + this.tests = tests; + this.displayName = displayName; + } - public override IEnumerable EnumerateTestCases() => Children.SelectMany(x => x.EnumerateTestCases()); + public override IEnumerable EnumerateTestCases() => Children.SelectMany(x => x.EnumerateTestCases()); - public void RunTest() - { - tests.RunTests(this); - } + public void RunTest() + { + tests.RunTests(this); + } - public override TestCaseViewModel LocateTestCase(ITestCase testCase) + public override TestCaseViewModel LocateTestCase(ITestCase testCase) + { + foreach (var child in Children) { - foreach (var child in Children) - { - var result = child.LocateTestCase(testCase); - if (result != null) - return result; - } - return null; + var result = child.LocateTestCase(testCase); + if (result != null) + return result; } - - public override string DisplayName => displayName; + return null; } + + public override string DisplayName => displayName; } diff --git a/sources/tests/xunit.runner.stride/ViewModels/TestNodeViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/TestNodeViewModel.cs index a2e2fb811c..072cbe91fa 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/TestNodeViewModel.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/TestNodeViewModel.cs @@ -1,5 +1,6 @@ -using ReactiveUI; -using System; +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + using System.Collections.Generic; using Xunit.Abstractions; @@ -15,21 +16,21 @@ public abstract class TestNodeViewModel : ViewModelBase public bool Running { get => running; - set => this.RaiseAndSetIfChanged(ref running, value); + set => SetProperty(ref running, value); } bool failed; public bool Failed { get => failed; - set => this.RaiseAndSetIfChanged(ref failed, value); + set => SetProperty(ref failed, value); } bool succeeded; public bool Succeeded { get => succeeded; - set => this.RaiseAndSetIfChanged(ref succeeded, value); + set => SetProperty(ref succeeded, value); } public abstract string DisplayName { get; } diff --git a/sources/tests/xunit.runner.stride/ViewModels/TestsViewModel.cs b/sources/tests/xunit.runner.stride/ViewModels/TestsViewModel.cs index b819ca0554..98b8b02004 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/TestsViewModel.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/TestsViewModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + using System; using System.Collections.Generic; using System.Linq; @@ -5,123 +8,121 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Threading; -using ReactiveUI; using Xunit; -namespace xunit.runner.stride.ViewModels +namespace xunit.runner.stride.ViewModels; + +public class TestsViewModel : ViewModelBase { - public class TestsViewModel : ViewModelBase + private XunitFrontController Controller { get; } + + public TestsViewModel() { - private XunitFrontController Controller { get; } + var assemblyFileName = Assembly.GetEntryAssembly().Location; - public TestsViewModel() - { - var assemblyFileName = Assembly.GetEntryAssembly().Location; + // TODO: currently we disable app domain otherwise GameTestBase.ForceInteractiveMode is not kept + // we should find another way to transfer this parameter + Controller = new XunitFrontController(AppDomainSupport.Denied, assemblyFileName); + var sink = new TestDiscoverySink(); + Controller.Find(true, sink, TestFrameworkOptions.ForDiscovery()); + sink.Finished.WaitOne(); - // TODO: currently we disable app domain otherwise GameTestBase.ForceInteractiveMode is not kept - // we should find another way to transfer this parameter - Controller = new XunitFrontController(AppDomainSupport.Denied, assemblyFileName); - var sink = new TestDiscoverySink(); - Controller.Find(true, sink, TestFrameworkOptions.ForDiscovery()); - sink.Finished.WaitOne(); - - var testAssemblyViewModel = new TestGroupViewModel(this, sink.TestCases.FirstOrDefault()?.TestMethod.TestClass.TestCollection.TestAssembly.Assembly.Name ?? "No tests were found"); - foreach (var testClass in sink.TestCases.GroupBy(x => x.TestMethod.TestClass)) + var testAssemblyViewModel = new TestGroupViewModel(this, sink.TestCases.FirstOrDefault()?.TestMethod.TestClass.TestCollection.TestAssembly.Assembly.Name ?? "No tests were found"); + foreach (var testClass in sink.TestCases.GroupBy(x => x.TestMethod.TestClass)) + { + var testClassViewModel = new TestGroupViewModel(this, testClass.Key.Class.Name); + testAssemblyViewModel.Children.Add(testClassViewModel); + foreach (var testCase in testClass) { - var testClassViewModel = new TestGroupViewModel(this, testClass.Key.Class.Name); - testAssemblyViewModel.Children.Add(testClassViewModel); - foreach (var testCase in testClass) - { - testClassViewModel.Children.Add(new TestCaseViewModel(this, testCase)); - } + testClassViewModel.Children.Add(new TestCaseViewModel(this, testCase)); } - TestCases.Add(testAssemblyViewModel); + } + TestCases.Add(testAssemblyViewModel); + } + + public async void RunTests(TestNodeViewModel testNodeViewModel) + { + var testCases = testNodeViewModel.EnumerateTestCases(); + var testCaseViewModels = new Dictionary(); + foreach (var testCase in testCases) + { + testCaseViewModels.Add(testCase.TestCase.UniqueID, testCase); } - public async void RunTests(TestNodeViewModel testNodeViewModel) + int testCasesFinished = 0; + await Task.Run(() => { - var testCases = testNodeViewModel.EnumerateTestCases(); - var testCaseViewModels = new Dictionary(); - foreach (var testCase in testCases) + // Reset progress + Dispatcher.UIThread.Post(() => { - testCaseViewModels.Add(testCase.TestCase.UniqueID, testCase); - } + TestCompletion = 0.0; + RunningTests = true; + }); - int testCasesFinished = 0; - await Task.Run(() => + var sink = new XSink { - // Reset progress - Dispatcher.UIThread.Post(() => - { - TestCompletion = 0.0; - RunningTests = true; - }); - - var sink = new XSink + HandleTestCaseStarting = args => { - HandleTestCaseStarting = args => + if (testCaseViewModels.TryGetValue(args.Message.TestCase.UniqueID, out var testCaseViewModel)) { - if (testCaseViewModels.TryGetValue(args.Message.TestCase.UniqueID, out var testCaseViewModel)) + Dispatcher.UIThread.Post(() => { - Dispatcher.UIThread.Post(() => - { - // Test status - testCaseViewModel.Running = true; - }); - } - }, - HandleTestCaseFinished = args => + // Test status + testCaseViewModel.Running = true; + }); + } + }, + HandleTestCaseFinished = args => + { + if (testCaseViewModels.TryGetValue(args.Message.TestCase.UniqueID, out var testCaseViewModel)) { - if (testCaseViewModels.TryGetValue(args.Message.TestCase.UniqueID, out var testCaseViewModel)) + Dispatcher.UIThread.Post(() => { - Dispatcher.UIThread.Post(() => - { - // Test status - testCaseViewModel.Failed = args.Message.TestsFailed > 0; - testCaseViewModel.Succeeded = args.Message.TestsFailed == 0; - testCaseViewModel.Running = false; - // Update progress - TestCompletion = ((double)Interlocked.Increment(ref testCasesFinished) / (double)testCaseViewModels.Count) * 100.0; - }); - } - }, - }; - Controller.RunTests(testCaseViewModels.Select(x => x.Value.TestCase).ToArray(), sink, TestFrameworkOptions.ForExecution()); - sink.Finished.WaitOne(); + // Test status + testCaseViewModel.Failed = args.Message.TestsFailed > 0; + testCaseViewModel.Succeeded = args.Message.TestsFailed == 0; + testCaseViewModel.Running = false; + // Update progress + TestCompletion = ((double)Interlocked.Increment(ref testCasesFinished) / (double)testCaseViewModels.Count) * 100.0; + }); + } + }, + }; + Controller.RunTests(testCaseViewModels.Select(x => x.Value.TestCase).ToArray(), sink, TestFrameworkOptions.ForExecution()); + sink.Finished.WaitOne(); - Dispatcher.UIThread.Post(() => - { - RunningTests = false; - }); + Dispatcher.UIThread.Post(() => + { + RunningTests = false; }); - } + }); + } - double testCompletion; - public double TestCompletion - { - get => testCompletion; - set => this.RaiseAndSetIfChanged(ref testCompletion, value); - } + double testCompletion; + public double TestCompletion + { + get => testCompletion; + set => SetProperty(ref testCompletion, value); + } - bool runningTests; - public bool RunningTests - { - get => runningTests; - set => this.RaiseAndSetIfChanged(ref runningTests, value); - } + bool runningTests; + public bool RunningTests + { + get => runningTests; + set => SetProperty(ref runningTests, value); + } - bool isInteractiveMode = false; - public bool IsInteractiveMode + bool isInteractiveMode = false; + public bool IsInteractiveMode + { + get => isInteractiveMode; + set { - get => isInteractiveMode; - set - { - this.RaiseAndSetIfChanged(ref isInteractiveMode, value); - SetInteractiveMode?.Invoke(isInteractiveMode); - } + SetProperty(ref isInteractiveMode, value); + SetInteractiveMode?.Invoke(isInteractiveMode); } - - public List TestCases { get; } = new List(); - public Action SetInteractiveMode { get; set; } } + + public List TestCases { get; } = []; + public Action SetInteractiveMode { get; set; } } diff --git a/sources/tests/xunit.runner.stride/ViewModels/ViewModelBase.cs b/sources/tests/xunit.runner.stride/ViewModels/ViewModelBase.cs index f594c5e2c1..bc0499a618 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/ViewModelBase.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/ViewModelBase.cs @@ -1,11 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Text; -using ReactiveUI; +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; namespace xunit.runner.stride.ViewModels { - public class ViewModelBase : ReactiveObject + public class ViewModelBase : ObservableObject { } } diff --git a/sources/tests/xunit.runner.stride/ViewModels/XSink.cs b/sources/tests/xunit.runner.stride/ViewModels/XSink.cs index bbb0635d2d..094dc66244 100644 --- a/sources/tests/xunit.runner.stride/ViewModels/XSink.cs +++ b/sources/tests/xunit.runner.stride/ViewModels/XSink.cs @@ -1,51 +1,53 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + using System; using System.Collections.Generic; using System.Threading; using Xunit; using Xunit.Abstractions; -namespace xunit.runner.stride.ViewModels +namespace xunit.runner.stride.ViewModels; + +public class XSink : IExecutionSink { - public class XSink : IExecutionSink + volatile int errors; + + public ManualResetEvent Finished { get; } = new ManualResetEvent(initialState: false); + + public ExecutionSummary ExecutionSummary { get; } = new ExecutionSummary(); + + public void Dispose() { - volatile int errors; - - public ManualResetEvent Finished { get; } = new ManualResetEvent(initialState: false); - - public ExecutionSummary ExecutionSummary { get; } = new ExecutionSummary(); - - public void Dispose() - { - } - - public bool OnMessageWithTypes(IMessageSinkMessage message, HashSet messageTypes) - { - Console.WriteLine($"{message.GetType().Name} ... {message}"); - - return message.Dispatch(messageTypes, HandleTestCaseFinished) - && message.Dispatch(messageTypes, HandleTestCaseStarting) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, HandleTestAssemblyFinished) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) - && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)); - } - - public MessageHandler HandleTestCaseFinished; - public MessageHandler HandleTestCaseStarting; - - void HandleTestAssemblyFinished(MessageHandlerArgs args) - { - ExecutionSummary.Total = args.Message.TestsRun; - ExecutionSummary.Failed = args.Message.TestsFailed; - ExecutionSummary.Skipped = args.Message.TestsSkipped; - ExecutionSummary.Time = args.Message.ExecutionTime; - ExecutionSummary.Errors = errors; - - Finished.Set(); - } + } + + public bool OnMessageWithTypes(IMessageSinkMessage message, HashSet messageTypes) + { + Console.WriteLine($"{message.GetType().Name} ... {message}"); + + return message.Dispatch(messageTypes, HandleTestCaseFinished) + && message.Dispatch(messageTypes, HandleTestCaseStarting) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, HandleTestAssemblyFinished) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)) + && message.Dispatch(messageTypes, args => Interlocked.Increment(ref errors)); + } + + public MessageHandler HandleTestCaseFinished; + public MessageHandler HandleTestCaseStarting; + + void HandleTestAssemblyFinished(MessageHandlerArgs args) + { + ExecutionSummary.Total = args.Message.TestsRun; + ExecutionSummary.Failed = args.Message.TestsFailed; + ExecutionSummary.Skipped = args.Message.TestsSkipped; + ExecutionSummary.Time = args.Message.ExecutionTime; + ExecutionSummary.Errors = errors; + + Finished.Set(); } } diff --git a/sources/tests/xunit.runner.stride/Views/MainView.axaml b/sources/tests/xunit.runner.stride/Views/MainView.axaml new file mode 100644 index 0000000000..8d38e5e7b3 --- /dev/null +++ b/sources/tests/xunit.runner.stride/Views/MainView.axaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/tests/xunit.runner.stride/Views/MainView.axaml.cs b/sources/tests/xunit.runner.stride/Views/MainView.axaml.cs new file mode 100644 index 0000000000..d7f0c75f50 --- /dev/null +++ b/sources/tests/xunit.runner.stride/Views/MainView.axaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; + +namespace xunit.runner.stride.Views; + +public partial class MainView : UserControl +{ + public MainView() + { + InitializeComponent(); + } +} diff --git a/sources/tests/xunit.runner.stride/Views/MainWindow.axaml b/sources/tests/xunit.runner.stride/Views/MainWindow.axaml new file mode 100644 index 0000000000..7ab02c9591 --- /dev/null +++ b/sources/tests/xunit.runner.stride/Views/MainWindow.axaml @@ -0,0 +1,11 @@ + + + diff --git a/sources/tests/xunit.runner.stride/Views/MainWindow.axaml.cs b/sources/tests/xunit.runner.stride/Views/MainWindow.axaml.cs new file mode 100644 index 0000000000..b5f1902e25 --- /dev/null +++ b/sources/tests/xunit.runner.stride/Views/MainWindow.axaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; + +namespace xunit.runner.stride.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} diff --git a/sources/tests/xunit.runner.stride/Views/MainWindow.xaml b/sources/tests/xunit.runner.stride/Views/MainWindow.xaml deleted file mode 100644 index 831e117a7d..0000000000 --- a/sources/tests/xunit.runner.stride/Views/MainWindow.xaml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/sources/tests/xunit.runner.stride/Views/MainWindow.xaml.cs b/sources/tests/xunit.runner.stride/Views/MainWindow.xaml.cs deleted file mode 100644 index 60c8abe25d..0000000000 --- a/sources/tests/xunit.runner.stride/Views/MainWindow.xaml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace xunit.runner.stride.Views -{ - public class MainWindow : Window - { - public MainWindow() - { - InitializeComponent(); - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } - } -} diff --git a/sources/tests/xunit.runner.stride/app.manifest b/sources/tests/xunit.runner.stride/app.manifest new file mode 100644 index 0000000000..670f3455f4 --- /dev/null +++ b/sources/tests/xunit.runner.stride/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/sources/tests/xunit.runner.stride/xunit.runner.stride.csproj b/sources/tests/xunit.runner.stride/xunit.runner.stride.csproj index 36ab6f4334..632fca68cc 100644 --- a/sources/tests/xunit.runner.stride/xunit.runner.stride.csproj +++ b/sources/tests/xunit.runner.stride/xunit.runner.stride.csproj @@ -1,29 +1,25 @@ - net8.0 false true + app.manifest + - - %(Filename) - - - Designer - - + + - + + + - - - + \ No newline at end of file diff --git a/sources/tools/Stride.Code.Tests/Stride.Code.Tests.csproj b/sources/tools/Stride.Code.Tests/Stride.Code.Tests.csproj index f3905d4f20..b9d21332f5 100644 --- a/sources/tools/Stride.Code.Tests/Stride.Code.Tests.csproj +++ b/sources/tools/Stride.Code.Tests/Stride.Code.Tests.csproj @@ -8,7 +8,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/tools/Stride.Core.ProjectTemplating.Tests/Stride.Core.ProjectTemplating.Tests.csproj b/sources/tools/Stride.Core.ProjectTemplating.Tests/Stride.Core.ProjectTemplating.Tests.csproj index 8fa179f7ce..af31622080 100644 --- a/sources/tools/Stride.Core.ProjectTemplating.Tests/Stride.Core.ProjectTemplating.Tests.csproj +++ b/sources/tools/Stride.Core.ProjectTemplating.Tests/Stride.Core.ProjectTemplating.Tests.csproj @@ -19,7 +19,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/sources/tools/Stride.VisualStudio.Package.Tests/Stride.VisualStudio.Package.Tests.csproj b/sources/tools/Stride.VisualStudio.Package.Tests/Stride.VisualStudio.Package.Tests.csproj index a5d6984a80..ebe6777195 100644 --- a/sources/tools/Stride.VisualStudio.Package.Tests/Stride.VisualStudio.Package.Tests.csproj +++ b/sources/tools/Stride.VisualStudio.Package.Tests/Stride.VisualStudio.Package.Tests.csproj @@ -1,6 +1,7 @@ + false false net472 win-x64 @@ -12,34 +13,34 @@ false - + True - + True - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + True - - + + True From 45e5fcaab3c0e9e9b851f79c2dd63072abf89746 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Thu, 11 Jan 2024 18:04:02 +0100 Subject: [PATCH 008/247] [Build] Fix VSPackage build (#2102) --- sources/Directory.Packages.props | 4 ++-- .../Stride.VisualStudio.Package.csproj | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sources/Directory.Packages.props b/sources/Directory.Packages.props index 2daa2be23d..c7acf09e24 100644 --- a/sources/Directory.Packages.props +++ b/sources/Directory.Packages.props @@ -92,8 +92,8 @@ - - + + diff --git a/sources/tools/Stride.VisualStudio.Package/Stride.VisualStudio.Package.csproj b/sources/tools/Stride.VisualStudio.Package/Stride.VisualStudio.Package.csproj index 9edae2717f..edd7d93367 100644 --- a/sources/tools/Stride.VisualStudio.Package/Stride.VisualStudio.Package.csproj +++ b/sources/tools/Stride.VisualStudio.Package/Stride.VisualStudio.Package.csproj @@ -151,6 +151,4 @@ - - From be7126f9d703a519d2ed4a4928aa3f90bbb810d7 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Thu, 11 Jan 2024 18:45:35 +0100 Subject: [PATCH 009/247] [Build] Require VC++ 2019 redist minimum (#2100) --- .../Stride.Core.Assets.CompilerApp.targets | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/sources/assets/Stride.Core.Assets.CompilerApp/build/Stride.Core.Assets.CompilerApp.targets b/sources/assets/Stride.Core.Assets.CompilerApp/build/Stride.Core.Assets.CompilerApp.targets index dfb804ed78..00b46cd699 100644 --- a/sources/assets/Stride.Core.Assets.CompilerApp/build/Stride.Core.Assets.CompilerApp.targets +++ b/sources/assets/Stride.Core.Assets.CompilerApp/build/Stride.Core.Assets.CompilerApp.targets @@ -1,23 +1,30 @@ - - + + - + - <_StrideVisualCRuntime2013 Include="Visual C++ Redistributable for Visual Studio 2013 x86"> - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DevDiv\VC\Servicing\12.0\RuntimeMinimum', 'Version', null, RegistryView.Registry32)) - 12.0.21005 - http://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x86.exe - - <_StrideVisualCRuntime2013 Include="Visual C++ Redistributable for Visual Studio 2013 x64"> - $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\DevDiv\VC\Servicing\12.0\RuntimeMinimum', 'Version', null, RegistryView.Registry64)) - 12.0.21005 - http://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x64.exe - - - <_StrideVisualCRuntime2013NotInstalled Include="@(_StrideVisualCRuntime2013)" Condition="'%(_StrideVisualCRuntime2013.Version)' == '' Or $([System.Version]::Parse('%(Version)').CompareTo($([System.Version]::Parse('%(_StrideVisualCRuntime2013.ExpectedVersion)')))) < 0" /> + + <_StrideVisualCRuntime2019 Include="Visual C++ Redistributable for Visual Studio 2019 x86"> + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\X86', 'Bld', null, RegistryView.Registry32)) + 27820 + https://aka.ms/vs/17/release/vc_redist.x86.exe + + <_StrideVisualCRuntime2019 Include="Visual C++ Redistributable for Visual Studio 2019 x64"> + $([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\X64', 'Bld', null, RegistryView.Registry64)) + 27820 + https://aka.ms/vs/17/release/vc_redist.x64.exe + + + <_StrideVisualCRuntime2019NotInstalled Include="@(_StrideVisualCRuntime2019)" Condition="'%(_StrideVisualCRuntime2019.Version)' == '' Or $([System.Int32]::Parse('%(Version)').CompareTo($([System.Int32]::Parse('%(_StrideVisualCRuntime2019.ExpectedVersion)')))) < 0" /> - + - $(MSBuildThisFileDirectory)..\lib\net8.0-windows7.0\Stride.Core.Assets.CompilerApp.exe - $(MSBuildThisFileDirectory)..\bin\$(Configuration)\net8.0-windows7.0\Stride.Core.Assets.CompilerApp.exe + $(MSBuildThisFileDirectory)..\lib\net8.0\Stride.Core.Assets.CompilerApp.exe + $(MSBuildThisFileDirectory)..\bin\$(Configuration)\net8.0\Stride.Core.Assets.CompilerApp.exe diff --git a/sources/assets/Stride.Core.Assets/Stride.Core.Assets.csproj b/sources/assets/Stride.Core.Assets/Stride.Core.Assets.csproj index 5e170b3aa6..b9b3f0cc2a 100644 --- a/sources/assets/Stride.Core.Assets/Stride.Core.Assets.csproj +++ b/sources/assets/Stride.Core.Assets/Stride.Core.Assets.csproj @@ -4,7 +4,7 @@ 8.0.30703 2.0 true - $(StrideEditorTargetFramework) + $(StrideXplatEditorTargetFramework) true --auto-module-initializer --serialization true diff --git a/sources/assets/Stride.Core.Packages/Stride.Core.Packages.csproj b/sources/assets/Stride.Core.Packages/Stride.Core.Packages.csproj index 624cddadd9..6cb4fb3a30 100644 --- a/sources/assets/Stride.Core.Packages/Stride.Core.Packages.csproj +++ b/sources/assets/Stride.Core.Packages/Stride.Core.Packages.csproj @@ -2,7 +2,7 @@ true - $(StrideEditorTargetFramework) + $(StrideXplatEditorTargetFramework) true --auto-module-initializer --serialization diff --git a/sources/buildengine/Stride.Core.BuildEngine.Common/Stride.Core.BuildEngine.Common.csproj b/sources/buildengine/Stride.Core.BuildEngine.Common/Stride.Core.BuildEngine.Common.csproj index f06b1cf6af..375e7834d5 100644 --- a/sources/buildengine/Stride.Core.BuildEngine.Common/Stride.Core.BuildEngine.Common.csproj +++ b/sources/buildengine/Stride.Core.BuildEngine.Common/Stride.Core.BuildEngine.Common.csproj @@ -5,7 +5,7 @@ 8.0.30703 2.0 true - $(StrideEditorTargetFramework) + $(StrideXplatEditorTargetFramework) --auto-module-initializer --serialization diff --git a/sources/core/Stride.Core.Design/Stride.Core.Design.csproj b/sources/core/Stride.Core.Design/Stride.Core.Design.csproj index 297036e219..939ae7e337 100644 --- a/sources/core/Stride.Core.Design/Stride.Core.Design.csproj +++ b/sources/core/Stride.Core.Design/Stride.Core.Design.csproj @@ -6,7 +6,7 @@ true true --auto-module-initializer --serialization - $(StrideEditorTargetFramework) + $(StrideXplatEditorTargetFramework) WindowsTools true diff --git a/sources/core/Stride.Core.Translation/Stride.Core.Translation.csproj b/sources/core/Stride.Core.Translation/Stride.Core.Translation.csproj index 72e37689a0..0d7f6340b2 100644 --- a/sources/core/Stride.Core.Translation/Stride.Core.Translation.csproj +++ b/sources/core/Stride.Core.Translation/Stride.Core.Translation.csproj @@ -4,7 +4,7 @@ 8.0.30703 2.0 true - $(StrideEditorTargetFramework) + $(StrideXplatEditorTargetFramework) WindowsTools true --auto-module-initializer --serialization diff --git a/sources/core/Stride.Core.Yaml/Stride.Core.Yaml.csproj b/sources/core/Stride.Core.Yaml/Stride.Core.Yaml.csproj index c24305b854..7bb9770799 100644 --- a/sources/core/Stride.Core.Yaml/Stride.Core.Yaml.csproj +++ b/sources/core/Stride.Core.Yaml/Stride.Core.Yaml.csproj @@ -5,7 +5,7 @@ 2.0 true false - net8.0 + $(StrideXplatEditorTargetFramework) WindowsTools diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs index 33d0e61ce7..651d0f98cb 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeHierarchyEditorViewModel.cs @@ -456,7 +456,7 @@ protected async Task PasteIntoItems([NotNull] [ItemNotNull] IEnumerable().MessageBox(error, MessageBoxButton.OK, MessageBoxImage.Information); + await ServiceProvider.Get().MessageBoxAsync(error, MessageBoxButton.OK, MessageBoxImage.Information); return; } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeItemViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeItemViewModel.cs index 7980a72501..0fd6bc293f 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeItemViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/AssetCompositeGameEditor/ViewModels/AssetCompositeItemViewModel.cs @@ -9,8 +9,8 @@ using Stride.Core.Annotations; using Stride.Core.Extensions; using Stride.Core.Presentation.Collections; -using Stride.Core.Presentation.ViewModel; using Stride.Core.Quantum; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/AddAssetPolicies/AddEntityComponentFileAssetPolicy.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/AddAssetPolicies/AddEntityComponentFileAssetPolicy.cs index 1132844a76..2b9ae6f973 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/AddAssetPolicies/AddEntityComponentFileAssetPolicy.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/AddAssetPolicies/AddEntityComponentFileAssetPolicy.cs @@ -6,10 +6,10 @@ using Stride.Core.Assets; using Stride.Core.Assets.Editor.ViewModel; using Stride.Core.Annotations; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.ViewModel; using Stride.Assets.Scripts; using Stride.Engine; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorLightingViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorLightingViewModel.cs index 4ea9490fe3..21ad023387 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorLightingViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorLightingViewModel.cs @@ -12,12 +12,12 @@ using Stride.Core.Mathematics; using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Services; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Engine; using Stride.Graphics; using Stride.Rendering.LightProbes; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { @@ -108,22 +108,17 @@ private async Task RebuildLightProbes(int bounces) private async Task CaptureCubemap() { - var dialog = ServiceProvider.Get().CreateFileSaveModalDialog(); - dialog.Filters.Add(new FileDialogFilter("DDS texture", "dds")); - dialog.DefaultExtension = "dds"; - if (editor.Session.SolutionPath != null) - dialog.InitialDirectory = editor.Session.SolutionPath.GetFullDirectory().ToWindowsPath(); - - var result = await dialog.ShowModal(); - if (result == DialogResult.Ok) + var filepath = await ServiceProvider.Get().SaveFilePickerAsync( + editor.Session.SolutionPath?.GetFullDirectory().ToWindowsPath(), + [new FilePickerFilter("DDS texture") { Patterns = ["*.dds"] }], + "dds"); + if (filepath is not null) { // Capture cubemap - using (var image = await CubemapService.CaptureCubemap()) - { - // And save it - using (var file = File.Create(dialog.FilePath)) - image.Save(file, ImageFileType.Dds); - } + using var image = await CubemapService.CaptureCubemap(); + // And save it + using var file = File.Create(filepath); + image.Save(file, ImageFileType.Dds); } } } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationGroupViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationGroupViewModel.cs index d72e08232a..f8142c6e6f 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationGroupViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationGroupViewModel.cs @@ -5,10 +5,10 @@ using Stride.Core.Annotations; using Stride.Core.Mathematics; using Stride.Core.Presentation.Extensions; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Navigation; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationViewModel.cs index 8849cc7075..2e0fb125ad 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorNavigationViewModel.cs @@ -12,12 +12,12 @@ using Stride.Core.Extensions; using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Commands; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Game; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.SceneEditor; using Stride.Navigation; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorRenderingViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorRenderingViewModel.cs index 4c4d0a8e16..29ddfd946b 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorRenderingViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EditorRenderingViewModel.cs @@ -5,12 +5,12 @@ using System.Collections.Generic; using System.Linq; using Stride.Core.Annotations; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Materials; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.SceneEditor; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityFolderViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityFolderViewModel.cs index d455ade7cd..ad0ba1d3c5 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityFolderViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityFolderViewModel.cs @@ -154,7 +154,7 @@ private bool CanUpdateName([CanBeNull] string newName) var canRename = Parent.Folders.All(x => x == this || !string.Equals(x.Name, newName, FolderCase)); if (!canRename) { - ServiceProvider.Get() + ServiceProvider.Get() .BlockingMessageBox(string.Format(Tr._p("Message", "Unable to rename the folder '{0}' to '{1}'. A folder with the same name already exists."), name, newName), MessageBoxButton.OK, MessageBoxImage.Information); } diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityGizmosViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityGizmosViewModel.cs index 0f08762080..691dcafde6 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityGizmosViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityGizmosViewModel.cs @@ -9,11 +9,11 @@ using Stride.Core.Mathematics; using Stride.Core.Reflection; using Stride.Core.Presentation.Commands; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.SceneEditor; using Stride.Engine; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs index b0db39d8d9..aad1f2a2ca 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityHierarchyEditorViewModel.cs @@ -496,7 +496,7 @@ protected override async Task Delete() confirmMessage = string.Format(Tr._p("Message", "Are you sure you want to delete these {0} entities?"), entitiesToDelete.Count); var checkedMessage = string.Format(Stride.Core.Assets.Editor.Settings.EditorSettings.AlwaysDeleteWithoutAsking, "entities"); var buttons = DialogHelper.CreateButtons(new[] { Tr._p("Button", "Delete"), Tr._p("Button", "Cancel") }, 1, 2); - var result = await ServiceProvider.Get().CheckedMessageBox(confirmMessage, false, checkedMessage, buttons, MessageBoxImage.Question); + var result = await ServiceProvider.Get().CheckedMessageBoxAsync(confirmMessage, false, checkedMessage, buttons, MessageBoxImage.Question); if (result.Result != 1) return; if (result.IsChecked == true) diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityTransformationViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityTransformationViewModel.cs index d83f112590..063f000c86 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityTransformationViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/EntityTransformationViewModel.cs @@ -6,12 +6,12 @@ using System.Linq; using Stride.Core.Annotations; using Stride.Core.Mathematics; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; using Stride.Assets.Presentation.SceneEditor; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/GizmoViewModel.cs b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/GizmoViewModel.cs index 530f825977..b91bc9bf3e 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/GizmoViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/ViewModels/GizmoViewModel.cs @@ -4,9 +4,9 @@ using System.Diagnostics; using Stride.Core; using Stride.Core.Annotations; -using Stride.Core.Presentation.ViewModel; using Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.Services; using Stride.Assets.Presentation.AssetEditors.GameEditor.Services; +using Stride.Core.Presentation.ViewModels; namespace Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.ViewModels { diff --git a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Views/EntityHierarchyEditorView.xaml b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Views/EntityHierarchyEditorView.xaml index 2e4ba924cd..c1bcf574f8 100644 --- a/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Views/EntityHierarchyEditorView.xaml +++ b/sources/editor/Stride.Assets.Presentation/AssetEditors/EntityHierarchyEditor/Views/EntityHierarchyEditorView.xaml @@ -15,6 +15,8 @@ xmlns:svm="clr-namespace:Stride.Assets.Presentation.AssetEditors.SceneEditor.ViewModels" xmlns:entityFactories="clr-namespace:Stride.Assets.Presentation.AssetEditors.EntityHierarchyEditor.EntityFactories" xmlns:views="clr-namespace:Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.Views" + xmlns:cmd="clr-namespace:Stride.Core.Presentation.Commands;assembly=Stride.Core.Presentation" + xmlns:core="clr-namespace:Stride.Core.Presentation.Core;assembly=Stride.Core.Presentation" xmlns:sd="http://schemas.stride3d.net/xaml/presentation" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300" d:DataContext="{d:DesignInstance viewModel:EntityHierarchyViewModel}"> @@ -90,18 +92,18 @@ - - - - + + + + - + - + @@ -136,18 +138,18 @@ - - - - + + + + - + - + @@ -158,9 +160,9 @@ - - - + + + @@ -288,7 +290,7 @@ @@ -51,7 +50,7 @@ IsEnabled="{Binding Parent, Converter={sd:MatchType}, ConverterParameter={x:Type uevm:UIElementViewModel}, FallbackValue={sd:False}}" Visibility="{Binding Converter={sd:Chained {sd:MatchType}, {sd:VisibleOrCollapsed}, Parameter1={x:Type uevm:PanelViewModel}}, FallbackValue={sd:Collapsed}}" d:DataContext="{d:DesignInstance uevm:PanelViewModel}" /> - @@ -128,14 +127,14 @@ - - - + + + - - + + - + @@ -160,7 +159,7 @@ @@ -194,27 +193,27 @@ - + - - - - + + + + + Visibility="{Binding Converter={sd:Chained {sd:ObjectToBool}, {sd:VisibleOrCollapsed}, Parameter2={sd:False}}, FallbackValue={sd:Visible}}"> - + @@ -305,7 +304,7 @@ - @@ -157,7 +156,7 @@ Margin="5,0" IsHitTestVisible="False" VerticalAlignment="Center"/> - + diff --git a/sources/editor/Stride.Assets.Presentation/View/GraphicsCompositorTemplates.xaml b/sources/editor/Stride.Assets.Presentation/View/GraphicsCompositorTemplates.xaml index 9b79d9cd15..4f9bb21314 100644 --- a/sources/editor/Stride.Assets.Presentation/View/GraphicsCompositorTemplates.xaml +++ b/sources/editor/Stride.Assets.Presentation/View/GraphicsCompositorTemplates.xaml @@ -1,7 +1,6 @@ - diff --git a/sources/editor/Stride.Assets.Presentation/View/SkeletonPropertyTemplates.xaml b/sources/editor/Stride.Assets.Presentation/View/SkeletonPropertyTemplates.xaml index 8221227f43..da651de282 100644 --- a/sources/editor/Stride.Assets.Presentation/View/SkeletonPropertyTemplates.xaml +++ b/sources/editor/Stride.Assets.Presentation/View/SkeletonPropertyTemplates.xaml @@ -3,8 +3,7 @@ xmlns:templateProviders="clr-namespace:Stride.Assets.Presentation.TemplateProviders" xmlns:sd="http://schemas.stride3d.net/xaml/presentation" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" - xmlns:qvm="clr-namespace:Stride.Core.Presentation.Quantum.ViewModels;assembly=Stride.Core.Presentation.Quantum" - xmlns:view="clr-namespace:Stride.Core.Presentation.Quantum.View;assembly=Stride.Core.Presentation.Quantum"> + xmlns:qvm="clr-namespace:Stride.Core.Presentation.Quantum.ViewModels;assembly=Stride.Core.Presentation.Quantum"> @@ -34,7 +33,7 @@ diff --git a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml index 8d25361abf..1eef046c4b 100644 --- a/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml +++ b/sources/editor/Stride.Assets.Presentation/View/UIPropertyTemplates.xaml @@ -1,6 +1,5 @@ - @@ -9,16 +8,16 @@ - + - + - + + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml index aef7fd2b49..b7617e15e8 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml @@ -39,7 +39,8 @@ + Padding="2" + DataContext="{Binding Session.ActiveProperties}" /> - Property Grid + x:Class="Stride.GameStudio.Avalonia.Views.PropertyGridView" + x:DataType="vm:PropertiesViewModel"> + + + + + + + + + + + + + From 55d3d87f264d45ae9ab5c30a75489a65da495256 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Thu, 26 Oct 2023 22:44:17 +0200 Subject: [PATCH 212/247] [Editor] Add Stride.Editor project --- build/Stride.Xplat.Avalonia.slnf | 1 + build/Stride.sln | 15 ++++++++++++++ sources/editor/Stride.Editor/README.md | 20 +++++++++++++++++++ .../editor/Stride.Editor/Stride.Editor.csproj | 18 +++++++++++++++++ 4 files changed, 54 insertions(+) create mode 100644 sources/editor/Stride.Editor/README.md create mode 100644 sources/editor/Stride.Editor/Stride.Editor.csproj diff --git a/build/Stride.Xplat.Avalonia.slnf b/build/Stride.Xplat.Avalonia.slnf index a9b17afe63..9377a5ed20 100644 --- a/build/Stride.Xplat.Avalonia.slnf +++ b/build/Stride.Xplat.Avalonia.slnf @@ -28,6 +28,7 @@ "..\\sources\\editor\\Stride.Core.MostRecentlyUsedFiles\\Stride.Core.MostRecentlyUsedFiles.shproj", "..\\sources\\editor\\Stride.Editor.CrashReport\\Stride.Editor.CrashReport.shproj", "..\\sources\\editor\\Stride.Editor.Wpf\\Stride.Editor.Wpf.csproj", + "..\\sources\\editor\\Stride.Editor\\Stride.Editor.csproj", "..\\sources\\editor\\Stride.GameStudio\\Stride.GameStudio.csproj", "..\\sources\\editor\\Stride.PrivacyPolicy\\Stride.PrivacyPolicy.shproj", "..\\sources\\editor\\Stride.Samples.Templates\\Stride.Samples.Templates.csproj", diff --git a/build/Stride.sln b/build/Stride.sln index 8aa4a8a0f2..a4e6014156 100644 --- a/build/Stride.sln +++ b/build/Stride.sln @@ -354,6 +354,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Assets.Editor", "..\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Assets.Editor.Avalonia", "..\sources\xplat-editor\Stride.Assets.Editor.Avalonia\Stride.Assets.Editor.Avalonia.csproj", "{6F5A1D7F-15A0-4439-86AE-7102478DAA8B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Editor", "..\sources\editor\Stride.Editor\Stride.Editor.csproj", "{CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1599,6 +1601,18 @@ Global {6F5A1D7F-15A0-4439-86AE-7102478DAA8B}.Release|Mixed Platforms.Build.0 = Debug|Any CPU {6F5A1D7F-15A0-4439-86AE-7102478DAA8B}.Release|Win32.ActiveCfg = Debug|Any CPU {6F5A1D7F-15A0-4439-86AE-7102478DAA8B}.Release|Win32.Build.0 = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Win32.ActiveCfg = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Debug|Win32.Build.0 = Debug|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Any CPU.Build.0 = Release|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Win32.ActiveCfg = Release|Any CPU + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Win32.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1734,6 +1748,7 @@ Global {C075170E-2DCB-41BE-9106-19923F655AA1} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} {B7FD8293-7F32-46FF-AAF6-36D2C594760D} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} {6F5A1D7F-15A0-4439-86AE-7102478DAA8B} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} + {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2} diff --git a/sources/editor/Stride.Editor/README.md b/sources/editor/Stride.Editor/README.md new file mode 100644 index 0000000000..001ac834c4 --- /dev/null +++ b/sources/editor/Stride.Editor/README.md @@ -0,0 +1,20 @@ +# Stride.Editor + +This project is the main project for the `Game` support in the editor. + +## Depencencies + +* It can references any Stride libraries, as long as their are cross-platform. +* It should be platform-agnostic as well as UI-agnostic. + In other words, no dependencies on platform (e.g. Windows), or UI library (e.g. Avalonia, WPF) are allowed. +* It will likely reference `Stride.Core.Assets.Editor` as well as Stride runtime libraries. + +## Implementations + +All `Game` supporting helpers and managers for the editors are here. + +## Notes + +* The goal is to be able to share that library with any application that wants to work with Stride runtime and manage instance of `Game`. +* It is Stride specific, but it can be used by other applications beside the GameStudio. + diff --git a/sources/editor/Stride.Editor/Stride.Editor.csproj b/sources/editor/Stride.Editor/Stride.Editor.csproj new file mode 100644 index 0000000000..38759009da --- /dev/null +++ b/sources/editor/Stride.Editor/Stride.Editor.csproj @@ -0,0 +1,18 @@ + + + + + $(StrideXplatEditorTargetFramework) + enable + latest + enable + + + + + Properties\SharedAssemblyInfo.cs + + + + + From 94232e5d5cb6a88cdacd02e9fabe695daac430a9 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Fri, 27 Oct 2023 20:39:01 +0200 Subject: [PATCH 213/247] [Editor] Add Stride.Editor.Avalonia project --- build/Stride.Xplat.Avalonia.slnf | 1 + build/Stride.sln | 15 +++++++++++++++ .../xplat-editor/Stride.Editor.Avalonia/README.md | 7 +++++++ .../Stride.Editor.Avalonia.csproj | 10 ++++++++++ 4 files changed, 33 insertions(+) create mode 100644 sources/xplat-editor/Stride.Editor.Avalonia/README.md create mode 100644 sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj diff --git a/build/Stride.Xplat.Avalonia.slnf b/build/Stride.Xplat.Avalonia.slnf index 9377a5ed20..2504d5533a 100644 --- a/build/Stride.Xplat.Avalonia.slnf +++ b/build/Stride.Xplat.Avalonia.slnf @@ -79,6 +79,7 @@ "..\\sources\\tools\\Stride.TextureConverter\\Stride.TextureConverter.csproj", "..\\sources\\xplat-editor\\Stride.Assets.Editor.Avalonia\\Stride.Assets.Editor.Avalonia.csproj", "..\\sources\\xplat-editor\\Stride.Core.Presentation.Avalonia\\Stride.Core.Presentation.Avalonia.csproj", + "..\\sources\\xplat-editor\\Stride.Editor.Avalonia\\Stride.Editor.Avalonia.csproj", "..\\sources\\xplat-editor\\Stride.GameStudio.Avalonia.Desktop\\Stride.GameStudio.Avalonia.Desktop.csproj", "..\\sources\\xplat-editor\\Stride.GameStudio.Avalonia\\Stride.GameStudio.Avalonia.csproj" ] diff --git a/build/Stride.sln b/build/Stride.sln index a4e6014156..3b5d8ecd7c 100644 --- a/build/Stride.sln +++ b/build/Stride.sln @@ -356,6 +356,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Assets.Editor.Avalon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Editor", "..\sources\editor\Stride.Editor\Stride.Editor.csproj", "{CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Editor.Avalonia", "..\sources\xplat-editor\Stride.Editor.Avalonia\Stride.Editor.Avalonia.csproj", "{2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1613,6 +1615,18 @@ Global {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Mixed Platforms.Build.0 = Release|Any CPU {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Win32.ActiveCfg = Release|Any CPU {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14}.Release|Win32.Build.0 = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Win32.ActiveCfg = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Debug|Win32.Build.0 = Debug|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Any CPU.Build.0 = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Win32.ActiveCfg = Release|Any CPU + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}.Release|Win32.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1749,6 +1763,7 @@ Global {B7FD8293-7F32-46FF-AAF6-36D2C594760D} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} {6F5A1D7F-15A0-4439-86AE-7102478DAA8B} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} + {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2} diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/README.md b/sources/xplat-editor/Stride.Editor.Avalonia/README.md new file mode 100644 index 0000000000..9f91a81b86 --- /dev/null +++ b/sources/xplat-editor/Stride.Editor.Avalonia/README.md @@ -0,0 +1,7 @@ +# Stride.Editor.Avalonia + +This project is similar to `Stride.Editor` but with Avalonia-specific implementations. + +## Notes + +* It is Stride specific, but it can be used by other applications beside the GameStudio, as long as they use Avalonia. diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj new file mode 100644 index 0000000000..95b3ef3a51 --- /dev/null +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file From 56e800aa00bb3da38f2c43cb51e06e30920aa844 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 28 Oct 2023 13:13:05 +0200 Subject: [PATCH 214/247] [Editor] Thumbnails --- .../Compiler/AssetCommand.cs | 3 - .../Services/AssetBuildUnit.cs | 94 +++++ .../Services/AssetBuilderService.cs | 115 ++++++ .../Services/AssetBuiltEventArgs.cs | 33 ++ .../Services/IBuildService.cs | 16 + .../Services/IDebugPage.cs | 9 + .../Services/IEditorDebugService.cs | 21 + .../Services/IThumbnailService.cs | 38 ++ .../Services/QueuePosition.cs | 19 + .../Services/ThumbnailCompletedArgs.cs | 19 + .../ViewModels/SessionViewModel.cs | 103 ++++- .../ViewModels/ThumbnailsViewModel.cs | 149 +++++++ .../SessionObjectPropertiesViewModel.cs | 2 +- .../ViewModels/AssetChangedEventArgs.cs | 24 ++ .../ViewModels/AssetDependenciesViewModel.cs | 268 +++++++++++++ .../ViewModels/AssetViewModel.cs | 52 ++- .../ViewModels/ISessionViewModel.cs | 16 + .../ViewModels/PackageViewModel.cs | 26 +- .../SessionStateChangedEventArgs.cs | 9 + .../ViewModels/ThumbnailData.cs | 50 +++ .../Build/DefaultAssetBuilderPriorities.cs | 12 + .../Build/EditorGameBuildUnit.cs | 33 ++ .../Build/GameSettingsProviderService.cs | 88 +++++ .../Build/GameStudioBuilderService.cs | 137 +++++++ .../Stride.Editor/Build/GameStudioDatabase.cs | 140 +++++++ .../Build/IGameSettingsAccessor.cs | 19 + .../Build/PrecompiledAssetBuildUnit.cs | 35 ++ .../Build/StrideShaderImporter.cs | 133 +++++++ .../Stride.Editor/Engine/EntityExtensions.cs | 222 +++++++++++ .../Preview/EditorGameCompilationContext.cs | 10 + .../Preview/PreviewCompilationContext.cs | 10 + .../Resources/DefaultThumbnails.cs | 34 ++ .../Resources/EmbeddedResourceReader.cs | 36 ++ .../Resources/ThumbnailDependencyError.png | 3 + .../Resources/ThumbnailDependencyWarning.png | 3 + .../Stride.Editor/Resources/appbar.box.png | 3 + .../Resources/appbar.checkmark.cross.png | 3 + .../Resources/appbar.page.delete.png | 3 + .../Resources/appbar.resource.png | 3 + .../editor/Stride.Editor/Stride.Editor.csproj | 17 + .../Thumbnails/BitmapThumbnailData.cs | 62 +++ .../CustomAssetThumbnailCompiler.cs | 55 +++ .../Thumbnails/GameStudioThumbnailService.cs | 320 +++++++++++++++ .../Thumbnails/IThumbnailCommand.cs | 17 + .../Thumbnails/IThumbnailCompiler.cs | 13 + .../Thumbnails/ResourceThumbnailData.cs | 47 +++ .../Thumbnails/StaticThumbnailCommand.cs | 95 +++++ .../Thumbnails/StaticThumbnailCompiler.cs | 37 ++ .../Thumbnails/StrideThumbnailCommand.cs | 187 +++++++++ .../Thumbnails/ThumbnailAssetBuildUnit.cs | 34 ++ .../Thumbnails/ThumbnailBuildHelper.cs | 143 +++++++ .../Thumbnails/ThumbnailBuildStep.cs | 49 +++ .../Thumbnails/ThumbnailBuiltEventArgs.cs | 65 ++++ .../Thumbnails/ThumbnailCommand.cs | 56 +++ .../Thumbnails/ThumbnailCommandParameters.cs | 38 ++ .../Thumbnails/ThumbnailCompilationContext.cs | 9 + .../Thumbnails/ThumbnailCompilerBase.cs | 197 ++++++++++ .../Thumbnails/ThumbnailCompilerContext.cs | 111 ++++++ .../Thumbnails/ThumbnailFromEntityCommand.cs | 177 +++++++++ .../ThumbnailFromSpriteBatchCommand.cs | 93 +++++ .../Thumbnails/ThumbnailFromTextureCommand.cs | 103 +++++ .../Thumbnails/ThumbnailGenerator.cs | 364 ++++++++++++++++++ .../Thumbnails/ThumbnailListCompiler.cs | 77 ++++ .../Properties/AssemblyInfo.cs | 1 + .../Stride.Assets.Editor.Avalonia.csproj | 1 + .../Converters/StrideImage.cs | 45 +++ .../Views/AssetExplorerView.axaml | 3 + 67 files changed, 4390 insertions(+), 19 deletions(-) create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/AssetBuildUnit.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/AssetBuilderService.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/AssetBuiltEventArgs.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/IBuildService.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/IThumbnailService.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/QueuePosition.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/ThumbnailCompletedArgs.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/ThumbnailsViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetChangedEventArgs.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionStateChangedEventArgs.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/ThumbnailData.cs create mode 100644 sources/editor/Stride.Editor/Build/DefaultAssetBuilderPriorities.cs create mode 100644 sources/editor/Stride.Editor/Build/EditorGameBuildUnit.cs create mode 100644 sources/editor/Stride.Editor/Build/GameSettingsProviderService.cs create mode 100644 sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs create mode 100644 sources/editor/Stride.Editor/Build/GameStudioDatabase.cs create mode 100644 sources/editor/Stride.Editor/Build/IGameSettingsAccessor.cs create mode 100644 sources/editor/Stride.Editor/Build/PrecompiledAssetBuildUnit.cs create mode 100644 sources/editor/Stride.Editor/Build/StrideShaderImporter.cs create mode 100644 sources/editor/Stride.Editor/Engine/EntityExtensions.cs create mode 100644 sources/editor/Stride.Editor/Preview/EditorGameCompilationContext.cs create mode 100644 sources/editor/Stride.Editor/Preview/PreviewCompilationContext.cs create mode 100644 sources/editor/Stride.Editor/Resources/DefaultThumbnails.cs create mode 100644 sources/editor/Stride.Editor/Resources/EmbeddedResourceReader.cs create mode 100644 sources/editor/Stride.Editor/Resources/ThumbnailDependencyError.png create mode 100644 sources/editor/Stride.Editor/Resources/ThumbnailDependencyWarning.png create mode 100644 sources/editor/Stride.Editor/Resources/appbar.box.png create mode 100644 sources/editor/Stride.Editor/Resources/appbar.checkmark.cross.png create mode 100644 sources/editor/Stride.Editor/Resources/appbar.page.delete.png create mode 100644 sources/editor/Stride.Editor/Resources/appbar.resource.png create mode 100644 sources/editor/Stride.Editor/Thumbnails/BitmapThumbnailData.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/CustomAssetThumbnailCompiler.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/GameStudioThumbnailService.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/IThumbnailCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/IThumbnailCompiler.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ResourceThumbnailData.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCompiler.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/StrideThumbnailCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailAssetBuildUnit.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildHelper.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildStep.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailBuiltEventArgs.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailCommandParameters.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilationContext.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerBase.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerContext.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailFromEntityCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailFromSpriteBatchCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailFromTextureCommand.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailGenerator.cs create mode 100644 sources/editor/Stride.Editor/Thumbnails/ThumbnailListCompiler.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Converters/StrideImage.cs diff --git a/sources/assets/Stride.Core.Assets/Compiler/AssetCommand.cs b/sources/assets/Stride.Core.Assets/Compiler/AssetCommand.cs index 0218b941c6..928e41ab2d 100644 --- a/sources/assets/Stride.Core.Assets/Compiler/AssetCommand.cs +++ b/sources/assets/Stride.Core.Assets/Compiler/AssetCommand.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; using Stride.Core.BuildEngine; using Stride.Core.Serialization; diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuildUnit.cs b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuildUnit.cs new file mode 100644 index 0000000000..391292da11 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuildUnit.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.BuildEngine; +using Stride.Core.Serialization.Contents; + +namespace Stride.Core.Assets.Editor.Services; + +public struct AssetBuildUnitIdentifier +{ + public static AssetBuildUnitIdentifier Default = new(Guid.Empty, AssetId.Empty); + + public AssetBuildUnitIdentifier(Guid contextIdentifier, AssetId assetIdentifier) + { + ContextIdentifier = contextIdentifier; + AssetIdentifier = assetIdentifier; + } + + public Guid ContextIdentifier { get; } + + public AssetId AssetIdentifier { get; } +} + +public abstract class AssetBuildUnit : IComparable +{ + private readonly TaskCompletionSource taskCompletionSource = new(); + private ListBuildStep buildStep; // FIXME nullability + + protected AssetBuildUnit(AssetBuildUnitIdentifier identifier) + { + Identifier = identifier; + } + + public int PriorityMajor { get; set; } + + public int PriorityMinor { get; set; } + + public AssetBuildUnitIdentifier Identifier { get; private set; } + + public bool Processed => buildStep.Processed; + + public bool Succeeded => buildStep.Succeeded; + + public bool Failed => buildStep.Failed; + + public IReadOnlyDictionary? OutputObjects => buildStep.OutputObjects; + + public ListBuildStep? GetBuildStep() + { + try + { + buildStep = Prepare(); + } + catch (Exception) + { + // TODO: properly log errors + //Builder.Logger.Error("An exception was triggered during the compilation of the preview items '{0}':\n" + e.Message, AssetItem.Location); + return null; + } + if (buildStep != null) + { + buildStep.StepProcessed += StepProcessed; + } + return buildStep; + } + + public async Task Wait() + { + return await taskCompletionSource.Task; + } + + protected abstract ListBuildStep Prepare(); + + protected virtual void PostBuild() + { + // Intentionally does nothing by default. + } + + private void StepProcessed(object? sender, BuildStepEventArgs e) + { + e.Step.StepProcessed -= StepProcessed; + PostBuild(); + taskCompletionSource.SetResult(e.Step.Status); + } + + public int CompareTo(AssetBuildUnit other) + { + var priorityMajorDiff = PriorityMajor.CompareTo(other.PriorityMajor); + if (priorityMajorDiff != 0) + return priorityMajorDiff; + + return PriorityMinor.CompareTo(other.PriorityMinor); + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuilderService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuilderService.cs new file mode 100644 index 0000000000..c2cf9b8810 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuilderService.cs @@ -0,0 +1,115 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Compiler; +using Stride.Core.BuildEngine; +using Stride.Core.Collections; +using Stride.Core.Diagnostics; +using Stride.Core.Mathematics; + +namespace Stride.Core.Assets.Editor.Services; + +public class AssetBuilderService : IBuildService, IDisposable +{ + private const string IndexName = "AssetBuilderServiceIndex"; + + private readonly object queueLock = new(); + + private readonly DynamicBuilder builder; + private readonly PriorityNodeQueue queue = new(); + + // TODO: this is temporary until we have thread local databases (and a better solution for databases used in standard tasks) + public static readonly object OutOfMicrothreadDatabaseLock = new(); + + public AssetBuilderService(string buildDirectory) + { + // We want at least 2 threads, since one will be used for DynamicBuildStep (which is a special blocking step) + var processorCount = Environment.ProcessorCount; + var threadCount = MathUtil.Clamp(3*processorCount/4, 2, processorCount - 1); + + // Mount database (otherwise it will be mounted by DynamicBuilder thread, and it might happen too late) + Builder.OpenObjectDatabase(buildDirectory, IndexName); + + var builderInstance = new Builder(GlobalLogger.GetLogger("AssetBuilderService"), buildDirectory, IndexName) + { + BuilderName = "AssetBuilderService Builder", + ThreadCount = threadCount, + }; + builder = new DynamicBuilder(builderInstance, new AnonymousBuildStepProvider(GetNextBuildStep), "Asset Builder service thread."); + builder.Start(); + } + + public event EventHandler AssetBuilt; + + public virtual void Dispose() + { + builder.Dispose(); + } + + private BuildStep? GetNextBuildStep(int maxPriority) + { + while (true) + { + AssetBuildUnit unit; + lock (queueLock) + { + if (queue.Empty) + { + return null; + } + unit = queue.Dequeue(); + } + + // Check that priority is good enough + if (unit.PriorityMajor > maxPriority) + return null; + + var buildStep = unit.GetBuildStep(); + + // If this build step couldn't be built, let's find another one + if (buildStep == null) + continue; + + // Forward priority to build engine (still very coarse, but should help) + buildStep.Priority = unit.PriorityMajor; + + foreach (var step in buildStep.EnumerateRecursively()) + { + if (step is AssetBuildStep assetStep) + { + assetStep.Priority = unit.PriorityMajor; + assetStep.StepProcessed += (s, e) => NotifyAssetBuilt(assetStep.AssetItem, assetStep.Logger); + } + } + + return buildStep; + } + } + + public PriorityQueueNode PushBuildUnit(AssetBuildUnit unit) + { + PriorityQueueNode result; + + lock (queueLock) + { + result = queue.Enqueue(unit); + } + + builder.NotifyBuildStepAvailable(); + + return result; + } + + public void RemoveBuildUnit(PriorityQueueNode node) + { + lock (queueLock) + { + queue.Remove(node); + } + } + + private void NotifyAssetBuilt(AssetItem assetItem, LoggerResult buildLog) + { + AssetBuilt?.Invoke(this, new AssetBuiltEventArgs(assetItem, buildLog)); + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuiltEventArgs.cs b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuiltEventArgs.cs new file mode 100644 index 0000000000..1afd01768d --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/AssetBuiltEventArgs.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Diagnostics; + +namespace Stride.Core.Assets.Editor.Services; + +/// +/// This class represents the argument of the event. +/// +public class AssetBuiltEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The asset item that has been built. + /// The log of the build. + public AssetBuiltEventArgs(AssetItem assetItem, LoggerResult buildLog) + { + AssetItem = assetItem; + BuildLog = buildLog; + } + + /// + /// Gets the asset item that has been built. + /// + public AssetItem AssetItem { get; private set; } + + /// + /// Gets the log of the build. + /// + public LoggerResult BuildLog { get; set; } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IBuildService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IBuildService.cs new file mode 100644 index 0000000000..f15dcfd920 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IBuildService.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System; + +namespace Stride.Core.Assets.Editor.Services; + +/// +/// This interface represents a service that build assets. +/// +public interface IBuildService +{ + /// + /// Raised when an asset has been built. + /// + event EventHandler AssetBuilt; +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs new file mode 100644 index 0000000000..5def190c8c --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Editor.Services; + +public interface IDebugPage +{ + string Title { get; set; } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs new file mode 100644 index 0000000000..3dfc1f44c1 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Services; + +namespace Stride.Core.Assets.Editor.Services; + +public interface IEditorDebugService +{ + IDebugPage CreateLogDebugPage(Logger logger, string title, bool register = true); + + IDebugPage CreateUndoRedoDebugPage(IUndoRedoService service, string title, bool register = true); + + IDebugPage CreateAssetNodesDebugPage(ISessionViewModel session, string title, bool register = true); + + void RegisterDebugPage(IDebugPage page); + + void UnregisterDebugPage(IDebugPage page); +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IThumbnailService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IThumbnailService.cs new file mode 100644 index 0000000000..4e2c19fe05 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IThumbnailService.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Editor.Services; + +/// +/// An interface that is capable of enqueuing thumbnail compilation orders and notify thumbnail compilation completion. +/// +public interface IThumbnailService : IDisposable +{ + /// + /// Adds the given asset items to the queue. + /// + /// The asset items to add to the queue. + /// The position in the queue from which to start insertion. + /// If an asset item is already in the queue, it might be moved to upper priority if is . + void AddThumbnailAssetItems(IEnumerable assetItems, QueuePosition position); + + /// + /// Increases the priority of the thumbnail compilation of the given asset items, if they are in the queue. If an asset item is not in the queue, it is not added. + /// + /// The asset items to increase the priority. + /// This method is equivalent to with except that it won't add asset items that are not already in the queue. + void IncreaseThumbnailPriority(IEnumerable assetItems); + + /// + /// Indicates whether the given asset type has static thumbnails + /// + /// The asset type. + /// A type has static thumbnails if the thumbnail image does not depend on the asset properties. + /// True if the asset type has static thumbnails, False otherwise. + bool HasStaticThumbnail(Type assetType); + + /// + /// Raised when a thumbnail is successfully compiled. + /// + event EventHandler? ThumbnailCompleted; +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/QueuePosition.cs b/sources/editor/Stride.Core.Assets.Editor/Services/QueuePosition.cs new file mode 100644 index 0000000000..9ddf2fa497 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/QueuePosition.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +namespace Stride.Core.Assets.Editor.Services +{ + /// + /// Represents which position to insert items in a queue. + /// + public enum QueuePosition + { + /// + /// Inserts items in the first position. + /// + First, + /// + /// Inserts items in the last position. + /// + Last + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/ThumbnailCompletedArgs.cs b/sources/editor/Stride.Core.Assets.Editor/Services/ThumbnailCompletedArgs.cs new file mode 100644 index 0000000000..f70ff8c68a --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/ThumbnailCompletedArgs.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.Services +{ + public class ThumbnailCompletedArgs : EventArgs + { + public ThumbnailCompletedArgs(AssetId assetId, ThumbnailData data) + { + AssetId = assetId; + Data = data; + } + + public AssetId AssetId { get; private set; } + + public ThumbnailData Data { get; private set; } + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs index c4c45d7f5e..e515e05130 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System.Collections.Concurrent; +using Stride.Core.Assets.Analysis; using Stride.Core.Assets.Editor.Internal; using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Presentation.Components.Properties; @@ -21,6 +22,7 @@ public sealed class SessionViewModel : DispatcherViewModel, ISessionViewModel { private SessionObjectPropertiesViewModel activeProperties; private readonly ConcurrentDictionary assetIdMap = []; + private ProjectViewModel? currentProject; private readonly Dictionary packageMap = []; private readonly PackageSession session; @@ -42,17 +44,16 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi // Initialize the asset collection view model EditorCollection = new EditorCollectionViewModel(this); - ActiveProperties = AssetCollection.AssetViewProperties; + activeProperties = AssetCollection.AssetViewProperties; // Initialize commands EditSelectedContentCommand = new AnonymousCommand(serviceProvider, OnEditSelectedContent); // Create package view models - this.session.Projects.ForEach(x => - { - var package = CreateProjectViewModel(x, true); - AllPackages.Add(package); - }); + this.session.Projects.ForEach(x => CreateProjectViewModel(x, true)); + + // Initialize other sub view models + Thumbnails = new ThumbnailsViewModel(this); GraphContainer = new AssetPropertyGraphContainer(AssetNodeContainer); } @@ -68,25 +69,62 @@ public SessionObjectPropertiesViewModel ActiveProperties { if (SetValue(ref activeProperties, value)) { + // FIXME xplat-editor //ActiveAssetsChanged?.Invoke(this, new ActiveAssetsChangedArgs(value?.GetRelatedAssets().ToList())); } } } - public ObservableList AllPackages { get; } = []; + public IEnumerable AllAssets => AllPackages.SelectMany(x => x.Assets); + + public IEnumerable AllPackages => packageMap.Keys; public AssetCollectionViewModel AssetCollection { get; } public AssetNodeContainer AssetNodeContainer { get; } + + /// + /// Gets the current active project for build/startup operations. + /// + // TODO: this property should become cancellable to maintain action stack consistency! Undoing a "mark as root" operation after changing the current package wouldn't work. + public ProjectViewModel? CurrentProject + { + get => currentProject; + private set + { + var oldValue = currentProject; + //SetValueUncancellable(ref currentProject, value, () => UpdateCurrentProject(oldValue, value)); + SetValue(ref currentProject, value, () => UpdateCurrentProject(oldValue, value)); + } + } + + /// + /// Gets the dependency manager associated to this session. + /// + public IAssetDependencyManager DependencyManager => session.DependencyManager; public EditorCollectionViewModel EditorCollection { get; } public AssetPropertyGraphContainer GraphContainer { get; } + public ThumbnailsViewModel Thumbnails { get; } + public ICommandBase EditSelectedContentCommand { get; } internal Dictionary AssetViewModelTypes { get; } = []; + internal IUndoRedoService? UndoRedoService => ServiceProvider.TryGet(); + + /// + /// Raised when some assets are modified. + /// + public event EventHandler? AssetPropertiesChanged; + + /// + /// Raised when the session state changed (e.g. current package). + /// + public event EventHandler? SessionStateChanged; + /// public AssetViewModel? GetAssetById(AssetId id) { @@ -135,6 +173,16 @@ public SessionObjectPropertiesViewModel ActiveProperties return sessionViewModel; } + /// + public override void Destroy() + { + EnsureNotDestroyed(nameof(SessionViewModel)); + + Thumbnails.Destroy(); + + base.Destroy(); + } + /// public Type GetAssetViewModelType(AssetItem assetItem) { @@ -189,6 +237,47 @@ private void LoadAssetsFromPackages(CancellationToken token = default) package.LoadPackageInformation(token); } + + // This transaction is done to prevent action responding to undoRedoService.TransactionCompletion to occur during loading + using var transaction = UndoRedoService?.CreateTransaction(); + ProcessAddedPackages(AllPackages).Forget(); + } + + private async Task ProcessAddedPackages(IEnumerable packages) + { + var packageList = packages.ToList(); + // We must refresh asset bases after all packages have been added, because we might have cross-packages references here. + packageList.SelectMany(x => x.Assets).ForEach(x => x.Initialize()); + await AssetDependenciesViewModel.TriggerInitialReferenceBuild(this); + await Dispatcher.InvokeAsync(() => packageList.ForEach(x => Thumbnails.StartInitialBuild(x))); + } + + private void SetCurrentProject(object selectedItem) + { + if (selectedItem is not ProjectViewModel project) + { + // Editor.MessageBox(Resources.Strings.SessionViewModel.SelectExecutableAsCurrentProject, MessageBoxButton.OK, MessageBoxImage.Information); + return; + } + + CurrentProject = project; + AllAssets.ForEach(x => x.Dependencies.NotifyRootAssetChange(false)); + // FIXME xplat-editor + //SelectionIsRoot = ActiveAssetView.SelectedAssets.All(x => x.Dependencies.IsRoot); + } + + private void UpdateCurrentProject(ProjectViewModel? oldValue, ProjectViewModel? newValue) + { + //if (oldValue != null) + //{ + // oldValue.IsCurrentProject = false; + //} + //if (newValue != null) + //{ + // newValue.IsCurrentProject = true; + //} + //ToggleIsRootOnSelectedAssetCommand.IsEnabled = CurrentProject != null; + //UpdateSessionState(); } #region Commands diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/ThumbnailsViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/ThumbnailsViewModel.cs new file mode 100644 index 0000000000..311c8d3be0 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/ThumbnailsViewModel.cs @@ -0,0 +1,149 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Specialized; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Extensions; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.ViewModels; + +/// +/// This view model manages updating thumbnails of . +/// +public class ThumbnailsViewModel : DispatcherViewModel +{ + private readonly SessionViewModel session; + private readonly HashSet initialQueue = []; + private IThumbnailService? thumbnailService; + + /// + /// Initializes a new instance of the class. + /// + /// The session associated to this view model. + public ThumbnailsViewModel(SessionViewModel session) + : base(session.SafeArgument(nameof(session)).ServiceProvider) + { + this.session = session; + // FIXME xplat-editor + //session.AssetCollection.FilteredContent.CollectionChanged += VisibleAssetsChanged; + session.AssetCollection.SelectedContent.CollectionChanged += VisibleAssetsChanged; + session.AssetPropertiesChanged += AssetPropertiesChanged; + ServiceProvider.ServiceRegistered += ServiceRegistered; + } + + /// + public override void Destroy() + { + // FIXME xplat-editor + //session.AssetCollection.FilteredContent.CollectionChanged -= VisibleAssetsChanged; + session.AssetCollection.SelectedContent.CollectionChanged -= VisibleAssetsChanged; + session.AssetPropertiesChanged -= AssetPropertiesChanged; + ServiceProvider.ServiceRegistered -= ServiceRegistered; + + if (thumbnailService != null) + { + thumbnailService.ThumbnailCompleted -= ThumbnailCompleted; + thumbnailService.Dispose(); + } + base.Destroy(); + } + + /// + /// Increases the priority of thumbnail processing for the given assets, if they are queued for thumbnail processing. + /// This methods has no effect for assets that are not currently in the thumbnail processing queue. + /// + /// + public void IncreaseThumbnailPriority(IEnumerable assets) + { + if (thumbnailService != null) + { + var thumbnailsToRefresh = new HashSet(); + thumbnailsToRefresh.AddRange(assets.Select(x => x.AssetItem)); + thumbnailService.IncreaseThumbnailPriority(thumbnailsToRefresh); + } + } + + /// + /// Starts the first build of thumbnails from the given package. This method should be invoked only once per package, after it and its dependencies have been loaded. + /// + /// The package for which to build thumbnails. + internal void StartInitialBuild(PackageViewModel package) + { + if (thumbnailService == null) + { + // If the thumbnail service is not available yet, defer thumbnail build. + initialQueue.Add(package); + } + + // TODO: putting false here makes all thumbnails that are not visible to be incorrectly built + RefreshThumbnails(package.Assets, true); + } + + /// + /// Refreshes the thumbnails of the given assets. + /// + /// The assets for which to refresh thumbnails. + /// If true, the given assets will be put in the front of the thumbnail processing queue. + private void RefreshThumbnails(IEnumerable assets, bool firstPriority) + { + if (thumbnailService != null) + { + var assetItems = new HashSet(assets.Select(x => x.AssetItem)); + // We run this as a task to prevent dead lock + Task.Run(() => thumbnailService.AddThumbnailAssetItems(assetItems, firstPriority ? QueuePosition.First : QueuePosition.Last)); + } + } + + public void ForceRefreshThumbnails(IEnumerable assets) + { + if (thumbnailService != null) + { + var assetItems = new HashSet(assets.Select(x => x.AssetItem)); + // We run this as a task to prevent dead lock + Task.Run(() => thumbnailService.AddThumbnailAssetItems(assetItems, QueuePosition.First)); + } + } + + private void ServiceRegistered(object? sender, ServiceRegistrationEventArgs e) + { + if (e.Service is IThumbnailService service) + { + thumbnailService = service; + thumbnailService.ThumbnailCompleted += ThumbnailCompleted; + ServiceProvider.ServiceRegistered -= ServiceRegistered; + foreach (var package in initialQueue) + { + StartInitialBuild(package); + } + initialQueue.Clear(); + } + } + + private void VisibleAssetsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // FIXME xplat-editor + //IncreaseThumbnailPriority(session.AssetCollection.FilteredContent.OfType()); + IncreaseThumbnailPriority(session.AssetCollection.SelectedContent.OfType()); + } + + private void AssetPropertiesChanged(object? sender, AssetChangedEventArgs e) + { + var referencers = AssetViewModel.ComputeRecursiveReferencerAssets(e.Assets).ToList(); + RefreshThumbnails(e.Assets.Concat(referencers), true); + } + + private void ThumbnailCompleted(object? sender, ThumbnailCompletedArgs e) + { + var asset = session.GetAssetById(e.AssetId); + if (asset != null) + { + Dispatcher.InvokeAsync(async () => + { + asset.ThumbnailData = e.Data; + await e.Data.PrepareForPresentation(Dispatcher); + }); + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/Components/Properties/SessionObjectPropertiesViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/Components/Properties/SessionObjectPropertiesViewModel.cs index 795a5f81b2..221f2d56d7 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/Components/Properties/SessionObjectPropertiesViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/Components/Properties/SessionObjectPropertiesViewModel.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -//using Stride.Core.Assets.Editor.ViewModels; using Stride.Core.Assets.Presentation.Quantum.NodePresenters; using Stride.Core.Assets.Presentation.Quantum.ViewModels; using Stride.Core.Assets.Presentation.ViewModels; @@ -146,6 +145,7 @@ protected override void OnPropertyChanged(params string[] propertyNames) contextLock = true; if (Session.ActiveProperties != this) { + // FIXME xplat-editor //Session.ActiveProperties.ClearSelection(); } Session.ActiveProperties = this; diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetChangedEventArgs.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetChangedEventArgs.cs new file mode 100644 index 0000000000..3e8b20c8a7 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetChangedEventArgs.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Presentation.ViewModels; + +/// +/// Arguments of the event. +/// +public class AssetChangedEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The collection of assets that have changed. + public AssetChangedEventArgs(IReadOnlyCollection assets) + { + Assets = assets; + } + + /// + /// Gets the collection of assets that have changed. + /// + public IReadOnlyCollection Assets { get; } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs new file mode 100644 index 0000000000..21e692a098 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs @@ -0,0 +1,268 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Assets.Analysis; +using Stride.Core.Extensions; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public sealed class AssetDependenciesViewModel : DispatcherViewModel +{ + private static readonly HashSet DirtyDependencies = new(); + private static TaskCompletionSource? dependenciesUpdated; + private IReadOnlyCollection referencerAssets = new List(); + private IReadOnlyCollection referencedAssets = new List(); + private IReadOnlyCollection recursiveReferencerAssets = new List(); + private IReadOnlyCollection recursiveReferencedAssets = new List(); + + public AssetDependenciesViewModel(AssetViewModel asset, bool forcedRoot) + : base(asset.SafeArgument(nameof(asset)).ServiceProvider) + { + Asset = asset; + ToggleIsRootOnSelectedAssetCommand = new AnonymousCommand(ServiceProvider, () => IsRoot = !IsRoot); + ForcedRoot = forcedRoot; + DirtyDependencies.Add(asset); + } + + /// + /// Gets the asset related to this view model. + /// + public AssetViewModel Asset { get; } + + /// + /// Gets the session containing the related asset. + /// + public ISessionViewModel Session => Asset.Session; + + /// + /// Gets the collection of assets that directly references the related asset. + /// + /// This collection is updated asynchronously, however it is always up-to-date when is raised. + public IReadOnlyCollection ReferencerAssets { get { return referencerAssets; } private set { SetValue(ref referencerAssets, value); } } + + /// + /// Gets the collection of assets directly referenced by the related asset. + /// + /// This collection is updated asynchronously, however it is always up-to-date when is raised. + public IReadOnlyCollection ReferencedAssets { get { return referencedAssets; } private set { SetValue(ref referencedAssets, value); } } + + /// + /// Gets the collection of assets that references the related asset directly or indirectly. + /// + /// This collection is updated asynchronously, however it is always up-to-date when is raised. + public IReadOnlyCollection RecursiveReferencerAssets { get { return recursiveReferencerAssets; } private set { SetValue(ref recursiveReferencerAssets, value); } } + + /// + /// Gets the collection of assets referenced by the related asset directly or indirectly. + /// + /// This collection is updated asynchronously, however it is always up-to-date when is raised. + public IReadOnlyCollection RecursiveReferencedAssets { get { return recursiveReferencedAssets; } private set { SetValue(ref recursiveReferencedAssets, value); } } + + /// + /// Gets whether this asset and all its references will be compiled. + /// + public bool IsRoot + { + // FIXME xplat-editor + get { return /*!Asset.IsDeleted &&*/ (Session.CurrentProject?.IsInScope(Asset) ?? false) && (ForcedRoot || (Session.CurrentProject?.RootAssets.Contains(Asset) ?? false)); } + set + { + if ((Session.CurrentProject?.IsInScope(Asset) ?? false) && !ForcedRoot) + { + if (value) + Session.CurrentProject.RootAssets.Add(Asset); + else + Session.CurrentProject.RootAssets.Remove(Asset); + } + } + } + + /// + /// Gets whether this asset will be compiled as a dependency of an asset that has set to true. + /// + // FIXME xplat-editor + public bool IsIndirectlyIncluded => !IsRoot /*&& !Asset.IsDeleted*/ && RecursiveReferencerAssets.Any(x => x.Dependencies.IsRoot); + + /// + /// Gets whether this asset will be excluded from compilation. + /// + public bool IsExcluded => !IsRoot && !IsIndirectlyIncluded; + + /// + /// Gets whether this asset is forced to be a root asset. + /// + public bool ForcedRoot { get; } + + /// + /// Gets a command that will toggle the property. + /// + public ICommandBase ToggleIsRootOnSelectedAssetCommand { get; } + + public static Task NotifyAssetChanged(ISessionViewModel session, AssetViewModel? asset) + { + lock (DirtyDependencies) + { + if (asset != null) + { + DirtyDependencies.Add(asset); + } + + // If a task of updating dependencies is already running, then we should return it - this asset will be included into it. + var task = dependenciesUpdated; + if (task != null) + return task.Task; + + dependenciesUpdated = new TaskCompletionSource(); + } + + // Trigger the update for the next dispatcher frame + return session.Dispatcher.InvokeAsync(() => UpdateReferences(session)); + } + + public void NotifyRootAssetChange(bool notifyReferencedAssets) + { + OnPropertyChanging(nameof(IsRoot), nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + OnPropertyChanged(nameof(IsRoot), nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + if (notifyReferencedAssets) + { + foreach (var referencedAsset in RecursiveReferencedAssets) + { + referencedAsset.Dependencies.OnPropertyChanging(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + referencedAsset.Dependencies.OnPropertyChanged(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + } + } + } + + public static Task TriggerInitialReferenceBuild(ISessionViewModel session) => NotifyAssetChanged(session, null); + + private static void UpdateReferences(ISessionViewModel session) + { + if (session == null) throw new ArgumentNullException(nameof(session)); + + var dirtyReferencers = new HashSet(); + var dirtyAssets = new HashSet(); + var dirtyReferenced = new HashSet(); + var dirtyDirectReferenced = new HashSet(); + var referencerAssets = new HashSet(); + var referencedAssets = new HashSet(); + TaskCompletionSource? tcs; + lock (DirtyDependencies) + { + + foreach (var asset in DirtyDependencies) + { + // Add dirty dependencies from the previous reference values + dirtyReferencers.AddRange(asset.Dependencies.RecursiveReferencerAssets); + dirtyReferenced.AddRange(asset.Dependencies.RecursiveReferencedAssets); + dirtyDirectReferenced.AddRange(asset.Dependencies.ReferencedAssets); + + referencerAssets.Clear(); + referencedAssets.Clear(); + + // FIXME xplat-editor + //if (!asset.IsDeleted) + if (true) + { + var dependencyManager = session.DependencyManager; + var dependencies = dependencyManager.ComputeDependencies(asset.AssetItem.Id, AssetDependencySearchOptions.In | AssetDependencySearchOptions.Out, ContentLinkType.Reference); // TODO: Change ContentLinkType.Reference to handle other types + if (dependencies != null) + { + dependencies.LinksIn.Select(x => session.GetAssetById(x.Item.Id)).NotNull().ForEach(x => referencerAssets.Add(x)); + dependencies.LinksOut.Select(x => session.GetAssetById(x.Item.Id)).NotNull().ForEach(x => referencedAssets.Add(x)); + } + dirtyAssets.Add(asset); + + // Add dirty dependencies from the updated reference values + dirtyReferencers.AddRange(referencerAssets); + dirtyReferenced.AddRange(referencedAssets); + dirtyDirectReferenced.AddRange(referencedAssets); + } + + // Note: the collections can be empty. This is especially needed when reimporting - we want to be sure that references are cleared. + asset.Dependencies.ReferencerAssets = referencerAssets.ToList(); + asset.Dependencies.ReferencedAssets = referencedAssets.ToList(); + } + DirtyDependencies.Clear(); + + // Clear the task now that we processed all the dirty dependencies + tcs = dependenciesUpdated; + dependenciesUpdated = null; + } + + dirtyDirectReferenced.ExceptWith(dirtyAssets); + // Add the dirty assets to the list of asset that needs to update recursive referenced assets. + dirtyReferencers.AddRange(dirtyAssets); + // Imported/undeleted assets must update their recursive referencers + dirtyReferenced.AddRange(dirtyAssets); + + // Update the referencers of the (previous/updated) directly referenced assets + foreach (var asset in dirtyDirectReferenced) + { + var dependencyManager = session.DependencyManager; + var dependencies = dependencyManager.ComputeDependencies(asset.AssetItem.Id, AssetDependencySearchOptions.In, ContentLinkType.Reference); // TODO: Change ContentLinkType.Reference to handle other types + referencerAssets.Clear(); + dependencies?.LinksIn.Select(x => session.GetAssetById(x.Item.Id)).NotNull().ForEach(x => referencerAssets.Add(x!)); + asset.Dependencies.ReferencerAssets = referencerAssets.ToList(); + } + + // Update recursive lists of referenced/referenced assets for assets affected by the changes + foreach (var asset in dirtyReferenced) + { + asset.Dependencies.OnPropertyChanging(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + asset.Dependencies.UpdateRecursiveReferencerAssets(); + asset.Dependencies.OnPropertyChanged(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + } + foreach (var asset in dirtyReferencers) + { + asset.Dependencies.UpdateRecursiveReferencedAssets(); + } + foreach (var asset in dirtyAssets) + { + asset.Dependencies.OnPropertyChanging(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + asset.Dependencies.OnPropertyChanged(nameof(IsIndirectlyIncluded), nameof(IsExcluded)); + } + + // Job is completed, notify anything awaiting on it + tcs?.SetResult(0); + } + + private void UpdateRecursiveReferencerAssets() + { + var result = new HashSet(); + var dependenciesToProcess = new Stack(); + dependenciesToProcess.Push(this); + while (dependenciesToProcess.Count > 0) + { + var dependencies = dependenciesToProcess.Pop(); + foreach (var referencer in dependencies.ReferencerAssets) + { + if (!result.Contains(referencer)) + { + result.Add(referencer); + dependenciesToProcess.Push(referencer.Dependencies); + } + } + } + RecursiveReferencerAssets = result.ToList(); + } + + private void UpdateRecursiveReferencedAssets() + { + var result = new HashSet(); + var dependenciesToProcess = new Stack(); + dependenciesToProcess.Push(this); + while (dependenciesToProcess.Count > 0) + { + var dependencies = dependenciesToProcess.Pop(); + foreach (var referenced in dependencies.ReferencedAssets.Where(x => !result.Contains(x))) + { + result.Add(referenced); + dependenciesToProcess.Push(referenced.Dependencies); + } + } + RecursiveReferencedAssets = result.ToList(); + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs index 84a68ed7d3..ef6257c681 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System.Reflection; using Stride.Core.Assets.Presentation.Components.Properties; using Stride.Core.Assets.Quantum; using Stride.Core.Presentation.Quantum; +using Stride.Core.Presentation.Services; using Stride.Core.Quantum; namespace Stride.Core.Assets.Presentation.ViewModels; @@ -31,12 +33,15 @@ public abstract class AssetViewModel : SessionObjectViewModel, IAssetPropertyPro private AssetItem assetItem; private DirectoryBaseViewModel directory; private string name; + private ThumbnailData thumbnailData; protected AssetViewModel(AssetItem assetItem, DirectoryBaseViewModel directory) : base(directory.Session) { this.assetItem = assetItem; this.directory = directory; + var forcedRoot = AssetType.GetCustomAttribute()?.AlwaysMarkAsRoot ?? false; + Dependencies = new AssetDependenciesViewModel(this, forcedRoot); name = Path.GetFileName(assetItem.Location); PropertyGraph = Session.GraphContainer.TryGetGraph(assetItem.Id); Session.RegisterAsset(this); @@ -59,6 +64,11 @@ public DirectoryBaseViewModel Directory get => directory; private set => SetValue(ref directory, value); } + + /// + /// Gets the dependencies of this asset. + /// + public AssetDependenciesViewModel Dependencies { get; } public override string Name { @@ -67,21 +77,45 @@ public override string Name } public AssetPropertyGraph? PropertyGraph { get; } - + /// - /// Gets the url of this asset. + /// The associated to this . /// - public string Url => AssetItem.Location; + public ThumbnailData ThumbnailData + { + get => thumbnailData; + set => SetValue(ref thumbnailData, value); + } /// /// Gets the display name of the type of this asset. /// public string TypeDisplayName { get { var desc = DisplayAttribute.GetDisplay(AssetType); return desc != null ? desc.Name : AssetType.Name; } } + + /// + /// Gets the url of this asset. + /// + public string Url => AssetItem.Location; protected Package Package => Directory.Package.Package; protected internal IAssetObjectNode? AssetRootNode => PropertyGraph?.RootNode; + + protected internal IUndoRedoService? UndoRedoService => ServiceProvider.TryGet(); + /// + /// Initializes this asset. This method is guaranteed to be called once every other assets are loaded in the session. + /// + /// + /// Inheriting classes should override it when necessary, provided that they also call the base implementation. + /// + public virtual void Initialize() + { + using var transaction = UndoRedoService?.CreateTransaction(); + PropertyGraph?.Initialize(); + UndoRedoService?.SetName(transaction!, $"Reconcile {Url} with its archetypes"); + } + protected virtual GraphNodePath GetPathToPropertiesRootNode() { return new GraphNodePath(AssetRootNode); @@ -96,6 +130,18 @@ protected virtual GraphNodePath GetPathToPropertiesRootNode() protected virtual bool ShouldConstructPropertyMember(IMemberNode member) => true; + public static HashSet ComputeRecursiveReferencerAssets(IEnumerable assets) + { + var result = new HashSet(assets.SelectMany(x => x.Dependencies.RecursiveReferencerAssets)); + return result; + } + + public static HashSet ComputeRecursiveReferencedAssets(IEnumerable assets) + { + var result = new HashSet(assets.SelectMany(x => x.Dependencies.RecursiveReferencedAssets)); + return result; + } + AssetViewModel IAssetPropertyProviderViewModel.RelatedAsset => this; bool IPropertyProviderViewModel.CanProvidePropertiesViewModel => true; //!IsDeleted && IsEditable; diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs index cfdf309fb1..b8fad8fa3a 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs @@ -1,8 +1,10 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets.Analysis; using Stride.Core.Assets.Presentation.Components.Properties; using Stride.Core.Assets.Quantum; +using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; namespace Stride.Core.Assets.Presentation.ViewModels; @@ -11,12 +13,26 @@ public interface ISessionViewModel { SessionObjectPropertiesViewModel ActiveProperties { get; set; } + IEnumerable AllAssets { get; } + + IEnumerable AllPackages { get; } + AssetNodeContainer AssetNodeContainer { get; } + ProjectViewModel? CurrentProject { get; } + + IAssetDependencyManager DependencyManager { get; } + + IDispatcherService Dispatcher { get; } + AssetPropertyGraphContainer GraphContainer { get; } IViewModelServiceProvider ServiceProvider { get; } + event EventHandler? AssetPropertiesChanged; + + event EventHandler? SessionStateChanged; + /// /// Gets an instance of the asset which as the given identifier, if available. /// diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs index 4f6758a982..5d08f2ebe7 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs @@ -14,8 +14,6 @@ public class PackageViewModel : SessionObjectViewModel, IComparable content = new(ComparePackageContent); - protected readonly ObservableSet rootAssets = new(); - public PackageViewModel(ISessionViewModel session, PackageContainer packageContainer) : base(session) { @@ -69,6 +67,11 @@ public UFile PackagePath get => Package.FullPath; set => SetValue(() => Package.FullPath = value); } + + /// + /// Gets the collection of root assets for this package. + /// + public ObservableSet RootAssets { get; } = new ObservableSet(); public UDirectory RootDirectory => Package.RootDirectory; @@ -119,6 +122,19 @@ public void LoadPackageInformation(CancellationToken token = default) GetOrCreateAssetDirectory(explicitDirectory); } } + + /// + /// Indicates whether the given asset in within the scope of this package, either by being part of this package or part of + /// one of its dependencies. + /// + /// The asset for which to check if it's in the scope of this package + /// True if the asset is in scope, False otherwise. + public bool IsInScope(AssetViewModel asset) + { + var assetPackage = asset.Directory.Package; + // Note: Would be better to switch to Dependencies view model as soon as we have FlattenedDependencies in those + return assetPackage == this || Package.Container.FlattenedDependencies.Any(x => x.Package == assetPackage.Package); + } private static int ComparePackageContent(ViewModelBase x, ViewModelBase y) { @@ -153,12 +169,12 @@ private AssetViewModel CreateAsset(AssetItem assetItem, DirectoryBaseViewModel d private void FillRootAssetCollection() { - rootAssets.Clear(); - rootAssets.AddRange(Package.RootAssets.Select(x => Session.GetAssetById(x.Id)).NotNull()!); + RootAssets.Clear(); + RootAssets.AddRange(Package.RootAssets.Select(x => Session.GetAssetById(x.Id)).NotNull()!); foreach (var dependency in PackageContainer.FlattenedDependencies) { if (dependency.Package != null) - rootAssets.AddRange(dependency.Package.RootAssets.Select(x => Session.GetAssetById(x.Id)).NotNull()!); + RootAssets.AddRange(dependency.Package.RootAssets.Select(x => Session.GetAssetById(x.Id)).NotNull()!); } } } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionStateChangedEventArgs.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionStateChangedEventArgs.cs new file mode 100644 index 0000000000..a1c4a23c86 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionStateChangedEventArgs.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Presentation.ViewModels; + +/// +/// Arguments of the event. +/// +public class SessionStateChangedEventArgs : EventArgs { } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ThumbnailData.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ThumbnailData.cs new file mode 100644 index 0000000000..2c99f7d009 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ThumbnailData.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Concurrent; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Storage; + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public abstract class ThumbnailData : ViewModelBase +{ + protected readonly ObjectId thumbnailId; + private static readonly ConcurrentDictionary> ComputingThumbnails = new(); + private object? presenter; + + protected ThumbnailData(ObjectId thumbnailId) + : base(ViewModelServiceProvider.NullServiceProvider) + { + this.thumbnailId = thumbnailId; + } + + public object? Presenter + { + get => presenter; + set => SetValue(ref presenter, value); + } + + public async Task PrepareForPresentation(IDispatcherService dispatcher) + { + var task = ComputingThumbnails.GetOrAdd(thumbnailId, k => Task.Run(BuildImageSource)); + + var result = await task; + dispatcher.Invoke(() => Presenter = result); + FreeBuildingResources(); + + ComputingThumbnails.TryRemove(thumbnailId, out _); + } + + /// + /// Fetches and prepare the image source instance to be displayed. + /// + /// + protected abstract object? BuildImageSource(); + + /// + /// Clears the resources required to build the image source. + /// + protected abstract void FreeBuildingResources(); +} diff --git a/sources/editor/Stride.Editor/Build/DefaultAssetBuilderPriorities.cs b/sources/editor/Stride.Editor/Build/DefaultAssetBuilderPriorities.cs new file mode 100644 index 0000000000..2b6ec91df7 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/DefaultAssetBuilderPriorities.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +namespace Stride.Editor.Build; + +public static class DefaultAssetBuilderPriorities +{ + // Some default priorities for AssetBuildUnit.PriorityMajor + // Later we will add Effect too + public static readonly int ScenePriority = -10; + public static readonly int PreviewPriority = 10; + public static readonly int ThumbnailPriority = 20; +} diff --git a/sources/editor/Stride.Editor/Build/EditorGameBuildUnit.cs b/sources/editor/Stride.Editor/Build/EditorGameBuildUnit.cs new file mode 100644 index 0000000000..dbca26d878 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/EditorGameBuildUnit.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; + +namespace Stride.Editor.Build; + +public class EditorGameBuildUnit : AssetBuildUnit +{ + private readonly AssetItem asset; + private readonly AssetCompilerContext compilerContext; + private readonly AssetDependenciesCompiler compiler; + + private static readonly Guid SceneBuildUnitContextId = Guid.NewGuid(); + + public EditorGameBuildUnit(AssetItem asset, AssetCompilerContext compilerContext, AssetDependenciesCompiler assetDependenciesCompiler) + : base(new AssetBuildUnitIdentifier(SceneBuildUnitContextId, asset.Id)) + { + this.asset = asset; + this.compilerContext = compilerContext; + compiler = assetDependenciesCompiler; + PriorityMajor = DefaultAssetBuilderPriorities.ScenePriority; + } + + protected override ListBuildStep Prepare() + { + var result = compiler.Prepare(compilerContext, asset); + return result.BuildSteps; + } +} diff --git a/sources/editor/Stride.Editor/Build/GameSettingsProviderService.cs b/sources/editor/Stride.Editor/Build/GameSettingsProviderService.cs new file mode 100644 index 0000000000..d4df559520 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/GameSettingsProviderService.cs @@ -0,0 +1,88 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Assets; +using Stride.Data; +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Editor.Build; + +/// +/// Arguments of the event. +/// +public class GameSettingsChangedEventArgs : EventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// A copy of the current game settings asset. + public GameSettingsChangedEventArgs(GameSettingsAsset gameSettings) + { + GameSettings = gameSettings; + } + + /// + /// Gets a copy of the game settings asset that has changed. + /// + public GameSettingsAsset GameSettings { get; } +} + +public class GameSettingsProviderService : IDisposable, IGameSettingsAccessor +{ + private readonly ISessionViewModel session; + private Package? currentPackage; + + public GameSettingsProviderService(ISessionViewModel session) + { + this.session = session; + session.SessionStateChanged += SessionOnSessionStateChanged; + session.AssetPropertiesChanged += AssetPropertyChanged; + UpdateCurrentGameSettings(); + } + + public GameSettingsAsset CurrentGameSettings { get; private set; } + + public event EventHandler? GameSettingsChanged; + + public void Dispose() + { + session.SessionStateChanged -= SessionOnSessionStateChanged; + session.AssetPropertiesChanged -= AssetPropertyChanged; + } + + /// + public T? GetConfiguration() where T : Configuration + { + var configuration = CurrentGameSettings?.TryGet(); + return configuration; + } + + private void AssetPropertyChanged(object? sender, AssetChangedEventArgs e) + { + if (e.Assets.Any(x => x.Asset == CurrentGameSettings)) + { + RaiseGameSettings(CurrentGameSettings); + } + } + + private void SessionOnSessionStateChanged(object? sender, SessionStateChangedEventArgs sessionStateChangedEventArgs) + { + if (session.CurrentProject?.Package != currentPackage) + { + UpdateCurrentGameSettings(); + } + } + + private void UpdateCurrentGameSettings() + { + currentPackage = session.CurrentProject?.Package; + CurrentGameSettings = currentPackage?.GetGameSettingsAssetOrDefault() ?? GameSettingsFactory.Create(); + RaiseGameSettings(CurrentGameSettings); + } + + private void RaiseGameSettings(GameSettingsAsset gameSettingsAsset) + { + GameSettingsChanged?.Invoke(this, new GameSettingsChangedEventArgs(AssetCloner.Clone(gameSettingsAsset))); + } +} diff --git a/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs b/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs new file mode 100644 index 0000000000..1f23dbc526 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs @@ -0,0 +1,137 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.BuildEngine; +using Stride.Core.IO; +using Stride.Shaders.Compiler; + +namespace Stride.Editor.Build; + +public class GameStudioBuilderService : AssetBuilderService +{ + public static string GlobalEffectLogPath; + private readonly ManualResetEvent shaderLoadedEvent = new(false); + private readonly EffectPriorityScheduler taskScheduler; + private readonly EffectCompilerBase effectCompiler; + // FIXME xplat-editor + //private readonly IDebugPage assetBuilderServiceDebugPage; + //private readonly IDebugPage effectCompilerServiceDebugPage; + private readonly bool createDebugTools; + private int currentJobToken = -1; + + public GameStudioBuilderService(ISessionViewModel sessionViewModel, GameSettingsProviderService settingsProvider, string buildDirectory, bool createDebugTools = true) + : base(buildDirectory) + { + this.createDebugTools = createDebugTools; + if (createDebugTools) + { + // FIXME xplat-editor + //assetBuilderServiceDebugPage = EditorDebugTools.CreateLogDebugPage(GlobalLogger.GetLogger("AssetBuilderService"), "AssetBuilderService"); + //effectCompilerServiceDebugPage = EditorDebugTools.CreateLogDebugPage(GlobalLogger.GetLogger("EffectCompilerCache"), "EffectCompilerCache"); + } + + Session = sessionViewModel ?? throw new ArgumentNullException(nameof(sessionViewModel)); + + var shaderImporter = new StrideShaderImporter(); + var shaderBuildSteps = shaderImporter.CreateSystemShaderBuildSteps(sessionViewModel); + shaderBuildSteps.StepProcessed += ShaderBuildStepsStepProcessed; + PushBuildUnit(new PrecompiledAssetBuildUnit(AssetBuildUnitIdentifier.Default, shaderBuildSteps, true)); + + Database = new GameStudioDatabase(this, settingsProvider); + + const string shaderBundleUrl = "/binary/editor/EditorShadersD3D11.bundle"; + if (VirtualFileSystem.FileExists(shaderBundleUrl)) + { + Builder.ObjectDatabase.BundleBackend.LoadBundleFromUrl("EditorShadersD3D11", Builder.ObjectDatabase.ContentIndexMap, shaderBundleUrl, true).Wait(); + } + + // Use a shared database for our shader system + // TODO: Shaders compiled on main thread won't actually be visible to MicroThread build engine (contentIndexMap are separate). + // It will still work and cache because EffectCompilerCache caches not only at the index map level, but also at the database level. + // Later, we probably want to have a GetSharedDatabase() allowing us to mutate it (or merging our results back with IndexFileCommand.AddToSharedGroup()), + // so that database created with MountDatabase also have all the newest shaders. + taskScheduler = new EffectPriorityScheduler(ThreadPriority.BelowNormal, Math.Max(1, Environment.ProcessorCount / 2)); + TaskSchedulerSelector taskSchedulerSelector = (mixinTree, compilerParameters) => taskScheduler.GetOrCreatePriorityGroup(compilerParameters?.TaskPriority ?? 0); + effectCompiler = (EffectCompilerBase)EffectCompilerFactory.CreateEffectCompiler(MicrothreadLocalDatabases.GetSharedDatabase(), taskSchedulerSelector: taskSchedulerSelector); + + StartPushNotificationsTask(); + } + + /// + /// Gets the session view model attached to this building service. + /// + /// The session view model. + public ISessionViewModel Session { get; private set; } + + public GameStudioDatabase Database { get; } + + /// + /// Gets the effect compiler. + /// + public IEffectCompiler EffectCompiler => effectCompiler; + + public string EffectLogPath => GlobalEffectLogPath; + + /// + /// Gets whether this instance of has been disposed. + /// + public bool IsDisposed { get; private set; } + + public override void Dispose() + { + base.Dispose(); + if (createDebugTools) + { + // FIXME xplat-editor + //EditorDebugTools.UnregisterDebugPage(assetBuilderServiceDebugPage); + //EditorDebugTools.UnregisterDebugPage(effectCompilerServiceDebugPage); + } + if (!IsDisposed) + { + IsDisposed = true; + } + } + + private void ShaderBuildStepsStepProcessed(object? sender, BuildStepEventArgs e) + { + shaderLoadedEvent.Set(); + } + + public void WaitForShaders() + { + // TODO: turn into task + shaderLoadedEvent.WaitOne(); + } + + private void StartPushNotificationsTask() + { + Task.Run(async () => + { + while (!IsDisposed) + { + await Task.Delay(500); + if (currentJobToken >= 0) + { + if (taskScheduler.QueuedTaskCount > 0) + { + // FIXME xplat-editor + //EditorViewModel.Instance.Status.NotifyBackgroundJobProgress(currentJobToken, taskScheduler.QueuedTaskCount, true); + } + else + { + // FIXME xplat-editor + //EditorViewModel.Instance.Status.NotifyBackgroundJobFinished(currentJobToken); + currentJobToken = -1; + } + } + else if (taskScheduler.QueuedTaskCount > 0) + { + // FIXME xplat-editor + //currentJobToken = EditorViewModel.Instance.Status.NotifyBackgroundJobStarted("Building effects ({0} in queue)", JobPriority.Editor); + } + } + }); + } +} diff --git a/sources/editor/Stride.Editor/Build/GameStudioDatabase.cs b/sources/editor/Stride.Editor/Build/GameStudioDatabase.cs new file mode 100644 index 0000000000..f9fb8436ae --- /dev/null +++ b/sources/editor/Stride.Editor/Build/GameStudioDatabase.cs @@ -0,0 +1,140 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Core.BuildEngine; +using Stride.Core; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.MicroThreading; +using Stride.Core.Serialization.Contents; +using Stride.Assets; +using Stride.Editor.Preview; +using Stride.Graphics; + +namespace Stride.Editor.Build; + +public class GameStudioDatabase : IDisposable +{ + private readonly Dictionary database = new(); + private readonly GameStudioBuilderService assetBuilderService; + private readonly GameSettingsProviderService settingsProvider; + internal readonly AssetCompilerContext CompilerContext = new() { CompilationContext = typeof(AssetCompilationContext) }; + private readonly MicroThreadLock databaseLock = new(); + private readonly GameSettingsAsset databaseGameSettings; + internal readonly AssetDependenciesCompiler AssetDependenciesCompiler = new(typeof(EditorGameCompilationContext)); + private bool isDisposed; + + public GameStudioDatabase(GameStudioBuilderService assetBuilderService, GameSettingsProviderService settingsProvider) + { + this.assetBuilderService = assetBuilderService; + this.settingsProvider = settingsProvider; + + CompilerContext.Platform = PlatformType.Windows; + + databaseGameSettings = GameSettingsFactory.Create(); + //// TODO: get the best available between 10 and 11 + //databaseGameSettings.GetOrCreate().DefaultGraphicsProfile = GraphicsProfile.Level_11_0; + CompilerContext.SetGameSettingsAsset(databaseGameSettings); + CompilerContext.CompilationContext = typeof(EditorGameCompilationContext); + + UpdateGameSettings(settingsProvider.CurrentGameSettings); + settingsProvider.GameSettingsChanged += OnGameSettingsChanged; + + ((AssetDependencyManager)assetBuilderService.Session.DependencyManager).AssetChanged += OnAssetChanged; + } + + private void OnAssetChanged(AssetItem sender, bool oldValue, bool newValue) + { + //if (newValue) + //{ + // AssetDependenciesCompiler.BuildDependencyManager.AssetChanged(sender); + //} + } + + public void Dispose() + { + isDisposed = true; + databaseLock.Dispose(); + settingsProvider.GameSettingsChanged -= OnGameSettingsChanged; + ((AssetDependencyManager)assetBuilderService.Session.DependencyManager).AssetChanged -= OnAssetChanged; + CompilerContext.Dispose(); + database.Clear(); + } + + public ILogger Logger { get; } + + public Task ReserveSyncLock() => databaseLock.ReserveSyncLock(); + + public Task LockAsync() => databaseLock.LockAsync(); + + public async Task MountInCurrentMicroThread() + { + if (isDisposed) throw new ObjectDisposedException(nameof(GameStudioDatabase)); + if (Scheduler.CurrentMicroThread == null) throw new InvalidOperationException("The database can only be mounted in a micro-thread."); + + var lockObject = await databaseLock.LockAsync(); + // Return immediately if the database was disposed when waiting for the lock + if (isDisposed) + return lockObject; + + MicrothreadLocalDatabases.MountDatabase(database.Yield()); + return lockObject; + } + + public async Task Build(AssetItem asset, BuildDependencyType dependencyType = BuildDependencyType.Runtime) + { + if (isDisposed) + throw new ObjectDisposedException(nameof(GameStudioDatabase)); + + var buildUnit = new EditorGameBuildUnit(asset, CompilerContext, AssetDependenciesCompiler); + + try + { + assetBuilderService.PushBuildUnit(buildUnit); + await buildUnit.Wait(); + } + catch (Exception e) + { + Logger?.Error($"An error occurred while building the scene: {e.Message}", e); + return; + } + + // Merge build result into the database + using ((await databaseLock.ReserveSyncLock()).Lock()) + { + if (isDisposed) + return; + + if (buildUnit.Failed) + { + // Build failed => unregister object + // 1. If it is first-time scene loading and one of sub-asset failed, it will be in this state, but we don't care + // since database will be empty at that point (it won't have any effect) + // 2. The second case (the one we actually care about) happens when reloading a recently rebuilt individual asset (i.e. material or texture), + // this will actually remove it from database + database.Remove(new ObjectUrl(UrlType.Content, asset.Location)); + } + + foreach (var outputObject in buildUnit.OutputObjects) + { + database[outputObject.Key] = outputObject.Value; + } + } + } + + private void OnGameSettingsChanged(object sender, GameSettingsChangedEventArgs e) + { + UpdateGameSettings(e.GameSettings); + } + + private void UpdateGameSettings(GameSettingsAsset currentGameSettings) + { + databaseGameSettings.GetOrCreate().RenderingMode = currentGameSettings.GetOrCreate().RenderingMode; + databaseGameSettings.GetOrCreate().ColorSpace = currentGameSettings.GetOrCreate().ColorSpace; + databaseGameSettings.GetOrCreate().Groups = currentGameSettings.GetOrDefault().Groups; + databaseGameSettings.GetOrCreate().DefaultGraphicsProfile = currentGameSettings.GetOrCreate().DefaultGraphicsProfile; + } +} diff --git a/sources/editor/Stride.Editor/Build/IGameSettingsAccessor.cs b/sources/editor/Stride.Editor/Build/IGameSettingsAccessor.cs new file mode 100644 index 0000000000..0d4811aec1 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/IGameSettingsAccessor.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Data; + +namespace Stride.Editor.Build; + +/// +/// Used to access the game settings in a read-only way +/// +public interface IGameSettingsAccessor +{ + /// + /// Gets a copy of the requested . Can be null. + /// + /// The requestted + /// If not null, it will filter the results giving priority to the specified profile + /// The requested or null if not found + T? GetConfiguration() where T : Configuration; +} diff --git a/sources/editor/Stride.Editor/Build/PrecompiledAssetBuildUnit.cs b/sources/editor/Stride.Editor/Build/PrecompiledAssetBuildUnit.cs new file mode 100644 index 0000000000..a35da2acdf --- /dev/null +++ b/sources/editor/Stride.Editor/Build/PrecompiledAssetBuildUnit.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; + +namespace Stride.Editor.Build; + +public class PrecompiledAssetBuildUnit : AssetBuildUnit +{ + private readonly ListBuildStep buildStep; + + private readonly bool mergeInCommonDatabase; + + public PrecompiledAssetBuildUnit(AssetBuildUnitIdentifier identifier, ListBuildStep buildStep, bool mergeInCommonDatabase = false) + : base(identifier) + { + this.buildStep = buildStep; + this.mergeInCommonDatabase = mergeInCommonDatabase; + } + + protected override ListBuildStep Prepare() + { + return buildStep; + } + + protected override void PostBuild() + { + base.PostBuild(); + if (mergeInCommonDatabase) + { + MicrothreadLocalDatabases.AddToSharedGroup(buildStep.OutputObjects); + } + } +} diff --git a/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs b/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs new file mode 100644 index 0000000000..2798421245 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs @@ -0,0 +1,133 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.BuildEngine; +using Stride.Assets.Effect; +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Editor.Build; + +public class StrideShaderImporter +{ + /// + /// The current session being processed + /// + private readonly HashSet systemProjectsLoaded = new(); + + private class UpdateImportShaderCacheBuildStep : BuildStep + { + private readonly HashSet cachedProject; + + private readonly List importedProjectIds; + + public UpdateImportShaderCacheBuildStep(HashSet cachedProject, List importedProjectIds) + { + this.cachedProject = cachedProject; + this.importedProjectIds = importedProjectIds; + } + + public override string Title + { + get { return "UpdateImportShaderCacheBuildStep"; } + } + + public override Task Execute(IExecuteContext executeContext, BuilderContext builderContext) + { + // check the status of the import build steps + if (((ListBuildStep)Parent).Steps.Any(s => s.Failed)) + return Task.FromResult(ResultStatus.Successful); + + // Mark System projects as loaded + foreach (var projectId in importedProjectIds) + cachedProject.Add(projectId); + + return Task.FromResult(ResultStatus.Successful); + } + + public override string ToString() + { + return Title; + } + } + + /// + /// Creates a build step that will build all shaders from system packages. + /// + /// The session used to retrieve currently used system packages. + /// A containing the steps to build all shaders from system packages. + public ListBuildStep CreateSystemShaderBuildSteps(ISessionViewModel session) + { + if (session == null) throw new ArgumentNullException(nameof(session)); + // Check if there are any new system projects to preload + // TODO: PDX-1251: For now, allow non-system project as well (which means they will be loaded only once at startup) + // Later, they should be imported depending on what project the currently previewed/built asset is + var systemPackages = session.AllPackages.Where(project => /*project.IsSystem &&*/ !systemProjectsLoaded.Contains(project.Package.Meta.Name)).ToList(); + if (systemPackages.Count == 0) + return null; + + var importShadersRootProject = new StandalonePackage(new Package()); + var importShadersProjectSession = new PackageSession(); + importShadersProjectSession.Projects.Add(importShadersRootProject); + + foreach (var package in systemPackages) + { + var mapPackage = new Package { FullPath = package.PackagePath }; + foreach (var asset in package.Assets) + { + if (typeof(EffectShaderAsset).IsAssignableFrom(asset.AssetType)) + mapPackage.Assets.Add(new AssetItem(asset.Url, asset.Asset) { SourceFolder = asset.AssetItem.SourceFolder, AlternativePath = asset.AssetItem.AlternativePath }); + } + + importShadersProjectSession.Projects.Add(new StandalonePackage(mapPackage)); + importShadersRootProject.FlattenedDependencies.Add(new Dependency(mapPackage)); + } + + // compile the fake project (create the build steps) + var assetProjectCompiler = new PackageCompiler(new PackageAssetEnumerator(importShadersRootProject.Package)); + var context = new AssetCompilerContext { CompilationContext = typeof(AssetCompilationContext) }; + var dependenciesCompileResult = assetProjectCompiler.Prepare(context); + context.Dispose(); + + var buildSteps = dependenciesCompileResult.BuildSteps; + buildSteps?.Add(new UpdateImportShaderCacheBuildStep(systemProjectsLoaded, systemPackages.Select(x => x.Package.Meta.Name).ToList())); + + return buildSteps; + } + + public ListBuildStep CreateUserShaderBuildSteps(ISessionViewModel session) + { + var packages = session.AllPackages.Where(project => !project.Package.IsSystem).ToList(); + if (packages.Count == 0) + return null; + + var importShadersRootProject = new StandalonePackage(new Package()); + var importShadersProjectSession = new PackageSession(); + importShadersProjectSession.Projects.Add(importShadersRootProject); + + foreach (var package in packages) + { + var mapPackage = new Package { FullPath = package.PackagePath }; + foreach (var asset in package.Assets) + { + if (typeof(EffectShaderAsset).IsAssignableFrom(asset.AssetType)) + { + mapPackage.Assets.Add(new AssetItem(asset.Url, asset.Asset) { SourceFolder = asset.AssetItem.SourceFolder, AlternativePath = asset.AssetItem.AlternativePath }); + } + } + + importShadersProjectSession.Projects.Add(new StandalonePackage(mapPackage)); + importShadersRootProject.FlattenedDependencies.Add(new Dependency(mapPackage)); + } + + // compile the fake project (create the build steps) + var assetProjectCompiler = new PackageCompiler(new PackageAssetEnumerator(importShadersRootProject.Package)); + var dependenciesCompileResult = assetProjectCompiler.Prepare(new AssetCompilerContext { CompilationContext = typeof(AssetCompilationContext) }); + + var buildSteps = dependenciesCompileResult.BuildSteps; + buildSteps?.Add(new UpdateImportShaderCacheBuildStep(new HashSet(), packages.Select(x => x.Package.Meta.Name).ToList())); + + return buildSteps; + } +} diff --git a/sources/editor/Stride.Editor/Engine/EntityExtensions.cs b/sources/editor/Stride.Editor/Engine/EntityExtensions.cs new file mode 100644 index 0000000000..9c15273d60 --- /dev/null +++ b/sources/editor/Stride.Editor/Engine/EntityExtensions.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Extensions; +using Stride.Core.Mathematics; +using Stride.Engine; +using Stride.Navigation; +using Stride.Particles.Components; +using Stride.Rendering; +using Stride.SpriteStudio.Runtime; + +namespace Stride.Editor.Engine; + +public static class EntityExtensions +{ + public static Entity FindSubEntity(this Entity entity, Guid subEntityId) + { + if (entity.Id == subEntityId) + return entity; + + foreach (var child in entity.Transform.Children) + { + if (child.Entity.FindSubEntity(subEntityId) is Entity e) + return e; + } + + return null; + } + + public static Entity FindSubEntity(this Scene scene, Guid subEntityId) + { + if (scene.Entities.Count == 0) + return null; + + if (scene.Entities[0].EntityManager is EntityManager manager) + { + // The entity manager contains all entities of all the scenes under the root scene, + // it should be faster in most cases to iterate over that instead of going through the entity tree + var e = manager.GetEnumerator(); + while (e.MoveNext()) + { + if (e.Current.Id == subEntityId && ReferenceEquals(scene, e.Current.Scene)) + return e.Current; + } + } + else // Slow path, go through tree recursively + { + foreach (Entity entity in scene.Entities) + { + if (entity.FindSubEntity(subEntityId) is Entity e) + return e; + } + } + + return null; + } + + /// + /// Calculate the bounding sphere of the entity's models. + /// + /// The entity to measure + /// Indicate the child entities bounding spheres should be merged + /// Selects which meshes are considered for bounding box calculation. + /// The bounding sphere (world matrix included) + public static BoundingSphere CalculateBoundSphere(this Entity entity, bool isRecursive = true, Func> meshSelector = null) + { + entity.Transform.UpdateWorldMatrix(); + var worldMatrix = entity.Transform.WorldMatrix; + + var boundingSphere = BoundingSphere.Empty; + + // calculate the bounding sphere of the model if any + var modelComponent = entity.Get(); + var hasModel = modelComponent?.Model != null; + if (hasModel) + { + var hierarchy = modelComponent.Skeleton; + var nodeTransforms = new Matrix[hierarchy.Nodes.Length]; + + // Calculate node transforms here, since there might not be a ModelProcessor running + for (int i = 0; i < nodeTransforms.Length; i++) + { + if (hierarchy.Nodes[i].ParentIndex == -1) + { + nodeTransforms[i] = worldMatrix; + } + else + { + Matrix localMatrix; + + Matrix.Transformation( + ref hierarchy.Nodes[i].Transform.Scale, + ref hierarchy.Nodes[i].Transform.Rotation, + ref hierarchy.Nodes[i].Transform.Position, out localMatrix); + + Matrix.Multiply(ref localMatrix, ref nodeTransforms[hierarchy.Nodes[i].ParentIndex], out nodeTransforms[i]); + } + } + + // calculate the bounding sphere + var boundingBox = BoundingBoxExt.Empty; + + var meshes = modelComponent.Model.Meshes; + var filteredMeshes = meshSelector == null ? meshes : meshSelector(modelComponent.Model); + + // Calculate skinned bounding boxes. + // TODO: Cloned from ModelSkinningUpdater. Consolidate. + foreach (var mesh in filteredMeshes) + { + var skinning = mesh.Skinning; + + if (skinning == null) + { + // For unskinned meshes, use the original bounding box + var boundingBoxExt = (BoundingBoxExt)mesh.BoundingBox; + boundingBoxExt.Transform(nodeTransforms[mesh.NodeIndex]); + BoundingBoxExt.Merge(ref boundingBox, ref boundingBoxExt, out boundingBox); + } + else + { + var bones = skinning.Bones; + var bindPoseBoundingBox = new BoundingBoxExt(mesh.BoundingBox); + + for (var index = 0; index < bones.Length; index++) + { + var nodeIndex = bones[index].NodeIndex; + Matrix boneMatrix; + + // Compute bone matrix + Matrix.Multiply(ref bones[index].LinkToMeshMatrix, ref nodeTransforms[nodeIndex], out boneMatrix); + + // Fast AABB transform: http://zeuxcg.org/2010/10/17/aabb-from-obb-with-component-wise-abs/ + // Compute transformed AABB (by world) + var boundingBoxExt = bindPoseBoundingBox; + boundingBoxExt.Transform(boneMatrix); + BoundingBoxExt.Merge(ref boundingBox, ref boundingBoxExt, out boundingBox); + } + } + } + var halfSize = boundingBox.Extent; + var maxHalfSize = Math.Max(halfSize.X, Math.Max(halfSize.Y, halfSize.Z)); + boundingSphere = BoundingSphere.Merge(boundingSphere, new BoundingSphere(boundingBox.Center, maxHalfSize)); + } + + // Calculate the bounding sphere for the sprite component if any and merge the result + var spriteComponent = entity.Get(); + var hasSprite = spriteComponent?.CurrentSprite != null; + if (hasSprite && !(hasModel && meshSelector != null)) + { + var spriteSize = spriteComponent.CurrentSprite.Size; + var spriteDiagonalSize = MathF.Sqrt(spriteSize.X * spriteSize.X + spriteSize.Y * spriteSize.Y); + + // Note: this is probably wrong, need to unify with SpriteComponentRenderer + var center = worldMatrix.TranslationVector; + var scales = new Vector3(worldMatrix.Row1.Length(), worldMatrix.Row2.Length(), worldMatrix.Row3.Length()); + var maxScale = Math.Max(scales.X, Math.Max(scales.Y, scales.Z)); + + boundingSphere = BoundingSphere.Merge(boundingSphere, new BoundingSphere(center, maxScale * spriteDiagonalSize / 2f)); + } + + var spriteStudioComponent = entity.Get(); + if (spriteStudioComponent != null) + { + // Make sure nodes are prepared + if (!SpriteStudioProcessor.PrepareNodes(spriteStudioComponent)) return new BoundingSphere(); + + // Update root nodes + foreach (var node in spriteStudioComponent.Nodes) + { + node.UpdateTransformation(); + } + + // Compute bounding sphere for each node + foreach (var node in spriteStudioComponent.Nodes.SelectDeep(x => x.ChildrenNodes)) + { + if (node.Sprite == null || node.Hide != 0) continue; + + var nodeMatrix = node.ModelTransform * worldMatrix; + + var spriteSize = node.Sprite.Size; + var spriteDiagonalSize = MathF.Sqrt(spriteSize.X * spriteSize.X + spriteSize.Y * spriteSize.Y); + + Vector3 pos, scale; + nodeMatrix.Decompose(out scale, out pos); + + var center = pos; + var maxScale = Math.Max(scale.X, scale.Y); //2d ignore Z + + boundingSphere = BoundingSphere.Merge(boundingSphere, new BoundingSphere(center, maxScale * (spriteDiagonalSize / 2f))); + } + } + + var particleComponent = entity.Get(); + if (particleComponent != null) + { + var center = worldMatrix.TranslationVector; + var sphere = particleComponent.ParticleSystem?.BoundingShape != null ? BoundingSphere.FromBox(particleComponent.ParticleSystem.BoundingShape.GetAABB(center, Quaternion.Identity, 1.0f)) : new BoundingSphere(center, 2.0f); + boundingSphere = BoundingSphere.Merge(boundingSphere, sphere); + } + + var boundingBoxComponent = entity.Get(); + if (boundingBoxComponent != null) + { + var center = worldMatrix.TranslationVector; + var scales = new Vector3(worldMatrix.Row1.Length(), worldMatrix.Row2.Length(), worldMatrix.Row3.Length()) * boundingBoxComponent.Size; + boundingSphere = BoundingSphere.FromBox(new BoundingBox(-scales + center, scales + center)); + } + + // Extend the bounding sphere to include the children + if (isRecursive) + { + foreach (var child in entity.GetChildren()) + boundingSphere = BoundingSphere.Merge(boundingSphere, child.CalculateBoundSphere(true, meshSelector)); + } + + // If the entity does not contain any components having an impact on the bounding sphere, create an empty bounding sphere centered on the entity position. + if (boundingSphere == BoundingSphere.Empty) + boundingSphere = new BoundingSphere(worldMatrix.TranslationVector, 0); + + return boundingSphere; + } +} diff --git a/sources/editor/Stride.Editor/Preview/EditorGameCompilationContext.cs b/sources/editor/Stride.Editor/Preview/EditorGameCompilationContext.cs new file mode 100644 index 0000000000..a04ce83ce8 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/EditorGameCompilationContext.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Compiler; + +namespace Stride.Editor.Preview; + +public class EditorGameCompilationContext : AssetCompilationContext +{ +} diff --git a/sources/editor/Stride.Editor/Preview/PreviewCompilationContext.cs b/sources/editor/Stride.Editor/Preview/PreviewCompilationContext.cs new file mode 100644 index 0000000000..a876f2f65b --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/PreviewCompilationContext.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Compiler; + +namespace Stride.Editor.Preview; + +public class PreviewCompilationContext : AssetCompilationContext +{ +} diff --git a/sources/editor/Stride.Editor/Resources/DefaultThumbnails.cs b/sources/editor/Stride.Editor/Resources/DefaultThumbnails.cs new file mode 100644 index 0000000000..8ea18331d7 --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/DefaultThumbnails.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Reflection; +using Stride.Graphics; + +namespace Stride.Editor.Resources +{ + public static class DefaultThumbnails + { + private static readonly Assembly thisAssembly = typeof(DefaultThumbnails).Assembly; + + private static readonly Lazy lazyAssetBroken = new( + () => EmbeddedResourceReader.GetImage("Stride.Editor.Resources.appbar.checkmark.cross.png", thisAssembly)); + private static readonly Lazy lazyDependencyError = new( + () => EmbeddedResourceReader.GetImage("Stride.Editor.Resources.ThumbnailDependencyError.png", thisAssembly)); + private static readonly Lazy lazyDependencyWarning = new( + () => EmbeddedResourceReader.GetImage("Stride.Editor.Resources.ThumbnailDependencyWarning.png", thisAssembly)); + private static readonly Lazy lazyTextureNoSource = new( + () => EmbeddedResourceReader.GetImage("Stride.Editor.Resources.appbar.page.delete.png", thisAssembly)); + private static readonly Lazy lazyUserAsset = new( + () => EmbeddedResourceReader.GetImage("Stride.Editor.Resources.appbar.resource.png", thisAssembly)); + + public static Image AssetBroken => lazyAssetBroken.Value; + + public static Image DependencyError => lazyDependencyError.Value; + + public static Image DependencyWarning => lazyDependencyWarning.Value; + + public static Image TextureNoSource => lazyTextureNoSource.Value; + + public static Image UserAsset => lazyUserAsset.Value; + } +} diff --git a/sources/editor/Stride.Editor/Resources/EmbeddedResourceReader.cs b/sources/editor/Stride.Editor/Resources/EmbeddedResourceReader.cs new file mode 100644 index 0000000000..ae64f68d4b --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/EmbeddedResourceReader.cs @@ -0,0 +1,36 @@ + +using System.Reflection; +using Stride.Graphics; + +namespace Stride.Editor.Resources +{ + internal static class EmbeddedResourceReader + { + public static async Task GetBytesAsync(string name, Assembly? source = null) + { + using var stream = GetStream(name, source); + using var memory = new MemoryStream(); + await stream.CopyToAsync(memory); + return memory.ToArray(); + } + + public static Image GetImage(string name, Assembly? source = null) + { + using var stream = GetStream(name, source); + return Image.Load(stream); + } + + public static Stream GetStream(string name, Assembly? source = null) + { + source ??= Assembly.GetCallingAssembly(); + return source.GetManifestResourceStream(name) ?? throw new Exception($"Resource {name} not found in {source.GetName().Name}"); + } + + public static async Task GetStringAsync(string name, Assembly? source = null) + { + using var stream = GetStream(name, source); + using var reader = new StreamReader(stream); + return await reader.ReadToEndAsync(); + } + } +} diff --git a/sources/editor/Stride.Editor/Resources/ThumbnailDependencyError.png b/sources/editor/Stride.Editor/Resources/ThumbnailDependencyError.png new file mode 100644 index 0000000000..d8b4021f1a --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/ThumbnailDependencyError.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6dbdc4aee8144dde3ab9446388a2f2a0d7819568c1b76992b260acd40f858527 +size 1517 diff --git a/sources/editor/Stride.Editor/Resources/ThumbnailDependencyWarning.png b/sources/editor/Stride.Editor/Resources/ThumbnailDependencyWarning.png new file mode 100644 index 0000000000..e7d1d55b4e --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/ThumbnailDependencyWarning.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47fba25a67757b143d00b2ec385a79ad1273ae3a8eca0f5bbe5e636e4743b482 +size 1392 diff --git a/sources/editor/Stride.Editor/Resources/appbar.box.png b/sources/editor/Stride.Editor/Resources/appbar.box.png new file mode 100644 index 0000000000..fc15b18495 --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/appbar.box.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d14397d8ab354335f1ab9d13d27781063cf1e4f5b0999ee3bab4e2ec746e64e6 +size 610 diff --git a/sources/editor/Stride.Editor/Resources/appbar.checkmark.cross.png b/sources/editor/Stride.Editor/Resources/appbar.checkmark.cross.png new file mode 100644 index 0000000000..74de5f3733 --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/appbar.checkmark.cross.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea662eec4d8c353dfb7649340a29d6dc5e7ba27510117caa01ad86052a0dd716 +size 578 diff --git a/sources/editor/Stride.Editor/Resources/appbar.page.delete.png b/sources/editor/Stride.Editor/Resources/appbar.page.delete.png new file mode 100644 index 0000000000..9214c34aa5 --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/appbar.page.delete.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82ac92f20b0e9e7d4a6ca48ad1775d2ee041e52a8d7619c5ab8ba1bc4dfb99cf +size 859 diff --git a/sources/editor/Stride.Editor/Resources/appbar.resource.png b/sources/editor/Stride.Editor/Resources/appbar.resource.png new file mode 100644 index 0000000000..9ed80413ee --- /dev/null +++ b/sources/editor/Stride.Editor/Resources/appbar.resource.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d71d0de2cc8f2f44ff5cd5aa61382904ea26cf72e7a1956f21d47dd6313f478d +size 1170 diff --git a/sources/editor/Stride.Editor/Stride.Editor.csproj b/sources/editor/Stride.Editor/Stride.Editor.csproj index 38759009da..336eaa8697 100644 --- a/sources/editor/Stride.Editor/Stride.Editor.csproj +++ b/sources/editor/Stride.Editor/Stride.Editor.csproj @@ -14,5 +14,22 @@ + + + + + + + + + + + + + + + + + diff --git a/sources/editor/Stride.Editor/Thumbnails/BitmapThumbnailData.cs b/sources/editor/Stride.Editor/Thumbnails/BitmapThumbnailData.cs new file mode 100644 index 0000000000..c5e00637ce --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/BitmapThumbnailData.cs @@ -0,0 +1,62 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Storage; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails +{ + /// + /// Byte streams bitmap support for thumbnails. + /// + public sealed class BitmapThumbnailData : ThumbnailData + { + private static readonly ObjectCache Cache = new(512); + private Stream? thumbnailBitmapStream; + + public BitmapThumbnailData(ObjectId thumbnailId, Stream thumbnailBitmapStream) : base(thumbnailId) + { + this.thumbnailBitmapStream = thumbnailBitmapStream; + } + + /// + protected override Image? BuildImageSource() + { + return BuildAsBitmapImage(thumbnailId, thumbnailBitmapStream); + } + + /// + protected override void FreeBuildingResources() + { + thumbnailBitmapStream?.Dispose(); + thumbnailBitmapStream = null; + } + + private static Image? BuildAsBitmapImage(ObjectId thumbnailId, Stream? thumbnailStream) + { + if (thumbnailStream == null) + return null; + + var stream = thumbnailStream; + if (!stream.CanRead) + return null; + + var result = Cache.TryGet(thumbnailId); + if (result != null) + return result; + + try + { + var image = Image.Load(stream); + Cache.Cache(thumbnailId, image); + return image; + } + catch (Exception) + { + return null; + } + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/CustomAssetThumbnailCompiler.cs b/sources/editor/Stride.Editor/Thumbnails/CustomAssetThumbnailCompiler.cs new file mode 100644 index 0000000000..11f7d96dbf --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/CustomAssetThumbnailCompiler.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Mathematics; +using Stride.Core.Reflection; +using Stride.Core.Presentation.Core; +using Stride.Editor.Resources; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// +/// The compiler used by default to create thumbnails when the user has not explicitly defined the compiler to use for its asset. +/// +[AssetCompiler(typeof(Asset), typeof(ThumbnailCompilationContext))] +public class CustomAssetThumbnailCompiler : ThumbnailCompilerBase +{ + public CustomAssetThumbnailCompiler() + { + IsStatic = true; + } + + protected override void CompileThumbnail(ThumbnailCompilerContext context, string thumbnailStorageUrl, AssetItem assetItem, Package originalPackage, AssetCompilerResult result) + { + result.BuildSteps.Add(new CustomAssetThumbnailBuildCommand(context, thumbnailStorageUrl, assetItem, originalPackage, new ThumbnailCommandParameters(assetItem.Asset, thumbnailStorageUrl, context.ThumbnailResolution))); + } + + /// + /// Command used to build the thumbnail of the texture in the storage + /// + private class CustomAssetThumbnailBuildCommand : ThumbnailFromTextureCommand + { + public CustomAssetThumbnailBuildCommand(ThumbnailCompilerContext context, string url, AssetItem assetItem, IAssetFinder assetFinder, ThumbnailCommandParameters description) + : base(context, assetItem, assetFinder, url, description) + { + } + + protected override void PreloadAsset() + { + } + + protected override void SetThumbnailParameters() + { + var assetType = Parameters.Asset.GetType(); + TitleText = TypeDescriptorFactory.Default.AttributeRegistry.GetAttribute(assetType)?.Name ?? assetType.Name; + Font = DefaultFont; + FontColor = Color.White; + BackgroundColor = (Color)assetType.GetUniqueColor(); + BackgroundTexture = Texture.New(GraphicsDevice, DefaultThumbnails.UserAsset); + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/GameStudioThumbnailService.cs b/sources/editor/Stride.Editor/Thumbnails/GameStudioThumbnailService.cs new file mode 100644 index 0000000000..694621d555 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/GameStudioThumbnailService.cs @@ -0,0 +1,320 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.BuildEngine; +using Stride.Core.Collections; +using Stride.Core.Diagnostics; +using Stride.Editor.Build; +using Stride.Editor.Resources; +using Stride.Graphics; +using Stride.Shaders.Compiler; + +namespace Stride.Editor.Thumbnails; + +public class GameStudioThumbnailService : IThumbnailService +{ + private readonly object hashLock = new(); + private readonly Dictionary> thumbnailQueueHash = new(); + + // Note: KVP.Value is usually null, and is only set if a new request for the same thumbnail has been done and we need to start it when the current one finish (avoid running it twice at the same time) + private readonly Dictionary thumbnailsInProgressAndContinuation = new(); + private readonly ISessionViewModel session; + private readonly GameStudioBuilderService assetBuilderService; + private readonly ThumbnailListCompiler thumbnailCompiler; + private readonly AssetCompilerRegistry compilerRegistry; + private readonly List assetsToIncreasePriority = new(); + private readonly ThumbnailGenerator generator; + private bool thumbnailThreadShouldTerminate; + private RenderingMode renderingMode; + private ColorSpace colorSpace; + private GraphicsProfile graphicsProfile; + private int firstPriority, lastPriority; + + private int currentJobToken = -1; + + private GameSettingsAsset currentGameSettings; + + private readonly GameSettingsProviderService gameSettingsProviderService; + + public GameStudioThumbnailService(ISessionViewModel session, GameSettingsProviderService settingsProvider, GameStudioBuilderService assetBuilderService) + { + this.session = session; + this.assetBuilderService = assetBuilderService; + + generator = new ThumbnailGenerator((EffectCompilerBase)assetBuilderService.EffectCompiler); + compilerRegistry = new AssetCompilerRegistry { DefaultCompiler = new CustomAssetThumbnailCompiler() }; + thumbnailCompiler = new ThumbnailListCompiler(generator, ThumbnailBuilt, compilerRegistry); + + gameSettingsProviderService = settingsProvider; + gameSettingsProviderService.GameSettingsChanged += GameSettingsChanged; + UpdateGameSettings(settingsProvider.CurrentGameSettings); + StartPushNotificationsTask(); + } + + public void Dispose() + { + // Terminate thumbnail control thread + lock (hashLock) + { + foreach (var item in thumbnailQueueHash) + { + assetBuilderService.RemoveBuildUnit(item.Value); + } + thumbnailQueueHash.Clear(); + } + thumbnailThreadShouldTerminate = true; + generator.Dispose(); + gameSettingsProviderService.GameSettingsChanged -= GameSettingsChanged; + } + + private void GameSettingsChanged(object sender, GameSettingsChangedEventArgs e) + { + UpdateGameSettings(e.GameSettings); + } + + private void UpdateGameSettings(GameSettingsAsset gameSettings) + { + currentGameSettings = AssetCloner.Clone(gameSettings, AssetClonerFlags.RemoveUnloadableObjects); + + var shouldRefreshAllThumbnails = false; + if (renderingMode != gameSettings.GetOrCreate().RenderingMode) + { + renderingMode = gameSettings.GetOrCreate().RenderingMode; + shouldRefreshAllThumbnails = true; + } + if (colorSpace != gameSettings.GetOrCreate().ColorSpace) + { + colorSpace = gameSettings.GetOrCreate().ColorSpace; + shouldRefreshAllThumbnails = true; + } + if (graphicsProfile != gameSettings.GetOrCreate().DefaultGraphicsProfile) + { + graphicsProfile = gameSettings.GetOrCreate().DefaultGraphicsProfile; + shouldRefreshAllThumbnails = true; + } + if (shouldRefreshAllThumbnails) + { + var allAssets = session.AllAssets.Select(x => x.AssetItem).ToList(); + Task.Run(() => AddThumbnailAssetItems(allAssets, QueuePosition.First)); + } + } + + public static byte[] HandleBrokenThumbnail() + { + // Load broken asset thumbnail + var assetBrokenThumbnail = DefaultThumbnails.AssetBroken; + + // Apply thumbnail status in corner + ThumbnailBuildHelper.ApplyThumbnailStatus(assetBrokenThumbnail, LogMessageType.Error); + var memoryStream = new MemoryStream(); + assetBrokenThumbnail.Save(memoryStream, ImageFileType.Png); + + return memoryStream.ToArray(); + } + + public event EventHandler? ThumbnailCompleted; + + /// + public bool HasStaticThumbnail(Type assetType) + { + var compiler = (IThumbnailCompiler)compilerRegistry.GetCompiler(assetType, typeof(ThumbnailCompilationContext)); + return compiler?.IsStatic ?? true; + } + + public ListBuildStep Compile(AssetItem asset, GameSettingsAsset gameSettings) + { + // Mark thumbnail as being compiled + lock (hashLock) + { + thumbnailQueueHash.Remove(asset); + if (thumbnailsInProgressAndContinuation.ContainsKey(asset.Id) && System.Diagnostics.Debugger.IsAttached) + { + // Virgile: This case should not happen, but it happened to me once and could not reproduce. + // Please let me know if it happens to you. + // Note: this is likely not critical and should work fine even if it happens. + System.Diagnostics.Debugger.Break(); + } + thumbnailsInProgressAndContinuation[asset.Id] = null; + } + + return thumbnailCompiler.Compile(asset, gameSettings, HasStaticThumbnail(asset.Asset.GetType())); + } + + public void AddThumbnailAssetItems(IEnumerable assetItems, QueuePosition position) + { + if (!thumbnailThreadShouldTerminate) + { + assetBuilderService.WaitForShaders(); + + lock (hashLock) + { + foreach (var asset in assetItems) + { + if (thumbnailsInProgressAndContinuation.ContainsKey(asset.Id)) + { + // Thumbnail is already being generated, set this one as continuation + thumbnailsInProgressAndContinuation[asset.Id] = new ThumbnailContinuation(asset, position); + } + else if (position == QueuePosition.First) + { + if (thumbnailQueueHash.TryGetValue(asset, out var node)) + { + assetBuilderService.RemoveBuildUnit(node); + thumbnailQueueHash.Remove(asset); + } + + node = assetBuilderService.PushBuildUnit(new ThumbnailAssetBuildUnit(asset, currentGameSettings, this, firstPriority--)); + thumbnailQueueHash.Add(asset, node); + } + else + { + if (!thumbnailQueueHash.ContainsKey(asset)) + { + var node = assetBuilderService.PushBuildUnit(new ThumbnailAssetBuildUnit(asset, currentGameSettings, this, lastPriority++)); + thumbnailQueueHash.Add(asset, node); + } + } + } + } + } + } + + public void IncreaseThumbnailPriority(IEnumerable assetItems) + { + if (!thumbnailThreadShouldTerminate) + { + lock (hashLock) + { + // Batch assets whose priority needs to be updated + foreach (var assetItem in assetItems) + { + if (thumbnailQueueHash.TryGetValue(assetItem, out var node)) + { + var compiler = (IThumbnailCompiler)compilerRegistry.GetCompiler(assetItem.Asset.GetType(), typeof(ThumbnailCompilationContext)); + var priority = compiler.Priority; + assetsToIncreasePriority.Add(new ThumbnailPriorityItem(assetItem, node, priority)); + } + } + + // Sort by thumbnail priority + assetsToIncreasePriority.Sort(ThumbnailPriorityComparer.Default); + + // Readd at beginning of the queue (reverse so that firstPriority-- matches assetsToIncreasePriority order + for (var index = assetsToIncreasePriority.Count - 1; index >= 0; index--) + { + var thumbnailPriorityItem = assetsToIncreasePriority[index]; + var node = thumbnailPriorityItem.Node; + var asset = thumbnailPriorityItem.Asset; + + assetBuilderService.RemoveBuildUnit(node); + thumbnailQueueHash.Remove(asset); + node = assetBuilderService.PushBuildUnit(new ThumbnailAssetBuildUnit(asset, currentGameSettings, this, firstPriority--)); + thumbnailQueueHash.Add(asset, node); + } + + assetsToIncreasePriority.Clear(); + } + } + } + + private void ThumbnailBuilt(object sender, ThumbnailBuiltEventArgs e) + { + ThumbnailData thumbnailData = null; + if (e.ThumbnailStream != null) + { + var stream = new MemoryStream(); + e.ThumbnailStream.CopyTo(stream); + thumbnailData = new BitmapThumbnailData(e.ThumbnailId, stream); + } + + if (e.Result != ThumbnailBuildResult.Cancelled) + { + ThumbnailCompleted?.Invoke(this, new ThumbnailCompletedArgs(e.AssetId, thumbnailData)); + } + + lock (hashLock) + { + thumbnailsInProgressAndContinuation.TryGetValue(e.AssetId, out var thumbnailContinuation); + thumbnailsInProgressAndContinuation.Remove(e.AssetId); + + // Check if same asset has been requested again while it was compiling + if (thumbnailContinuation != null) + { + var priority = thumbnailContinuation.Position == QueuePosition.First ? firstPriority-- : lastPriority++; + var node = assetBuilderService.PushBuildUnit(new ThumbnailAssetBuildUnit(thumbnailContinuation.UpdatedAssetToRecompile, currentGameSettings, this, priority)); + thumbnailQueueHash.Add(thumbnailContinuation.UpdatedAssetToRecompile, node); + } + } + } + + private void StartPushNotificationsTask() + { + Task.Run(async () => + { + while (!thumbnailThreadShouldTerminate) + { + await Task.Delay(500); + if (currentJobToken >= 0) + { + if (thumbnailQueueHash.Count > 0) + { + // FIXME xplat-editor + //EditorViewModel.Instance.Status.NotifyBackgroundJobProgress(currentJobToken, thumbnailQueueHash.Count, true); + } + else + { + // FIXME xplat-editor + //EditorViewModel.Instance.Status.NotifyBackgroundJobFinished(currentJobToken); + currentJobToken = -1; + } + } + else if (thumbnailQueueHash.Count > 0) + { + // FIXME xplat-editor + //currentJobToken = EditorViewModel.Instance.Status.NotifyBackgroundJobStarted("Building thumbnails… ({0} in queue)", JobPriority.Background); + } + } + }); + } + + private readonly struct ThumbnailPriorityItem + { + public readonly AssetItem Asset; + public readonly PriorityQueueNode Node; + public readonly int Priority; + + public ThumbnailPriorityItem(AssetItem asset, PriorityQueueNode node, int priority) : this() + { + Asset = asset; + Node = node; + Priority = priority; + } + } + + private class ThumbnailPriorityComparer : Comparer + { + public static new readonly ThumbnailPriorityComparer Default = new(); + + public override int Compare(ThumbnailPriorityItem x, ThumbnailPriorityItem y) + { + return x.Priority - y.Priority; + } + } + + private class ThumbnailContinuation + { + public readonly AssetItem UpdatedAssetToRecompile; + public readonly QueuePosition Position; + + public ThumbnailContinuation(AssetItem updatedAssetToRecompile, QueuePosition position) + { + UpdatedAssetToRecompile = updatedAssetToRecompile; + Position = position; + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/IThumbnailCommand.cs b/sources/editor/Stride.Editor/Thumbnails/IThumbnailCommand.cs new file mode 100644 index 0000000000..5af45997a3 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/IThumbnailCommand.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Diagnostics; + +namespace Stride.Editor.Thumbnails; + +public interface IThumbnailCommand +{ + /// + /// Gets or sets the dependency build step. + /// + /// + /// The dependency build step. + /// + LogMessageType DependencyBuildStatus { get; set; } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/IThumbnailCompiler.cs b/sources/editor/Stride.Editor/Thumbnails/IThumbnailCompiler.cs new file mode 100644 index 0000000000..698d9d0910 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/IThumbnailCompiler.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Compiler; + +namespace Stride.Editor.Thumbnails; + +public interface IThumbnailCompiler : IAssetCompiler +{ + int Priority { get; set; } + + bool IsStatic { get; set; } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ResourceThumbnailData.cs b/sources/editor/Stride.Editor/Thumbnails/ResourceThumbnailData.cs new file mode 100644 index 0000000000..9796472d0c --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ResourceThumbnailData.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Storage; +using Stride.Editor.Resources; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails +{ + /// + /// Generic Image resources, DrawingImage vectors, etc. support for thumbnails. + /// + public sealed class ResourceThumbnailData : ThumbnailData + { + private string? resourceKey; + + /// The key used to fetch the resource, most likely a string. + public ResourceThumbnailData(ObjectId thumbnailId, object resourceKey) + : base(thumbnailId) + { + this.resourceKey = resourceKey.ToString(); + } + + /// + protected override Image? BuildImageSource() + { + if (resourceKey == null) + return null; + + try + { + return EmbeddedResourceReader.GetImage(resourceKey); + } + catch (Exception) + { + return null; + } + } + + /// + protected override void FreeBuildingResources() + { + resourceKey = null; + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCommand.cs b/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCommand.cs new file mode 100644 index 0000000000..4bace8c1f8 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCommand.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Core.Serialization; +using Stride.TextureConverter; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// +/// Command used to build a thumbnail from a static image. +/// +public class StaticThumbnailCommand : AssetCommand, IThumbnailCommand +{ + private readonly byte[] staticImageData; + + private readonly Int2 thumbnailSize; + + // TODO: This is not serializable (OK for now since thumbnails are never built in a separate process); later, a specific class to store results will be needed + /// + public LogMessageType DependencyBuildStatus { get; set; } + + public StaticThumbnailCommand(string url, byte[] staticImageData, Int2 thumbnailSize, bool loadAsSRgb, IAssetFinder assetFinder) + : base(url, new StaticThumbnailCommandParameters(thumbnailSize, typeof(T).FullName, loadAsSRgb), assetFinder) + { + this.staticImageData = staticImageData; + this.thumbnailSize = thumbnailSize; + } + + protected override void ComputeParameterHash(BinarySerializationWriter writer) + { + base.ComputeParameterHash(writer); + if (DependencyBuildStatus >= LogMessageType.Warning) + writer.Write(DependencyBuildStatus); + } + + protected override Task DoCommandOverride(ICommandContext commandContext) + { + // load the sound thumbnail image from the resources + using (var imageStream = new MemoryStream(staticImageData)) + using (var image = Image.Load(imageStream)) + using (var texTool = new TextureTool()) + using (var texImage = texTool.Load(image, Parameters.SRgb)) + { + // Rescale image so that it fits the thumbnail asked resolution + texTool.Decompress(texImage, texImage.Format.IsSRgb()); + texTool.Resize(texImage, thumbnailSize.X, thumbnailSize.Y, Filter.Rescaling.Lanczos3); + + // Save + using (var outputImageStream = MicrothreadLocalDatabases.DatabaseFileProvider.OpenStream(Url, VirtualFileMode.Create, VirtualFileAccess.Write)) + using (var outputImage = texTool.ConvertToStrideImage(texImage)) + { + ThumbnailBuildHelper.ApplyThumbnailStatus(outputImage, DependencyBuildStatus); + + outputImage.Save(outputImageStream, ImageFileType.Png); + + commandContext.Logger.Verbose($"Thumbnail creation successful [{Url}] to ({outputImage.Description.Width}x{outputImage.Description.Height},{outputImage.Description.Format})"); + } + } + + return Task.FromResult(ResultStatus.Successful); + } +} + +/// +/// The parameters of the animation thumbnail command that will be used to produce the command hash. +/// Since the animation image is constant, only the size of the thumbnail should be hashed. +/// +[DataContract] +public class StaticThumbnailCommandParameters +{ + public Int2 ThumbnailSize; + + public string Typename; + + public bool SRgb; + + public StaticThumbnailCommandParameters() + { + } + + public StaticThumbnailCommandParameters(Int2 thumbnailSize, string typename, bool srgb) + { + ThumbnailSize = thumbnailSize; + Typename = typename; + SRgb = srgb; + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCompiler.cs b/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCompiler.cs new file mode 100644 index 0000000000..fb8fef7fe3 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/StaticThumbnailCompiler.cs @@ -0,0 +1,37 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.IO; +using Stride.Assets; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// +/// The base class for a static thumbnail compiler +/// +/// The type of asset taken in charge by the compiler +public class StaticThumbnailCompiler : ThumbnailCompilerBase where T : Asset +{ + private readonly byte[] staticImageData; + + public StaticThumbnailCompiler(byte[] staticImageData) + { + this.staticImageData = staticImageData; + IsStatic = true; + } + + protected override void CompileThumbnail(ThumbnailCompilerContext context, string thumbnailStorageUrl, AssetItem assetItem, Package originalPackage, AssetCompilerResult result) + { + var gameSettings = context.GetGameSettingsAsset(); + var renderingSettings = gameSettings.GetOrCreate(); + result.BuildSteps.Add(new ThumbnailBuildStep(new StaticThumbnailCommand(thumbnailStorageUrl, staticImageData, context.ThumbnailResolution, renderingSettings.ColorSpace == ColorSpace.Linear, assetItem.Package))); + } + + protected override string BuildThumbnailStoreName(UFile assetUrl) + { + return ThumbnailStorageNamePrefix + typeof(T).Name; + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/StrideThumbnailCommand.cs b/sources/editor/Stride.Editor/Thumbnails/StrideThumbnailCommand.cs new file mode 100644 index 0000000000..1ad294cdbb --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/StrideThumbnailCommand.cs @@ -0,0 +1,187 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Core.Serialization; +using Stride.Core.Serialization.Contents; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering.Compositing; +using EditorSettings = Stride.Assets.EditorSettings; + +namespace Stride.Editor.Thumbnails; + +/// +/// The base command to build stride thumbnails. +/// - It uses the underlying to build thumbnail via scenes. +/// - It extracts and exposes the thumbnail services from the , +/// - It defines the , base functions to be overridden. +/// +public abstract class StrideThumbnailCommand : ThumbnailCommand, IThumbnailCommand where TRuntimeAsset : class +{ + private readonly AssetItem assetItem; + + /// + /// The compilation context. + /// + protected readonly ThumbnailCompilerContext CompilerContext; + + /// + /// The game instance in charge of rendering the thumbnails and the previews. + /// + protected readonly ThumbnailGenerator Generator; + + /// + /// The asset object loaded by the game. + /// + protected TRuntimeAsset LoadedAsset; + + protected readonly Color ThumbnailBackgroundColor = Color.FromBgra(0xFF434343); + + protected StrideThumbnailCommand(ThumbnailCompilerContext context, AssetItem assetItem, IAssetFinder assetFinder, string url, ThumbnailCommandParameters parameters) + : base(url, assetItem, parameters, assetFinder) + { + CompilerContext = context ?? throw new ArgumentNullException(nameof(context)); + this.assetItem = assetItem; + + // Copy GameSettings ColorSpace/RenderingMode to the parameters + var gameSettings = context.GetGameSettingsAsset(); + + var renderingSettings = gameSettings.GetOrCreate(); + parameters.ColorSpace = renderingSettings.ColorSpace; + parameters.RenderingMode = gameSettings.GetOrCreate().RenderingMode; + + Generator = context.Properties.Get(ThumbnailGenerator.Key) ?? throw new ArgumentException("The provided context does not contain required stride information needed to build the thumbnails."); + } + + /// + public LogMessageType DependencyBuildStatus { get; set; } + + protected UFile AssetUrl => assetItem.Location; + + /// + /// The default font used to build the thumbnails. + /// + protected SpriteFont DefaultFont => Generator.DefaultFont; + + /// + /// The graphics device used to build the thumbnails. + /// + protected GraphicsDevice GraphicsDevice => Generator.GraphicsDevice; + + /// + /// The list of services available to build the thumbnails. + /// + protected IServiceRegistry Services => Generator.Services; + + /// + /// An instance of sprite batch available to draw thumbnails. + /// + protected SpriteBatch SpriteBatch => Generator.SpriteBatch; + + /// + /// An instance of ui batch available to draw thumbnails, including SDF fonts. + /// + protected UIBatch UIBatch => Generator.UIBatch; + + /// + /// A unique key to identify the shared graphics compositor to use for this command. will be called once for each different value of that exists. + /// + protected abstract string GraphicsCompositorKey { get; } + + /// + protected override void ComputeParameterHash(BinarySerializationWriter writer) + { + base.ComputeParameterHash(writer); + if (DependencyBuildStatus >= LogMessageType.Warning) + writer.Write(DependencyBuildStatus); + var gameSettings = CompilerContext.GetGameSettingsAsset(); + if (gameSettings != null) + { + var editorRenderingMode = gameSettings.GetOrCreate().RenderingMode; + var colorSpace = gameSettings.GetOrCreate().ColorSpace; + writer.Write(editorRenderingMode); + writer.Write(colorSpace); + } + } + + /// + protected sealed override Task DoCommandOverride(ICommandContext commandContext) + { + PreloadAsset(); + var graphicsCompositor = GraphicsDevice.GetOrCreateSharedData(GraphicsCompositorKey, CreateSharedGraphicsCompositor); + var scene = CreateScene(graphicsCompositor); + var result = Generator.BuildThumbnail(Url, scene, graphicsCompositor, MicrothreadLocalDatabases.DatabaseFileProvider, Parameters.ThumbnailSize, Parameters.ColorSpace, Parameters.RenderingMode, commandContext.Logger, DependencyBuildStatus, CustomizeThumbnail); + DestroyScene(scene); + UnloadAsset(); + return Task.FromResult(result); + } + + /// + /// Creates the scene for capturing the thumbnail + /// + /// The graphics compositor to use to render the scene. + /// A new scene ready to be rendered to generate a thumbnail. + /// The given might be shared between multiple command types, hence should not be modified. + protected abstract Scene CreateScene(GraphicsCompositor graphicsCompositor); + + /// + /// Creates a new instance of , to be shared between all instances of this command. + /// + /// The graphics device in use. + /// A new instance of . + /// The returned instance must be a new instance of , since its lifetime and disposal is handled by the object. + protected abstract GraphicsCompositor CreateSharedGraphicsCompositor(GraphicsDevice device); + + /// + /// Customizes the given image, rendered by the instance of . + /// + /// The image to customize. + protected virtual void CustomizeThumbnail(Image image) + { + } + + /// + /// Destroys the scene used to render the thumbnail. + /// + /// The scene to destroy, corresponding to the scene created earlier by . + protected virtual void DestroyScene(Scene scene) + { + } + + /// + /// Loads the assets that will be needed to render the thumbnails. They must have been built before running this command, therefore the instances + /// to build them must be set to be dependencies of this command in the method. + /// + /// + protected virtual void PreloadAsset() + { + LoadedAsset = Generator.ContentManager.Load(AssetUrl, ContentManagerLoaderSettings.StreamingDisabled); + } + + /// + /// Unloads the assets loaded by . + /// + protected virtual void UnloadAsset() + { + UnloadAsset(ref LoadedAsset); + } + + /// + /// Unloads safely the given asset, if it's not null. + /// + protected void UnloadAsset(ref T loadedAsset) where T : class + { + if (loadedAsset != null) + { + Generator.ContentManager.Unload(loadedAsset); + loadedAsset = null; + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailAssetBuildUnit.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailAssetBuildUnit.cs new file mode 100644 index 0000000000..bbfbeab316 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailAssetBuildUnit.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; +using Stride.Editor.Build; + +namespace Stride.Editor.Thumbnails; + +public class ThumbnailAssetBuildUnit : AssetBuildUnit +{ + private static readonly Guid ThumbnailBuildUnitContextId = Guid.NewGuid(); + private readonly AssetItem asset; + private readonly GameStudioThumbnailService thumbnailService; + private readonly GameSettingsAsset gameSettings; + + public ThumbnailAssetBuildUnit(AssetItem asset, GameSettingsAsset gameSettings, GameStudioThumbnailService thumbnailService, int priorityOrder) + : base(new AssetBuildUnitIdentifier(ThumbnailBuildUnitContextId, asset.Id)) + { + this.asset = asset; + this.thumbnailService = thumbnailService; + this.gameSettings = gameSettings; + + PriorityMajor = DefaultAssetBuilderPriorities.ThumbnailPriority; + PriorityMinor = priorityOrder; + } + + protected override ListBuildStep Prepare() + { + return thumbnailService.Compile(asset, gameSettings); + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildHelper.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildHelper.cs new file mode 100644 index 0000000000..9398071e72 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildHelper.cs @@ -0,0 +1,143 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Diagnostics; +using Stride.Core.Mathematics; +using Stride.Editor.Resources; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// This should be used for overlaying build status icon on top of thumbnail. +public class ThumbnailBuildHelper : IDisposable +{ + private static readonly object thumbnailLock = new(); + private static GraphicsDevice staticGraphicsDevice; + private static SpriteBatch staticSpriteBatch; + + private static Texture staticRenderTarget; + private static Texture staticRenderTargetStaging; + + private static Texture errorTexture; + private static Texture warningTexture; + + public GraphicsDevice GraphicsDevice; + public GraphicsContext GraphicsContext; + public SpriteBatch SpriteBatch; + public Texture RenderTarget; + public Texture RenderTargetStaging; + + bool lockWasTaken; + + public ThumbnailBuildHelper() + { + Monitor.Enter(thumbnailLock, ref lockWasTaken); + + // Initialize device + InitializeDevice(); + } + + public void Dispose() + { + GraphicsContext.ResourceGroupAllocator.Dispose(); + + if (lockWasTaken) + Monitor.Exit(thumbnailLock); + } + + private void InitializeDevice() + { + // If first time, let's create graphics device and resources + if (staticGraphicsDevice == null) + { + staticGraphicsDevice = GraphicsDevice.New(); + staticSpriteBatch = new SpriteBatch(staticGraphicsDevice); + } + + GraphicsDevice = staticGraphicsDevice; + GraphicsContext = new GraphicsContext(staticGraphicsDevice); + SpriteBatch = staticSpriteBatch; + } + + public void InitializeRenderTargets(PixelFormat format, int width, int height) + { + // Create render target of appropriate size (we expect it to always be the same, otherwise this might need some improvement) + if (staticRenderTarget == null || (staticRenderTarget.Width != width || staticRenderTarget.Height != height || staticRenderTarget.Format != format)) + { + if (staticRenderTarget != null) + { + staticRenderTarget.Dispose(); + staticRenderTargetStaging.Dispose(); + } + + staticRenderTarget = Texture.New2D(staticGraphicsDevice, width, height, format, TextureFlags.RenderTarget); + staticRenderTargetStaging = staticRenderTarget.ToStaging(); + } + + RenderTarget = staticRenderTarget; + RenderTargetStaging = staticRenderTargetStaging; + } + + /// + /// Applies the build status on top of a thumbnail image (using overlay icons). + /// + /// The thumbnail image. + /// The dependency build status. + public static void ApplyThumbnailStatus(Image thumbnailImage, LogMessageType dependencyBuildStatus) + { + // No warning or error, nothing to do (or maybe we should display a logo for "info"?) + if (dependencyBuildStatus < LogMessageType.Warning) + return; + + using (var thumbnailBuilderHelper = new ThumbnailBuildHelper()) + { + if (errorTexture == null) + { + // Load status textures + errorTexture = Texture.New(thumbnailBuilderHelper.GraphicsDevice, DefaultThumbnails.DependencyError); + warningTexture = Texture.New(thumbnailBuilderHelper.GraphicsDevice, DefaultThumbnails.DependencyWarning); + } + + var texture = dependencyBuildStatus == LogMessageType.Warning ? warningTexture : errorTexture; + using (var thumbnailTexture = Texture.New(thumbnailBuilderHelper.GraphicsDevice, thumbnailImage)) + { + thumbnailBuilderHelper.CombineTextures(thumbnailTexture, texture, thumbnailImage.Description.Width - texture.Width - 4, thumbnailImage.Description.Height - texture.Height - 4); + } + + // Read back result to image + thumbnailBuilderHelper.RenderTarget.GetData(thumbnailBuilderHelper.GraphicsContext.CommandList, thumbnailBuilderHelper.RenderTargetStaging, new DataPointer(thumbnailImage.PixelBuffer[0].DataPointer, thumbnailImage.PixelBuffer[0].BufferStride)); + thumbnailImage.Description.Format = thumbnailBuilderHelper.RenderTarget.Format; // In case channels are swapped + } + } + + public void Combine(Texture texture, Image image) + { + lock (thumbnailLock) + { + using (var texture2 = Texture.New(GraphicsDevice, image)) + { + CombineTextures(texture, texture2, 0, 0); + } + + // Read back result to image + RenderTarget.GetData(GraphicsContext.CommandList, RenderTargetStaging, new DataPointer(image.PixelBuffer[0].DataPointer, image.PixelBuffer[0].BufferStride)); + image.Description.Format = RenderTarget.Format; // In case channels are swapped + } + } + + private void CombineTextures(Texture texture1, Texture texture2, int positionX, int positionY) + { + InitializeRenderTargets(PixelFormat.R8G8B8A8_UNorm, texture1.Description.Width, texture1.Description.Height); + + // Generate thumbnail with status icon + // Clear (transparent) + GraphicsContext.CommandList.Clear(RenderTarget, new Color4()); + GraphicsContext.CommandList.SetRenderTargetAndViewport(null, staticRenderTarget); + + // Render thumbnail and status sprite + SpriteBatch.Begin(GraphicsContext); + SpriteBatch.Draw(texture1, Vector2.Zero); + SpriteBatch.Draw(texture2, new Vector2(positionX, positionY)); + SpriteBatch.End(); + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildStep.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildStep.cs new file mode 100644 index 0000000000..89b3dd77fc --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuildStep.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.BuildEngine; + +namespace Stride.Editor.Thumbnails; + +/// +/// Special that will forward dependencies to a implementing . +/// This should be used to properly set . +/// +public class ThumbnailBuildStep : CommandBuildStep +{ + //private readonly BuildStep dependencies; + + public ThumbnailBuildStep(Command command) + : base(command) + { + //this.dependencies = dependencies; + } + + /// + public override Task Execute(IExecuteContext executeContext, BuilderContext builderContext) + { +// var thumbnailCompiler = Command as IThumbnailCommand; +// if (thumbnailCompiler != null) +// { +// var highestLogMessageType = LogMessageType.Debug; +// +// // Check worst type of log message in BuildStep +// foreach (var message in dependencies.EnumerateRecursively().SelectMany(x => x.Logger.Messages)) +// { +// if (highestLogMessageType < message.Type) +// highestLogMessageType = message.Type; +// } +// +// // Also, if build step failed, mark it as an error +// if (highestLogMessageType < LogMessageType.Error && dependencies.Failed) +// { +// highestLogMessageType = LogMessageType.Error; +// } +// +// // TODO: This is not serializable (OK for now since thumbnails are never built in a separate process); later, a specific class to store results will be needed +// thumbnailCompiler.DependencyBuildStatus = highestLogMessageType; +// } + + return base.Execute(executeContext, builderContext); + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuiltEventArgs.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuiltEventArgs.cs new file mode 100644 index 0000000000..c9d9a4d768 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailBuiltEventArgs.cs @@ -0,0 +1,65 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.IO; +using Stride.Core.Storage; + +namespace Stride.Editor.Thumbnails; + +/// +/// This enum describes the result of a thumbnail build operation. +/// +public enum ThumbnailBuildResult +{ + /// + /// The build has either failed, or not been triggered due to previous failures + /// + Failed, + /// + /// The build has either been successfully executed, or already up-to-date + /// + Succeeded, + /// + /// The build has been cancelled. + /// + Cancelled, +} + +/// +/// An event arguments class containing information about a thumbnail creation. +/// +public class ThumbnailBuiltEventArgs : EventArgs +{ + /// + /// Gets the id of the asset whose thumbnail has been built. + /// + public AssetId AssetId { get; internal set; } + + /// + /// Gets the url of the asset whose thumbnail has been built. + /// + public UFile Url { get; internal set; } + + /// + /// Gets the value indicating if the result of the build + /// + public ThumbnailBuildResult Result { get; internal set; } + + /// + /// Gets the value indicating if the built thumbnail is different from its previous version. + /// + public bool ThumbnailChanged { get; internal set; } + + /// + /// Gets the stream to the thumbnail PNG file corresponding to the item. + /// + /// This property is null if is not . + public Stream ThumbnailStream { get; internal set; } + + /// + /// Gets the hash of the stream to the thumbnail PNG file corresponding to the item. + /// + /// This property is equal to if is not . + public ObjectId ThumbnailId { get; internal set; } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommand.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommand.cs new file mode 100644 index 0000000000..4e8db77e2b --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommand.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Core.Serialization; + +namespace Stride.Editor.Thumbnails; + +/// +/// The base command to build thumbnails. +/// This command overrides so that it automatically returns all the item asset reference files. +/// By doing so the thumbnail is re-generated every time one of the dependencies changes. +/// +public abstract class ThumbnailCommand : AssetCommand +{ + private readonly AssetItem assetItem; + + protected ThumbnailCommand(string url, AssetItem assetItem, ThumbnailCommandParameters parameters, IAssetFinder assetFinder) + : base(url, parameters, assetFinder) + { + if (assetItem == null) throw new ArgumentNullException(nameof(assetItem)); + if (assetItem.Package == null) throw new ArgumentException("assetItem is not attached to a package"); + if (assetItem.Package.Session == null) throw new ArgumentException("assetItem is not attached to a package session"); + if (url == null) throw new ArgumentNullException(nameof(url)); + + this.assetItem = assetItem; + //InputFilesGetter = GetInputFilesImpl; + } + + protected override void ComputeParameterHash(BinarySerializationWriter writer) + { + base.ComputeParameterHash(writer); + var dependencies = assetItem.Package.Session.DependencyManager.ComputeDependencies(assetItem.Id, AssetDependencySearchOptions.Out | AssetDependencySearchOptions.Recursive, ContentLinkType.Reference); + if (dependencies != null) + { + foreach (var assetReference in dependencies.LinksOut) + { + var refAsset = assetReference.Item.Asset; + writer.SerializeExtended(ref refAsset, ArchiveMode.Serialize); + } + } + } + +// private IEnumerable GetInputFilesImpl() +// { +// var dependencies = assetItem.Package.Session.DependencyManager.ComputeDependencies(assetItem.Id, AssetDependencySearchOptions.Out | AssetDependencySearchOptions.Recursive, ContentLinkType.Reference); +// if (dependencies != null) +// { +// foreach (var assetReference in dependencies.LinksOut) +// yield return new ObjectUrl(UrlType. +// ContentLink, assetReference.Item.Location); +// } +// } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommandParameters.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommandParameters.cs new file mode 100644 index 0000000000..5a7814839a --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCommandParameters.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core; +using Stride.Core.Mathematics; +using Stride.Assets; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// +/// The minimum parameters needed by a thumbnail build command. +/// +[DataContract] +public class ThumbnailCommandParameters +{ + public ThumbnailCommandParameters() + { + } + + public ThumbnailCommandParameters(Asset asset, string thumbnailUrl, Int2 thumbnailSize) + { + Asset = asset; + ThumbnailUrl = thumbnailUrl; + ThumbnailSize = thumbnailSize; + } + + public Asset Asset; + + public string ThumbnailUrl; // needed to force re-calculation of thumbnails when asset file is move + + public Int2 ThumbnailSize; + + public ColorSpace ColorSpace { get; set; } + + public RenderingMode RenderingMode { get; set; } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilationContext.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilationContext.cs new file mode 100644 index 0000000000..36bf02f2a8 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilationContext.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets.Compiler; + +namespace Stride.Editor.Thumbnails; + +public class ThumbnailCompilationContext : ICompilationContext +{ +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerBase.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerBase.cs new file mode 100644 index 0000000000..effeaa0900 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerBase.cs @@ -0,0 +1,197 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Core.Assets.Diagnostics; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.IO; +using Stride.Core.Serialization.Contents; +using Stride.Core.Storage; + +namespace Stride.Editor.Thumbnails; + +/// +/// Base implementation for suitable to build a thumbnail of a single type of . +/// +/// Type of the asset +public abstract class ThumbnailCompilerBase : IThumbnailCompiler where T : Asset +{ + protected const string ThumbnailStorageNamePrefix = "__THUMBNAIL__"; + + /// + /// The typed asset associated to + /// + protected T Asset; + + private class ThumbnailFailureBuildStep : BuildStep + { + public ThumbnailFailureBuildStep(IEnumerable messages) + { + messages.ForEach(x => Logger.Log(x)); + } + + public override string Title { get { return "FailureThumbnail"; } } + + public override Task Execute(IExecuteContext executeContext, BuilderContext builderContext) + { + return Task.FromResult(ResultStatus.Failed); + } + + public override string ToString() + { + return Title; + } + } + + /// + public int Priority { get; set; } + + /// + public bool IsStatic { get; set; } + + /// + /// Compiles the asset from the specified package. + /// + /// The thumbnail compile context + /// The absolute URL to the asset's thumbnail, relative to the storage. + /// The asset to compile + /// + /// The result where the commands and logs should be output. + protected abstract void CompileThumbnail(ThumbnailCompilerContext context, string thumbnailStorageUrl, AssetItem assetItem, Package originalPackage, AssetCompilerResult result); + + protected virtual string BuildThumbnailStoreName(UFile assetUrl) + { + return assetUrl.GetDirectoryAndFileNameWithoutExtension().Insert(0, ThumbnailStorageNamePrefix); + } + + private static void OnThumbnailStepProcessed(ThumbnailCompilerContext context, AssetItem assetItem, string thumbnailStorageUrl, BuildStepEventArgs buildStepEventArgs) + { + // returns immediately if the user has not subscribe to the event + if (!context.ShouldNotifyThumbnailBuilt) + return; + + // TODO: the way to get last build step (which should be thumbnail, not its dependencies) should be done differently, at the compiler level + // (we need to generate two build step that can be accessed directly, one for dependency and one for thumbnail) + var lastBuildStep = buildStepEventArgs.Step is ListBuildStep ? ((ListBuildStep)buildStepEventArgs.Step).Steps.LastOrDefault() ?? buildStepEventArgs.Step : buildStepEventArgs.Step; + + // Retrieving build result + var result = ThumbnailBuildResult.Failed; + if (lastBuildStep.Succeeded) + result = ThumbnailBuildResult.Succeeded; + else if (lastBuildStep.Status == ResultStatus.Cancelled) + result = ThumbnailBuildResult.Cancelled; + + // TODO: Display error logo if anything else went wrong? + + var changed = lastBuildStep.Status != ResultStatus.NotTriggeredWasSuccessful; + + // Open the image data stream if the build succeeded + Stream thumbnailStream = null; + ObjectId thumbnailHash = ObjectId.Empty; + + if (lastBuildStep.Succeeded) + { + thumbnailStream = MicrothreadLocalDatabases.DatabaseFileProvider.OpenStream(thumbnailStorageUrl, VirtualFileMode.Open, VirtualFileAccess.Read); + thumbnailHash = MicrothreadLocalDatabases.DatabaseFileProvider.ContentIndexMap[thumbnailStorageUrl]; + } + + try + { + context.NotifyThumbnailBuilt(assetItem, result, changed, thumbnailStream, thumbnailHash); + } + finally + { + // Close the image data stream if opened + if (thumbnailStream != null) + { + thumbnailStream.Dispose(); + } + } + } + + public AssetCompilerResult Prepare(AssetCompilerContext context, AssetItem assetItem) + { + var compilerResult = new AssetCompilerResult(); + + Asset = (T)assetItem.Asset; + var thumbnailCompilerContext = (ThumbnailCompilerContext)context; + + // Build the path of the thumbnail in the storage + var thumbnailStorageUrl = BuildThumbnailStoreName(assetItem.Location); + + // Check if this asset produced any error + // (dependent assets errors are generally ignored as long as thumbnail could be generated, + // but we will add a thumbnail overlay to indicate the state is not good) + var currentAssetHasErrors = false; + + try + { + CompileThumbnail(thumbnailCompilerContext, thumbnailStorageUrl, assetItem, assetItem.Package, compilerResult); + } + catch (Exception) + { + // If an exception occurs, ensure that the build of thumbnail will fail. + compilerResult.Error($"An exception occurred while compiling the asset [{assetItem.Location}]"); + } + + foreach (var logMessage in compilerResult.Messages) + { + // Ignore anything less than error + if (!logMessage.IsAtLeast(LogMessageType.Error)) + continue; + + // Check if there is any non-asset log message + // (they are probably just emitted by current compiler, so they concern current asset) + // TODO: Maybe we should wrap every message in AssetLogMessage before copying them in compilerResult? + var assetLogMessage = logMessage as AssetLogMessage; + if (assetLogMessage == null) + { + currentAssetHasErrors = true; + break; + } + + // If it was an asset log message, check it concerns current asset + if (assetLogMessage.AssetReference != null && assetLogMessage.AssetReference.Location == assetItem.Location) + { + currentAssetHasErrors = true; + break; + } + } + if (currentAssetHasErrors) + { + // if a problem occurs while compiling, we add a special build step that will always fail. + compilerResult.BuildSteps.Add(new ThumbnailFailureBuildStep(compilerResult.Messages)); + } + + var currentAsset = assetItem; // copy the current asset item and embrace it in the callback + compilerResult.BuildSteps.StepProcessed += (_, buildStepArgs) => OnThumbnailStepProcessed(thumbnailCompilerContext, currentAsset, thumbnailStorageUrl, buildStepArgs); + + return compilerResult; + } + + public virtual IEnumerable GetInputFiles(AssetItem assetItem) + { + yield break; + } + + public virtual IEnumerable GetInputTypes(AssetItem assetItem) + { + yield break; + } + + public virtual IEnumerable GetInputTypesToExclude(AssetItem assetItem) + { + yield break; + } + + public virtual bool AlwaysCheckRuntimeTypes { get; } = true; + + public IEnumerable GetRuntimeTypes(AssetItem assetItem) + { + yield break; + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerContext.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerContext.cs new file mode 100644 index 0000000000..5c3bff3663 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailCompilerContext.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Diagnostics; +using Stride.Core.Mathematics; +using Stride.Core.Storage; +using Stride.Editor.Resources; +using Stride.Graphics; + +namespace Stride.Editor.Thumbnails; + +/// +/// The context used when building the thumbnail of an asset in a Package. +/// +public class ThumbnailCompilerContext : AssetCompilerContext +{ + private readonly object thumbnailCounterLock = new(); + + /// + /// Initializes a new instance of the class. + /// + public ThumbnailCompilerContext() + { + ThumbnailResolution = 128 * Int2.One; + CompilationContext = typeof(ThumbnailCompilationContext); + } + + /// + /// Gets the desired resolution for thumbnails. + /// + public Int2 ThumbnailResolution { get; private set; } + + /// + /// Indicate whether the fact that a thumbnail has been built should be notified with + /// + /// Use this property to avoid doing unnecessary stream operation when event has no subscriber. + public bool ShouldNotifyThumbnailBuilt => ThumbnailBuilt != null; + + /// + /// The array of data representing the thumbnail to display when a thumbnail build failed. + /// + public static Task BuildFailedThumbnail = Task.Run(() => HandleBrokenThumbnail()); + + /// + /// The event raised when a thumbnail has finished to build. + /// + public event EventHandler ThumbnailBuilt; + + /// + /// Notify that the thumbnail has just been built. This method will raise the event. + /// + /// The asset item whose thumbnail has been built. + /// A value indicating whether the build was successful, failed or cancelled. + /// A boolean indicating whether the thumbnal has changed since the last generation. + /// A stream to the thumbnail image. + /// + internal void NotifyThumbnailBuilt(AssetItem assetItem, ThumbnailBuildResult result, bool changed, Stream thumbnailStream, ObjectId thumbnailHash) + { + try + { + // TODO: this lock seems to be useless now, check if we can safely remove it + Monitor.Enter(thumbnailCounterLock); + var handler = ThumbnailBuilt; + if (handler != null) + { + // create the thumbnail build event arguments + var thumbnailBuiltArgs = new ThumbnailBuiltEventArgs + { + AssetId = assetItem.Id, + Url = assetItem.Location, + Result = result, + ThumbnailChanged = changed + }; + + Monitor.Exit(thumbnailCounterLock); + + // Open the image data stream if the build succeeded + if (thumbnailBuiltArgs.Result == ThumbnailBuildResult.Succeeded) + { + thumbnailBuiltArgs.ThumbnailStream = thumbnailStream; + thumbnailBuiltArgs.ThumbnailId = thumbnailHash; + } + else if (BuildFailedThumbnail != null) + { + thumbnailBuiltArgs.ThumbnailStream = new MemoryStream(BuildFailedThumbnail.Result); + } + handler(assetItem, thumbnailBuiltArgs); + } + } + finally + { + if (Monitor.IsEntered(thumbnailCounterLock)) + Monitor.Exit(thumbnailCounterLock); + } + } + + private static byte[] HandleBrokenThumbnail() + { + // Load broken asset thumbnail + var assetBrokenThumbnail = DefaultThumbnails.AssetBroken; + + // Apply thumbnail status in corner + ThumbnailBuildHelper.ApplyThumbnailStatus(assetBrokenThumbnail, LogMessageType.Error); + var memoryStream = new MemoryStream(); + assetBrokenThumbnail.Save(memoryStream, ImageFileType.Png); + + return memoryStream.ToArray(); + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromEntityCommand.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromEntityCommand.cs new file mode 100644 index 0000000000..8b8fc686cb --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromEntityCommand.cs @@ -0,0 +1,177 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Editor.Engine; +using Stride.Engine; +using Stride.Graphics; +using Stride.Particles.Rendering; +using Stride.Rendering; +using Stride.Rendering.Compositing; +using Stride.Rendering.Lights; +using Stride.SpriteStudio.Runtime; + +namespace Stride.Editor.Thumbnails; + +/// +/// A command that creates the thumbnail by rendering an entity. +/// +/// The runtime type of the asset +public abstract class ThumbnailFromEntityCommand : StrideThumbnailCommand + where TRuntimeAsset : class +{ + private static readonly string ThumbnailEntityGraphicsCompositorKey = nameof(ThumbnailEntityGraphicsCompositorKey); + public const string EditorForwardShadingEffect = "StrideEditorForwardShadingEffect"; + + /// + /// The root entity used for the thumbnail. + /// + protected Entity Entity; + + protected ThumbnailFromEntityCommand(ThumbnailCompilerContext context, AssetItem assetItem, IAssetFinder assetFinder, string url, ThumbnailCommandParameters parameters) + : base(context, assetItem, assetFinder, url, parameters) + { + } + + /// + protected override string GraphicsCompositorKey => ThumbnailEntityGraphicsCompositorKey + "_" + ModelEffectName; + + protected virtual string ModelEffectName => EditorForwardShadingEffect; + + /// + protected override GraphicsCompositor CreateSharedGraphicsCompositor(GraphicsDevice device) + { + var result = GraphicsCompositorHelper.CreateDefault(false, ModelEffectName, null, ThumbnailBackgroundColor); + + var opaqueStage = result.RenderStages.First(x => x.Name.Equals("Opaque")); + var transparentStage = result.RenderStages.First(x => x.Name.Equals("Transparent")); + + // Add particles, UI and SpriteStudio renderers + result.RenderFeatures.Add( + new ParticleEmitterRenderFeature + { + RenderStageSelectors = + { + new ParticleEmitterTransparentRenderStageSelector + { + EffectName = "Particles", + OpaqueRenderStage = opaqueStage, + TransparentRenderStage = transparentStage, + } + }, + }); + + result.RenderFeatures.Add( + new SpriteStudioRenderFeature + { + RenderStageSelectors = + { + new SimpleGroupToRenderStageSelector() + { + EffectName = "SpriteStudio", + RenderStage = transparentStage, + } + } + }); + + return result; + } + + /// + /// Create the entity to display in the thumbnail. + /// + protected abstract Entity CreateEntity(); + + /// + /// Unload and destroy the entity used in the thumbnail. + /// + /// + protected Task DestroyEntity() + { + return Task.FromResult(true); + } + + protected override Scene CreateScene(GraphicsCompositor graphicsCompositor) + { + // create the entity preview scene + var entityScene = new Scene(); + + Entity = CreateEntity(); + if (Entity == null) + return null; + + AdjustEntity(); + + var camera = CreateCamera(graphicsCompositor); + entityScene.Entities.Add(camera.Entity); + + SetupLighting(entityScene); + + entityScene.Entities.Add(Entity); + + return entityScene; + } + + protected virtual CameraComponent CreateCamera(GraphicsCompositor graphicsCompositor) + { + var cameraComponent = new CameraComponent + { + Slot = new SceneCameraSlotId(graphicsCompositor.Cameras[0].Id), + UseCustomAspectRatio = true, + AspectRatio = Parameters.ThumbnailSize.X / (float)Parameters.ThumbnailSize.Y, + }; + + // setup the camera + var cameraEntity = new Entity("Thumbnail Camera") { cameraComponent }; + var cameraToFront = new Vector2(1f / MathF.Tan(MathUtil.DegreesToRadians(cameraComponent.VerticalFieldOfView / 2 * cameraComponent.AspectRatio)), + 1f / MathF.Tan(MathUtil.DegreesToRadians(cameraComponent.VerticalFieldOfView / 2))); + var cameraDistanceFromCenter = 1f + Math.Max(cameraToFront.X, cameraToFront.Y); // we want the front face of the element to be fully visible (not only center) + cameraEntity.Transform.Position = new Vector3(0, 0, cameraDistanceFromCenter); + + // rotate a bit the camera to have a nice viewing angle. + var rotationQuaternion = Quaternion.RotationX(-MathUtil.Pi / 6) * Quaternion.RotationY(-MathUtil.Pi / 4); + rotationQuaternion.Rotate(ref cameraEntity.Transform.Position); + cameraEntity.Transform.Rotation = Quaternion.RotationX(-MathUtil.Pi / 6) * Quaternion.RotationY(-MathUtil.Pi / 4); + + cameraComponent.NearClipPlane = cameraDistanceFromCenter / 50f; + cameraComponent.FarClipPlane = cameraDistanceFromCenter * 50f; + + return cameraComponent; + } + protected virtual void SetupLighting(Scene scene) + { + // Depending on RenderingMode, use HDR or LDR settings for the lights + var factor = 1.0f; + if (Parameters.RenderingMode == RenderingMode.HDR) + { + factor = 5.0f; + } + + var ambientLight = new Entity("Thumbnail Ambient Light1") { new LightComponent { Type = new LightAmbient(), Intensity = 0.02f } }; + var frontDirectionalLight = new Entity("Thumbnail Directional Front") { new LightComponent { Intensity = 0.07f * factor } }; + var topDirectionalLight = new Entity("Thumbnail Directional Top") { new LightComponent { Intensity = 0.8f * factor } }; + topDirectionalLight.Transform.Rotation = Quaternion.RotationX(MathUtil.DegreesToRadians(-80)); + scene.Entities.Add(ambientLight); + scene.Entities.Add(frontDirectionalLight); + scene.Entities.Add(topDirectionalLight); + } + + protected virtual void AdjustEntity() + { + var boundingSphere = Entity.CalculateBoundSphere(); + + Entity.Transform.Scale = boundingSphere.Radius > MathUtil.ZeroTolerance ? new Vector3(1f / boundingSphere.Radius) : Vector3.One; + Entity.Transform.Position = -Entity.Transform.Scale * boundingSphere.Center; + } + + protected override void DestroyScene(Scene scene) + { + base.DestroyScene(scene); + + scene.Entities.Remove(Entity); + scene.Dispose(); + DestroyEntity(); + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromSpriteBatchCommand.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromSpriteBatchCommand.cs new file mode 100644 index 0000000000..7f2663bf2a --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromSpriteBatchCommand.cs @@ -0,0 +1,93 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering; +using Stride.Rendering.Compositing; + +namespace Stride.Editor.Thumbnails; + +internal interface IThumbnailFromSpriteBatchCommand +{ + void RenderSprites(RenderDrawContext context); +} + +internal static class ThumbnailFromSpriteBatchCommand +{ + public static readonly PropertyKey Key = new(nameof(ThumbnailFromSpriteBatchCommand), typeof(ThumbnailFromSpriteBatchCommand)); +} + +/// +/// A command that creates the thumbnail using the sprite batch +/// +/// The type of the runtime object asset to load +public abstract class ThumbnailFromSpriteBatchCommand : StrideThumbnailCommand, IThumbnailFromSpriteBatchCommand where TRuntimeAsset : class +{ + private static readonly string ThumbnailSpriteBatchGraphicsCompositorKey = nameof(ThumbnailSpriteBatchGraphicsCompositorKey); + private readonly Scene spriteScene; + protected EffectInstance EffectInstance; + + protected ThumbnailFromSpriteBatchCommand(ThumbnailCompilerContext context, AssetItem assetItem, IAssetFinder assetFinder, string url, ThumbnailCommandParameters parameters) + : base(context, assetItem, assetFinder, url, parameters) + { + spriteScene = new Scene(); + spriteScene.Tags.Add(ThumbnailFromSpriteBatchCommand.Key, this); + EffectInstance = null; + // Always render spritebatch in LDR mode + Parameters.RenderingMode = RenderingMode.LDR; + } + + /// + protected override string GraphicsCompositorKey => ThumbnailSpriteBatchGraphicsCompositorKey; + + /// + protected override Scene CreateScene(GraphicsCompositor graphicsCompositor) + { + return spriteScene; + } + + /// + protected override GraphicsCompositor CreateSharedGraphicsCompositor(GraphicsDevice device) + { + return new GraphicsCompositor + { + Game = new SceneRendererCollection + { + new ClearRenderer { Color = ThumbnailBackgroundColor }, + new DelegateSceneRenderer(SafeRenderSprites), + } + }; + } + + private static void SafeRenderSprites(RenderDrawContext context) + { + // Note: this assumes that the Scene returned by CreateScene is the first child scene of the RootScene. Changing this in ThumbnailGenerator will break this code! + var command = SceneInstance.GetCurrent(context.RenderContext).RootScene.Children.First().Tags.Get(ThumbnailFromSpriteBatchCommand.Key); + command.RenderSprites(context); + } + + /// + /// Renders the sprites for this thumbnail. + /// + /// The to use to render the sprites. + protected abstract void RenderSprites(RenderDrawContext context); + + /// + void IThumbnailFromSpriteBatchCommand.RenderSprites(RenderDrawContext context) + { + try + { + // draws + SpriteBatch.Begin(context.GraphicsContext, SpriteSortMode.Deferred, null, null, null, null, EffectInstance); + RenderSprites(context); + } + finally + { + SpriteBatch.End(); + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromTextureCommand.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromTextureCommand.cs new file mode 100644 index 0000000000..ea351e2f5c --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailFromTextureCommand.cs @@ -0,0 +1,103 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering; +using Stride.Rendering.Compositing; + +namespace Stride.Editor.Thumbnails; + +/// +/// A command that creates the thumbnail using a background texture and title text +/// +/// The type of the runtime object asset to load +public abstract class ThumbnailFromTextureCommand : ThumbnailFromSpriteBatchCommand where TRuntimeAsset : class +{ + /// + /// The background image used when taking the thumbnail. + /// + protected Texture BackgroundTexture; + + /// + /// The color used when rendering the thumbnail background. + /// + protected Color BackgroundColor = Color.White; + + /// + /// The color added to the background when rendering the thumbnail. + /// + protected Color AdditiveColor = Color.Zero; + + /// + /// Swizzle mode for sampling (RGBA, RRRR, RRR1) + /// + protected SwizzleMode Swizzle = SwizzleMode.None; + + /// + /// The font used when taking the thumbnail. + /// + protected SpriteFont Font; + + /// + /// The color used when rendering the thumbnail text. + /// + protected Color FontColor = Color.White; + + /// + /// The size used when rendering the font. + /// + protected float FontSize = 0.95f; + + /// + /// The text to draw when taking the thumbnail. + /// + protected string TitleText; + + protected ThumbnailFromTextureCommand(ThumbnailCompilerContext context, AssetItem assetItem, IAssetFinder assetFinder, string url, ThumbnailCommandParameters parameters) + : base(context, assetItem, assetFinder, url, parameters) + { + } + + protected override Scene CreateScene(GraphicsCompositor graphicsCompositor) + { + SetThumbnailParameters(); + + return base.CreateScene(graphicsCompositor); + } + + protected abstract void SetThumbnailParameters(); + + protected override void RenderSprites(RenderDrawContext context) + { + var thumbnailSize = new Vector2(context.CommandList.RenderTarget.ViewWidth, context.CommandList.RenderTarget.ViewHeight); + + // the background texture + if (BackgroundTexture != null) + SpriteBatch.Draw(BackgroundTexture, new RectangleF(0, 0, thumbnailSize.X, thumbnailSize.Y), null, BackgroundColor, 0, Vector2.Zero, SpriteEffects.None, ImageOrientation.AsIs, 0, AdditiveColor, Swizzle); + + if (Font != null) + { + // Measure the type name to draw and calculate the scale factor needed for the name to enter the thumbnail + var typeNameSize = Font.MeasureString(TitleText); + var scale = FontSize * Math.Min(thumbnailSize.X / typeNameSize.X, thumbnailSize.Y / typeNameSize.Y); + var desiredFontSize = scale * Font.Size; + + if (Font.FontType == SpriteFontType.Dynamic) + { + scale = 1f; + + // Get the exact size of the font rendered with the desired size + typeNameSize = Font.MeasureString(TitleText, desiredFontSize); + + // force pre-generation of the glyph + Font.PreGenerateGlyphs(TitleText, new Vector2(desiredFontSize, desiredFontSize)); + } + + // the title text + SpriteBatch.DrawString(Font, TitleText, desiredFontSize, thumbnailSize/2, FontColor, 0, typeNameSize/2, scale*Vector2.One, SpriteEffects.None, 1, TextAlignment.Center); + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailGenerator.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailGenerator.cs new file mode 100644 index 0000000000..9906bc51b6 --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailGenerator.cs @@ -0,0 +1,364 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Assets.SpriteFont; +using Stride.Assets.SpriteFont.Compiler; +using Stride.Core; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Core.Serialization.Contents; +using Stride.Engine; +using Stride.Engine.Design; +using Stride.Games; +using Stride.Graphics; +using Stride.Graphics.Font; +using Stride.Physics; +using Stride.Rendering; +using Stride.Rendering.Compositing; +using Stride.Rendering.Fonts; +using Stride.Shaders.Compiler; +using Stride.UI; + +namespace Stride.Editor.Thumbnails; + +public class ThumbnailGenerator : IDisposable +{ + /// + /// A constant string that can be used to identify an attribute of type in collections. + /// + public static readonly PropertyKey Key = new("ThumbnailGeneratorKey", typeof(ThumbnailGenerator)); + + /// + /// The list of services used for the thumbnails + /// + public readonly IServiceRegistry Services; + + /// + /// The preview game system collection. + /// + private readonly GameSystemCollection gameSystems; + + /// + /// The asset manager to use when building thumbnails. + /// + public readonly ContentManager ContentManager; + + /// + /// Gets the instance of graphics device dedicated to thumbnail generation. + /// + public GraphicsDevice GraphicsDevice { get; } + + public CommandList GraphicsCommandList { get; } + + public GraphicsContext GraphicsContext { get; set; } + + private readonly SceneSystem sceneSystem; + + public EffectSystem EffectSystem { get; } + + public IGraphicsDeviceService GraphicsDeviceService { get; private set; } + + /// + /// An instance of sprite batch that can be used to draw text or images. + /// + public readonly SpriteBatch SpriteBatch; + + /// + /// An instance of ui batch that can be used to draw text or images, including sdf fonts. + /// + public readonly UIBatch UIBatch; + + /// + /// A default font that can be used when rendering previews or thumbnails + /// + public readonly SpriteFont DefaultFont; + + private readonly GameTime nullGameTime; + + private readonly GameFontSystem fontSystem; + + private readonly object lockObject = new(); + + private readonly HashSet thumbnailGraphicsCompositors = new(); + + /// + /// The main scene used to render thumbnails. + /// + private Scene thumbnailScene; + + public ThumbnailGenerator(EffectCompilerBase effectCompiler) + { + // create base services + Services = new ServiceRegistry(); + Services.AddService(MicrothreadLocalDatabases.ProviderService); + ContentManager = new ContentManager(Services); + Services.AddService(ContentManager); + Services.AddService(ContentManager); + + GraphicsDevice = GraphicsDevice.New(); + GraphicsContext = new GraphicsContext(GraphicsDevice); + GraphicsCommandList = GraphicsContext.CommandList; + Services.AddService(GraphicsContext); + sceneSystem = new SceneSystem(Services); + Services.AddService(sceneSystem); + fontSystem = new GameFontSystem(Services); + Services.AddService(fontSystem.FontSystem); + Services.AddService(fontSystem.FontSystem); + + GraphicsDeviceService = new GraphicsDeviceServiceLocal(Services, GraphicsDevice); + Services.AddService(GraphicsDeviceService); + + var uiSystem = new UISystem(Services); + Services.AddService(uiSystem); + + var physicsSystem = new Bullet2PhysicsSystem(Services); + Services.AddService(physicsSystem); + + gameSystems = new GameSystemCollection(Services) { fontSystem, uiSystem, physicsSystem }; + Services.AddService(gameSystems); + Simulation.DisableSimulation = true; //make sure we do not simulate physics within the editor + + // initialize base services + gameSystems.Initialize(); + + // create remaining services + EffectSystem = new EffectSystem(Services); + Services.AddService(EffectSystem); + + gameSystems.Add(EffectSystem); + gameSystems.Add(sceneSystem); + EffectSystem.Initialize(); + + // Mount the same database for the cache + EffectSystem.Compiler = EffectCompilerFactory.CreateEffectCompiler(effectCompiler.FileProvider, EffectSystem); + + // Deactivate the asynchronous effect compilation + ((EffectCompilerCache)EffectSystem.Compiler).CompileEffectAsynchronously = false; + + // load game system content + gameSystems.LoadContent(); + + // create the default fonts + var fontItem = OfflineRasterizedSpriteFontFactory.Create(); + fontItem.FontType.Size = 22; + DefaultFont = OfflineRasterizedFontCompiler.Compile(fontSystem.FontSystem, fontItem, true); + + // create utility members + nullGameTime = new GameTime(); + SpriteBatch = new SpriteBatch(GraphicsDevice); + UIBatch = new UIBatch(GraphicsDevice); + + // create the pipeline + SetUpPipeline(); + } + + private void SetUpPipeline() + { + // create the main preview scene. + thumbnailScene = new Scene(); + + // create and set the main scene instance + sceneSystem.SceneInstance = new SceneInstance(Services, thumbnailScene, ExecutionMode.Thumbnail); + } + + /// + /// The micro-thread in charge of processing the thumbnail build requests and creating the thumbnails. + /// + private ResultStatus ProcessThumbnailRequests(ThumbnailBuildRequest request) + { + var status = ResultStatus.Successful; + + // Global lock so that only one rendering happens at the same time + lock (lockObject) + { + try + { + lock (AssetBuilderService.OutOfMicrothreadDatabaseLock) + { + MicrothreadLocalDatabases.MountCommonDatabase(); + + // set the master output + var renderTarget = GraphicsContext.Allocator.GetTemporaryTexture2D(request.Size.X, request.Size.Y, request.ColorSpace == ColorSpace.Linear ? PixelFormat.R8G8B8A8_UNorm_SRgb : PixelFormat.R8G8B8A8_UNorm, TextureFlags.ShaderResource | TextureFlags.RenderTarget); + var depthStencil = GraphicsContext.Allocator.GetTemporaryTexture2D(request.Size.X, request.Size.Y, PixelFormat.D24_UNorm_S8_UInt, TextureFlags.DepthStencil); + + try + { + // Fake presenter + // TODO GRAPHICS REFACTOR: Try to remove that + GraphicsDevice.Presenter = new RenderTargetGraphicsPresenter(GraphicsDevice, renderTarget, depthStencil.ViewFormat); + + // Always clear the state of the GraphicsDevice to make sure a scene doesn't start with a wrong setup + GraphicsCommandList.ClearState(); + + // Setup the color space when rendering a thumbnail + GraphicsDevice.ColorSpace = request.ColorSpace; + + // render the thumbnail + thumbnailScene.Children.Add(request.Scene); + + // Store the graphics compositor to use, so we can dispose it when disposing this ThumbnailGenerator + thumbnailGraphicsCompositors.Add(request.GraphicsCompositor); + sceneSystem.GraphicsCompositor = request.GraphicsCompositor; + + // Render once to setup render processors + // TODO GRAPHICS REFACTOR: Should not require two rendering + GraphicsContext.ResourceGroupAllocator.Reset(GraphicsContext.CommandList); + gameSystems.Draw(nullGameTime); + + // Draw + gameSystems.Update(nullGameTime); + GraphicsContext.ResourceGroupAllocator.Reset(GraphicsContext.CommandList); + gameSystems.Draw(nullGameTime); + + // write the thumbnail to the file + using (var thumbnailImage = renderTarget.GetDataAsImage(GraphicsCommandList)) + using (var outputImageStream = request.FileProvider.OpenStream(request.Url, VirtualFileMode.Create, VirtualFileAccess.Write)) + { + request.PostProcessThumbnail?.Invoke(thumbnailImage); + + ThumbnailBuildHelper.ApplyThumbnailStatus(thumbnailImage, request.DependencyBuildStatus); + + thumbnailImage.Save(outputImageStream, ImageFileType.Png); + + request.Logger.Info($"Thumbnail creation successful [{request.Url}] to ({thumbnailImage.Description.Width}x{thumbnailImage.Description.Height},{thumbnailImage.Description.Format})"); + } + } + finally + { + // Cleanup the scene + thumbnailScene.Children.Clear(); + sceneSystem.GraphicsCompositor = null; + + GraphicsContext.Allocator.ReleaseReference(depthStencil); + GraphicsContext.Allocator.ReleaseReference(renderTarget); + } + + MicrothreadLocalDatabases.UnmountDatabase(); + } + } + catch (Exception e) + { + status = ResultStatus.Failed; + request.Logger.Error("An exception occurred while processing thumbnail request.", e); + } + } + + return status; + } + + /// + /// Request a thumbnail build action to the preview game. + /// + /// The url of the thumbnail on the storage + /// The scene to use to draw the thumbnail + /// The graphics compositor used to render the scene + /// The file provider to use when executing the build request. + /// The size of the thumbnail to create + /// + /// the rendering mode (hdr or ldr). + /// The logger + /// The dependency build status log level + /// The post-process code to customize thumbnail. + /// A task on which the user can wait for the thumbnail completion + public ResultStatus BuildThumbnail(string thumbnailUrl, Scene scene, GraphicsCompositor graphicsCompositor, DatabaseFileProvider provider, Int2 thumbnailSize, ColorSpace colorSpace, RenderingMode renderingMode, ILogger logger, LogMessageType logLevel, PostProcessThumbnailDelegate postProcessThumbnail = null) + { + return ProcessThumbnailRequests(new ThumbnailBuildRequest(thumbnailUrl, scene, graphicsCompositor, provider, thumbnailSize, colorSpace, renderingMode, logger, logLevel) { PostProcessThumbnail = postProcessThumbnail }); + } + + public void Dispose() + { + // destroy all game systems + thumbnailGraphicsCompositors.ForEach(x => x.Dispose()); + sceneSystem.Dispose(); + fontSystem.Dispose(); + EffectSystem.Dispose(); + GraphicsDevice.Dispose(); + } + + public delegate void PostProcessThumbnailDelegate(Image image); + + /// + /// Class representing a thumbnail build request. + /// + private class ThumbnailBuildRequest + { + /// + /// The command context of the build engine that asked for the thumbnail. + /// + public readonly ILogger Logger; + + /// + /// The url to where to save the created thumbnail + /// + public readonly string Url; + + /// + /// The size of the thumbnail. + /// + public readonly Int2 Size; + + /// + /// The color space of the thumbnail + /// + public readonly ColorSpace ColorSpace; + + /// + /// The rendering mode used for the thumbnails. + /// + public readonly RenderingMode RenderingMode; + + /// + /// The build drawAction to perform + /// + public readonly Scene Scene; + + /// + /// The graphics compositor used to render the thumbnail + /// + public readonly GraphicsCompositor GraphicsCompositor; + + /// + /// The file provider to use for the request + /// + public readonly DatabaseFileProvider FileProvider; + + /// + /// The dependent build step (to check status against) + /// + public readonly LogMessageType DependencyBuildStatus; + + /// + /// Post process the thumbnail. + /// + public PostProcessThumbnailDelegate PostProcessThumbnail; + + /// + /// Create a new thumbnail request from entity. + /// + /// The Url of the thumbnail + /// The scene describing the thumbnail to draw + /// The provider to use for the request. + /// The desired size of the thumbnail + /// The color space. + /// the rendering mode (hdr or ldr). + /// The logger + /// The dependency build status log level + public ThumbnailBuildRequest(string thumbnailUrl, Scene scene, GraphicsCompositor graphicsCompositor, DatabaseFileProvider provider, Int2 thumbnailSize, ColorSpace colorSpace, RenderingMode renderingMode, ILogger logger, LogMessageType logLevel) + { + Logger = logger; + Url = thumbnailUrl; + Size = thumbnailSize; + Scene = scene; + GraphicsCompositor = graphicsCompositor; + FileProvider = provider; + DependencyBuildStatus = logLevel; + ColorSpace = colorSpace; + RenderingMode = renderingMode; + } + } +} diff --git a/sources/editor/Stride.Editor/Thumbnails/ThumbnailListCompiler.cs b/sources/editor/Stride.Editor/Thumbnails/ThumbnailListCompiler.cs new file mode 100644 index 0000000000..b41117609c --- /dev/null +++ b/sources/editor/Stride.Editor/Thumbnails/ThumbnailListCompiler.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.BuildEngine; +using Stride.Editor.Preview; + +namespace Stride.Editor.Thumbnails; + +/// +/// A thumbnail list compiler. +/// This compiler creates the list of build steps to perform to produce the thumbnails of an list of . +/// +public class ThumbnailListCompiler : ItemListCompiler +{ + private readonly ThumbnailGenerator generator; + private readonly EventHandler builtAction; + private readonly AssetDependenciesCompiler dependenciesCompiler = new(typeof(PreviewCompilationContext)); + + /// + /// Creates an instance of . + /// + public ThumbnailListCompiler(ThumbnailGenerator generator, EventHandler builtAction, AssetCompilerRegistry thumbnailCompilerRegistry) + : base(thumbnailCompilerRegistry, typeof(ThumbnailCompilationContext)) + { + if (generator == null) throw new ArgumentNullException(nameof(generator)); + if (thumbnailCompilerRegistry == null) throw new ArgumentNullException(nameof(thumbnailCompilerRegistry)); + + this.generator = generator; + this.builtAction = builtAction; + } + + /// + /// Generates a to compile the thumbnail of the given asset. + /// + /// The asset to compile. + /// The current game settings + /// If the asset has to be compiled as well in the case of non static thumbnail + /// A containing the build steps to generate the thumbnail of the given asset. + public ListBuildStep Compile(AssetItem assetItem, GameSettingsAsset gameSettings, bool staticThumbnail) + { + if (assetItem == null) throw new ArgumentNullException(nameof(assetItem)); + + using (var context = new ThumbnailCompilerContext + { + Platform = PlatformType.Windows + }) + { + context.SetGameSettingsAsset(gameSettings); + context.CompilationContext = typeof(PreviewCompilationContext); + + context.Properties.Set(ThumbnailGenerator.Key, generator); + context.ThumbnailBuilt += builtAction; + + var result = new AssetCompilerResult(); + + if (!staticThumbnail) + { + //compile the actual asset + result = dependenciesCompiler.Prepare(context, assetItem); + } + + //compile the actual thumbnail + var thumbnailStep = CompileItem(context, result, assetItem); + + foreach (var buildStep in result.BuildSteps) + { + BuildStep.LinkBuildSteps(buildStep, thumbnailStep); + } + result.BuildSteps.Add(thumbnailStep); + return result.BuildSteps; + } + } +} diff --git a/sources/engine/Stride.SpriteStudio.Runtime/Properties/AssemblyInfo.cs b/sources/engine/Stride.SpriteStudio.Runtime/Properties/AssemblyInfo.cs index 59dc807b66..56a80f8be2 100644 --- a/sources/engine/Stride.SpriteStudio.Runtime/Properties/AssemblyInfo.cs +++ b/sources/engine/Stride.SpriteStudio.Runtime/Properties/AssemblyInfo.cs @@ -9,4 +9,5 @@ // associated with an assembly. [assembly: NeutralResourcesLanguage("en")] +[assembly: InternalsVisibleTo("Stride.Editor")] [assembly: InternalsVisibleTo("Stride.Editor.Wpf")] diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Stride.Assets.Editor.Avalonia.csproj b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Stride.Assets.Editor.Avalonia.csproj index 03d9795cdf..6da5b02bf5 100644 --- a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Stride.Assets.Editor.Avalonia.csproj +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Stride.Assets.Editor.Avalonia.csproj @@ -15,6 +15,7 @@ + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Converters/StrideImage.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Converters/StrideImage.cs new file mode 100644 index 0000000000..a3d252f229 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Converters/StrideImage.cs @@ -0,0 +1,45 @@ + +using System.Globalization; +using Avalonia; +using Stride.Core.Presentation.Avalonia.Converters; +using Stride.Graphics; + +namespace Stride.GameStudio.Avalonia.Converters; + +public sealed class StrideImage : OneWayValueConverter +{ + public override object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value == null) return AvaloniaProperty.UnsetValue; + + try + { + // FIXME xplat-editor this is a first attempt ut it's very likely broken + if (value is Image image) + { + var desc = image.Description; + global::Avalonia.Media.Imaging.Bitmap bitmap = new( + ToAvalonia(desc.Format), + global::Avalonia.Platform.AlphaFormat.Opaque, + image.DataPointer, new(desc.Width, desc.Height), new(96, 96), image.TotalSizeInBytes); + + return bitmap; + } + } + catch (Exception) + { + } + + return AvaloniaProperty.UnsetValue; + } + + private static global::Avalonia.Platform.PixelFormat ToAvalonia(PixelFormat pixelFormat) + { + return pixelFormat switch + { + PixelFormat.R8G8B8A8_SInt or PixelFormat.R8G8B8A8_SNorm or PixelFormat.R8G8B8A8_Typeless or PixelFormat.R8G8B8A8_UInt or PixelFormat.R8G8B8A8_UNorm or PixelFormat.R8G8B8A8_UNorm_SRgb => global::Avalonia.Platform.PixelFormats.Rgba8888, + PixelFormat.B8G8R8A8_Typeless or PixelFormat.B8G8R8A8_UNorm or PixelFormat.B8G8R8A8_UNorm_SRgb => global::Avalonia.Platform.PixelFormats.Bgra8888, + _ => default + }; + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml index e8c87cb40b..7db603a00d 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/AssetExplorerView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="using:Stride.Core.Assets.Presentation.ViewModels" xmlns:vm2="using:Stride.Core.Assets.Editor.ViewModels" + xmlns:cv="using:Stride.GameStudio.Avalonia.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Stride.GameStudio.Avalonia.Views.AssetExplorerView" x:DataType="vm2:AssetCollectionViewModel"> @@ -30,6 +31,8 @@ + From b100a919d34509a65a3626d52e634151c1a6ff95 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 29 Oct 2023 22:49:02 +0100 Subject: [PATCH 215/247] [Plugin] Initialize session and register preview types --- build/Stride.sln | 12 +++- .../Stride.Assets.Editor.csproj | 1 + .../StrideEditorPlugin.cs | 62 ++++++++++++++++++- .../StrideDefaultAssetsPlugin.cs | 6 ++ .../AssetsEditorPlugin.cs | 22 ++++--- .../Services/IAssetsPluginService.cs | 13 ++++ .../ViewModels/SessionViewModel.cs | 16 +++-- .../AssetsPlugin.cs | 2 + .../Annotations/AssetPreviewAttribute.cs | 29 +++++++++ .../Annotations/AssetPreviewViewAttribute.cs | 29 +++++++++ .../AssetPreviewViewModelAttribute.cs | 29 +++++++++ .../Stride.Editor/Preview/IAssetPreview.cs | 12 ++++ .../ViewModels/IAssetPreviewViewModel.cs | 21 +++++++ .../Preview/Views/IPreviewView.cs | 11 ++++ sources/editor/Stride.Editor/README.md | 1 + .../StrideEditorViewPlugin.cs | 31 +++++++++- .../Stride.GameStudio.Avalonia/App.axaml.cs | 3 +- .../Internal/TypeHelpers.cs | 29 +++++++++ .../Stride.GameStudio.Avalonia/README.md | 2 + .../Services/PluginService.cs | 53 ++++++++++++---- .../Stride.GameStudio.Avalonia.csproj | 6 ++ 21 files changed, 357 insertions(+), 33 deletions(-) create mode 100644 sources/editor/Stride.Editor/Annotations/AssetPreviewAttribute.cs create mode 100644 sources/editor/Stride.Editor/Annotations/AssetPreviewViewAttribute.cs create mode 100644 sources/editor/Stride.Editor/Annotations/AssetPreviewViewModelAttribute.cs create mode 100644 sources/editor/Stride.Editor/Preview/IAssetPreview.cs create mode 100644 sources/editor/Stride.Editor/Preview/ViewModels/IAssetPreviewViewModel.cs create mode 100644 sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Internal/TypeHelpers.cs rename sources/{editor/Stride.Core.Assets.Editor => xplat-editor/Stride.GameStudio.Avalonia}/Services/PluginService.cs (50%) diff --git a/build/Stride.sln b/build/Stride.sln index 3b5d8ecd7c..b5bf60bf20 100644 --- a/build/Stride.sln +++ b/build/Stride.sln @@ -358,6 +358,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Editor", "..\sources EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Editor.Avalonia", "..\sources\xplat-editor\Stride.Editor.Avalonia\Stride.Editor.Avalonia.csproj", "{2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{358506E6-D2AE-4678-A434-C25F09417858}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{0CEFA54A-2AF7-4F86-8AA0-79701FB17090}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1757,13 +1761,15 @@ Global {19838BC4-E889-422B-B234-B94A3996A4AA} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} {736BE0DA-CA67-43A4-A31A-A485022E81E9} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} {C85BE53E-C890-4843-A7B8-8AFEEB7E5787} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} - {BB6F37DE-40E5-4AC6-B599-358D99EFD493} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} + {BB6F37DE-40E5-4AC6-B599-358D99EFD493} = {0CEFA54A-2AF7-4F86-8AA0-79701FB17090} {E9ABC6BF-8869-4827-A41F-A9CD1CCE7248} = {75A820AB-0F21-40F2-9448-5D7F495B97A0} {C075170E-2DCB-41BE-9106-19923F655AA1} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} - {B7FD8293-7F32-46FF-AAF6-36D2C594760D} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} - {6F5A1D7F-15A0-4439-86AE-7102478DAA8B} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} + {B7FD8293-7F32-46FF-AAF6-36D2C594760D} = {0CEFA54A-2AF7-4F86-8AA0-79701FB17090} + {6F5A1D7F-15A0-4439-86AE-7102478DAA8B} = {358506E6-D2AE-4678-A434-C25F09417858} {CC16DE9A-F346-4AE3-9BF0-EE8A4D385B14} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} {2C7C4631-CCB2-4BDD-9D9B-7E5ABCCD51EB} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} + {358506E6-D2AE-4678-A434-C25F09417858} = {BD3F1AB2-28DA-4C23-B819-B5873C74E962} + {0CEFA54A-2AF7-4F86-8AA0-79701FB17090} = {5D2D3BE8-9910-45CA-8E45-95660DA4C563} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FF877973-604D-4EA7-B5F5-A129961F9EF2} diff --git a/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj b/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj index d1419b7dcc..c7a1e6cc0d 100644 --- a/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj +++ b/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj @@ -22,6 +22,7 @@ + diff --git a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs index 1ff672c9ea..3c1c96cbff 100644 --- a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs +++ b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs @@ -1,15 +1,75 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using System.Reflection; using Stride.Core.Assets.Editor; +using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.Diagnostics; +using Stride.Core.IO; +using Stride.Editor.Annotations; +using Stride.Editor.Build; +using Stride.Editor.Preview.ViewModels; +using Stride.Editor.Thumbnails; namespace Stride.Assets.Editor; -public class StrideEditorPlugin : AssetsEditorPlugin +public sealed class StrideEditorPlugin : AssetsEditorPlugin { public override void InitializePlugin(ILogger logger) { // nothing for now } + + public override void InitializeSession(ISessionViewModel session) + { + // FIXME xplat-editor + //var fallbackDirectory = UPath.Combine(EditorSettings.FallbackBuildCacheDirectory, new UDirectory(StrideGameStudio.EditorName)); + var fallbackDirectory = UPath.Combine(Path.Combine(Path.GetTempPath(), "Stride", "BuildCache"), new UDirectory("Stride Game Studio")); + var buildDirectory = fallbackDirectory; + try + { + // FIXME xplat-editor + var package = session.CurrentProject /*?? session.LocalPackages.First()*/; + if (package != null) + { + // In package, we override editor build directory to be per-project and be shared with game build directory + buildDirectory = $"{package.PackagePath.GetFullDirectory().ToOSPath()}\\obj\\stride\\assetbuild\\data"; + } + + // Attempt to create the directory to ensure it is valid. + if (!Directory.Exists(buildDirectory)) + Directory.CreateDirectory(buildDirectory); + } + catch (Exception) + { + buildDirectory = fallbackDirectory; + } + + var settingsProvider = new GameSettingsProviderService(session); + session.ServiceProvider.RegisterService(settingsProvider); + + var builderService = new GameStudioBuilderService(session, settingsProvider, buildDirectory); + session.ServiceProvider.RegisterService(builderService); + + var thumbnailService = new GameStudioThumbnailService(session, settingsProvider, builderService); + session.ServiceProvider.RegisterService(thumbnailService); + } + + public override void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes) + { + var pluginAssembly = GetType().Assembly; + foreach (var type in pluginAssembly.GetTypes()) + { + if (typeof(IAssetPreviewViewModel).IsAssignableFrom(type) && + type.GetCustomAttribute() is { } attribute) + { + assetPreviewViewModelTypes.Add(attribute.AssetPreviewType, type); + } + } + } + + public override void RegisterAssetPreviewViewTypes(IDictionary assetPreviewViewTypes) + { + // nothing for now + } } diff --git a/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs b/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs index 4df08c92aa..468fad1544 100644 --- a/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs +++ b/sources/editor/Stride.Assets.Presentation/StrideDefaultAssetsPlugin.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using Stride.Core.Assets.Presentation; +using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.Diagnostics; namespace Stride.Assets.Presentation; @@ -12,4 +13,9 @@ public override void InitializePlugin(ILogger logger) { // nothing for now } + + public override void InitializeSession(ISessionViewModel session) + { + // nothing for now + } } diff --git a/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs b/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs index 25936df59e..98a5b97934 100644 --- a/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs +++ b/sources/editor/Stride.Core.Assets.Editor/AssetsEditorPlugin.cs @@ -11,29 +11,33 @@ namespace Stride.Core.Assets.Editor; public abstract class AssetsEditorPlugin : AssetsPlugin { - public void RegisterAssetEditorViewTypes(IDictionary assetEditorViewModelTypes) + public void RegisterAssetEditorViewModelTypes(IDictionary assetEditorViewModelTypes) { var pluginAssembly = GetType().Assembly; foreach (var type in pluginAssembly.GetTypes()) { - if (typeof(IAssetEditorView).IsAssignableFrom(type) && - type.GetCustomAttribute() is { } attribute) + if (typeof(AssetEditorViewModel).IsAssignableFrom(type) && + type.GetCustomAttribute() is { } attribute) { - assetEditorViewModelTypes.Add(attribute.EditorViewModelType, type); + assetEditorViewModelTypes.Add(attribute.ViewModelType, type); } } } - - public void RegisterAssetEditorViewModelTypes(IDictionary assetEditorViewModelTypes) + + public void RegisterAssetEditorViewTypes(IDictionary assetEditorViewTypes) { var pluginAssembly = GetType().Assembly; foreach (var type in pluginAssembly.GetTypes()) { - if (typeof(AssetEditorViewModel).IsAssignableFrom(type) && - type.GetCustomAttribute() is { } attribute) + if (typeof(IAssetEditorView).IsAssignableFrom(type) && + type.GetCustomAttribute() is { } attribute) { - assetEditorViewModelTypes.Add(attribute.ViewModelType, type); + assetEditorViewTypes.Add(attribute.EditorViewModelType, type); } } } + + public abstract void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes); + + public abstract void RegisterAssetPreviewViewTypes(IDictionary assetPreviewViewTypes); } diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs index eb4aa3f004..809e2cb93e 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetsPluginService.cs @@ -1,11 +1,24 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets.Presentation; +using Stride.Core.Diagnostics; + namespace Stride.Core.Assets.Editor.Services; public interface IAssetsPluginService { + IReadOnlyCollection Plugins { get; } + + void EnsureInitialized(ILogger logger); + + Type? GetAssetViewModelType(Type assetType); + Type? GetEditorViewModelType(Type viewModelType); Type? GetEditorViewType(Type editorViewModelType); + + Type? GetPreviewViewModelType(Type previewType); + + Type? GetPreviewViewType(Type previewType); } diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs index e515e05130..6b41c6de68 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using Stride.Core.Assets.Analysis; -using Stride.Core.Assets.Editor.Internal; using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Presentation.Components.Properties; using Stride.Core.Assets.Presentation.ViewModels; @@ -31,9 +30,8 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi { this.session = session; - // Gather all data from plugins - var pluginService = ServiceProvider.Get(); - pluginService.RegisterSession(this, logger); + // Make sure plugins are initialized + PluginService.EnsureInitialized(logger); // Initialize the node container used for asset properties AssetNodeContainer = new AssetNodeContainer { NodeBuilder = { NodeFactory = new AssetNodeFactory() } }; @@ -56,6 +54,12 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi Thumbnails = new ThumbnailsViewModel(this); GraphContainer = new AssetPropertyGraphContainer(AssetNodeContainer); + + // Initialize session itself in plugins + foreach (var plugin in PluginService.Plugins) + { + plugin.InitializeSession(this); + } } /// @@ -111,7 +115,7 @@ private set public ICommandBase EditSelectedContentCommand { get; } - internal Dictionary AssetViewModelTypes { get; } = []; + internal IAssetsPluginService PluginService => ServiceProvider.Get(); internal IUndoRedoService? UndoRedoService => ServiceProvider.TryGet(); @@ -187,7 +191,7 @@ public override void Destroy() public Type GetAssetViewModelType(AssetItem assetItem) { var assetType = assetItem.Asset.GetType(); - return TypeHelpers.TryGetTypeOrBase(assetType, AssetViewModelTypes) ?? typeof(AssetViewModel<>); + return PluginService.GetAssetViewModelType(assetType) ?? typeof(AssetViewModel<>); } /// diff --git a/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs b/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs index f544943f3e..9b8ec6f573 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/AssetsPlugin.cs @@ -16,6 +16,8 @@ public abstract class AssetsPlugin public abstract void InitializePlugin(ILogger logger); + public abstract void InitializeSession(ISessionViewModel session); + public static void RegisterPlugin(Type type) { if (type.GetConstructor(Type.EmptyTypes) == null) diff --git a/sources/editor/Stride.Editor/Annotations/AssetPreviewAttribute.cs b/sources/editor/Stride.Editor/Annotations/AssetPreviewAttribute.cs new file mode 100644 index 0000000000..b133f08605 --- /dev/null +++ b/sources/editor/Stride.Editor/Annotations/AssetPreviewAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Annotations; +using Stride.Editor.Preview; + +namespace Stride.Editor.Annotations; + +/// +/// Annotates a type that implements an asset preview. +/// +public abstract class AssetPreviewAttribute : Attribute +{ + /// + /// The asset type described by this attribute. + /// + public abstract Type AssetType { get; } +} + +/// +[AttributeUsage(AttributeTargets.Class)] +[BaseTypeRequired(typeof(IAssetPreview))] +public sealed class AssetPreviewAttribute : AssetPreviewAttribute + where TAsset : Asset +{ + /// + public override Type AssetType => typeof(TAsset); +} diff --git a/sources/editor/Stride.Editor/Annotations/AssetPreviewViewAttribute.cs b/sources/editor/Stride.Editor/Annotations/AssetPreviewViewAttribute.cs new file mode 100644 index 0000000000..76312019e2 --- /dev/null +++ b/sources/editor/Stride.Editor/Annotations/AssetPreviewViewAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Annotations; +using Stride.Editor.Preview; +using Stride.Editor.Preview.Views; + +namespace Stride.Editor.Annotations; + +/// +/// Annotates a type that implements the view of an asset preview. +/// +public abstract class AssetPreviewViewAttribute : Attribute +{ + /// + /// The asset preview type associated with this attribute. + /// + public abstract Type AssetPreviewType { get; } +} + +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +[BaseTypeRequired(typeof(IPreviewView))] +public sealed class AssetPreviewViewAttribute : AssetPreviewViewAttribute + where TAssetPreview : IAssetPreview +{ + /// + public override Type AssetPreviewType => typeof(TAssetPreview); +} diff --git a/sources/editor/Stride.Editor/Annotations/AssetPreviewViewModelAttribute.cs b/sources/editor/Stride.Editor/Annotations/AssetPreviewViewModelAttribute.cs new file mode 100644 index 0000000000..d086dc1443 --- /dev/null +++ b/sources/editor/Stride.Editor/Annotations/AssetPreviewViewModelAttribute.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Annotations; +using Stride.Editor.Preview; +using Stride.Editor.Preview.ViewModels; + +namespace Stride.Editor.Annotations; + +/// +/// Annotates a type that implements the view model of an asset preview. +/// +public abstract class AssetPreviewViewModelAttribute : Attribute +{ + /// + /// The asset preview type associated with this attribute. + /// + public abstract Type AssetPreviewType { get; } +} + +/// +[AttributeUsage(AttributeTargets.Class)] +[BaseTypeRequired(typeof(IAssetPreviewViewModel))] +public sealed class AssetPreviewViewModelAttribute : AssetPreviewViewModelAttribute + where TAssetPreview : IAssetPreview +{ + /// + public override Type AssetPreviewType => typeof(TAssetPreview); +} diff --git a/sources/editor/Stride.Editor/Preview/IAssetPreview.cs b/sources/editor/Stride.Editor/Preview/IAssetPreview.cs new file mode 100644 index 0000000000..24d98ae5a6 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/IAssetPreview.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + + +namespace Stride.Editor.Preview; + +/// +/// This interface represents an object that can manage the preview of an asset. +/// +public interface IAssetPreview +{ +} diff --git a/sources/editor/Stride.Editor/Preview/ViewModels/IAssetPreviewViewModel.cs b/sources/editor/Stride.Editor/Preview/ViewModels/IAssetPreviewViewModel.cs new file mode 100644 index 0000000000..71c37372cd --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/ViewModels/IAssetPreviewViewModel.cs @@ -0,0 +1,21 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Editor.Preview.ViewModels; + +/// +/// An interface that represents a view model that can be attached to an . +/// +/// Implementation should provide at least one constructor with a as first argument. +public interface IAssetPreviewViewModel +{ + ISessionViewModel Session { get; } + + /// + /// Attaches the given preview to this view model. + /// + /// The preview to attach. + void AttachPreview(IAssetPreview preview); +} diff --git a/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs b/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs new file mode 100644 index 0000000000..78230b1b2b --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Editor.Preview.Views; + +/// +/// An interface that represents the view of a preview. +/// +public interface IPreviewView +{ +} diff --git a/sources/editor/Stride.Editor/README.md b/sources/editor/Stride.Editor/README.md index 001ac834c4..64d07c4d6e 100644 --- a/sources/editor/Stride.Editor/README.md +++ b/sources/editor/Stride.Editor/README.md @@ -8,6 +8,7 @@ This project is the main project for the `Game` support in the editor. * It should be platform-agnostic as well as UI-agnostic. In other words, no dependencies on platform (e.g. Windows), or UI library (e.g. Avalonia, WPF) are allowed. * It will likely reference `Stride.Core.Assets.Editor` as well as Stride runtime libraries. +* Ideally, it shouldn't reference `Stride.Assets`, but that currently isn't the case. ## Implementations diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs index 55e810a71c..5195d04026 100644 --- a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/StrideEditorViewPlugin.cs @@ -2,14 +2,43 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using Stride.Core.Assets.Editor; +using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.Diagnostics; +using Stride.Editor.Annotations; +using Stride.Editor.Preview.Views; +using System.Reflection; namespace Stride.Assets.Editor.Avalonia; -public class StrideEditorViewPlugin : AssetsEditorPlugin +public sealed class StrideEditorViewPlugin : AssetsEditorPlugin { public override void InitializePlugin(ILogger logger) { // nothing for now } + + public override void InitializeSession(ISessionViewModel session) + { + // nothing for now + } + + public override void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes) + { + // nothing for now + } + + public override void RegisterAssetPreviewViewTypes(IDictionary assetPreviewViewTypes) + { + var pluginAssembly = GetType().Assembly; + foreach (var type in pluginAssembly.GetTypes()) + { + if (typeof(IPreviewView).IsAssignableFrom(type)) + { + foreach (var attribute in type.GetCustomAttributes()) + { + assetPreviewViewTypes.Add(attribute.AssetPreviewType, type); + } + } + } + } } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs index 05de85c05e..b30aac8f6b 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs @@ -5,7 +5,6 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; -using Stride.Core.Assets.Editor.Services; using Stride.Core.Presentation.Avalonia.Services; using Stride.Core.Presentation.ViewModels; using Stride.GameStudio.Avalonia.Services; @@ -48,7 +47,7 @@ public override void OnFrameworkInitializationCompleted() private static void InitializePlugins() { - // TODO: load plugins from path, and ideally remove direct dependencies to these assemblies in this project. + // TODO xplat-editor load plugins from path, and ideally remove direct dependencies to these assemblies in this project. // Until then, use a hack to force loading the assemblies. string _; _ = typeof(Assets.Presentation.StrideDefaultAssetsPlugin).Name; diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Internal/TypeHelpers.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Internal/TypeHelpers.cs new file mode 100644 index 0000000000..e3fa006e5d --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Internal/TypeHelpers.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.GameStudio.Avalonia.Internal; + +internal static class TypeHelpers +{ + /// + /// Try to get the type associated with the given from the provided , + /// going through the type inheritance hierarchy until a match is found. + /// + /// A key to the provided . + /// A dictionary mapping types. + /// The associated type if found; otherwise, null. + public static Type? TryGetTypeOrBase(Type keyType, IReadOnlyDictionary typeMap) + { + var currentType = keyType; + Type? returnType; + do + { + if (typeMap.TryGetValue(currentType, out returnType)) + break; + + currentType = currentType.BaseType; + } while (currentType != null); + + return returnType; + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/README.md b/sources/xplat-editor/Stride.GameStudio.Avalonia/README.md index cc820beb8f..2d77cfe58d 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/README.md +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/README.md @@ -7,6 +7,8 @@ This project is the main project for the Avalonia version of the Game Studio. * It should reference the platform-agnostic libraries of the editor. * It should reference the Avalonia libraries. However it should be agnostic when it comes to the final target (Desktop, Mobile, Web, etc.). +* Ideally, `Stride.Assets` related libraries (e.g. `Stride.Assets.Editor`) shouldn't be referenced directly, but loaded through the plugin system. + That would make the whole GameStudio only depends on `Stride.Core.Assets` and Stride runtime, which means it could be reused by an alternative implementation of Stride assets. ## Implementations diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/PluginService.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/PluginService.cs similarity index 50% rename from sources/editor/Stride.Core.Assets.Editor/Services/PluginService.cs rename to sources/xplat-editor/Stride.GameStudio.Avalonia/Services/PluginService.cs index edf3364e1d..8b3cf521b0 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Services/PluginService.cs +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/PluginService.cs @@ -1,26 +1,33 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets; +using Stride.Core.Assets.Editor; using Stride.Core.Assets.Editor.Editors; -using Stride.Core.Assets.Editor.Internal; +using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Editor.ViewModels; using Stride.Core.Assets.Presentation; using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.Diagnostics; using Stride.Core.Extensions; +using Stride.Editor.Preview; +using Stride.Editor.Preview.ViewModels; +using Stride.Editor.Preview.Views; +using Stride.GameStudio.Avalonia.Internal; -namespace Stride.Core.Assets.Editor.Services; +namespace Stride.GameStudio.Avalonia.Services; public sealed class PluginService : IAssetsPluginService { - private readonly Dictionary assetEditorViewModelTypes = []; - private readonly Dictionary assetEditorViewTypes = []; + private readonly Dictionary assetViewModelTypes = new(); + private readonly Dictionary editorViewModelTypes = new(); + private readonly Dictionary editorViewTypes = new(); + private readonly Dictionary previewViewModelTypes = new(); + private readonly Dictionary previewViewViewTypes = new(); - public Type? GetEditorViewModelType(Type viewModelType) => TypeHelpers.TryGetTypeOrBase(viewModelType, assetEditorViewModelTypes); + public IReadOnlyCollection Plugins => AssetsPlugin.RegisteredPlugins; - public Type? GetEditorViewType(Type editorViewModelType) => TypeHelpers.TryGetTypeOrBase(editorViewModelType, assetEditorViewTypes); - - internal void RegisterSession(SessionViewModel session, ILogger logger) + public void EnsureInitialized(ILogger logger) { foreach (var plugin in AssetsPlugin.RegisteredPlugins) { @@ -31,7 +38,7 @@ internal void RegisterSession(SessionViewModel session, ILogger logger) plugin.RegisterAssetViewModelTypes(registeredAssetViewModelsTypes); AssertType(typeof(Asset), registeredAssetViewModelsTypes.Select(x => x.Key)); AssertType(typeof(AssetViewModel), registeredAssetViewModelsTypes.Select(x => x.Value)); - session.AssetViewModelTypes.AddRange(registeredAssetViewModelsTypes); + assetViewModelTypes.AddRange(registeredAssetViewModelsTypes); if (plugin is AssetsEditorPlugin editorPlugin) { @@ -40,18 +47,42 @@ internal void RegisterSession(SessionViewModel session, ILogger logger) editorPlugin.RegisterAssetEditorViewModelTypes(registeredAssetEditorViewModelsTypes); AssertType(typeof(AssetViewModel), registeredAssetEditorViewModelsTypes.Select(x => x.Key)); AssertType(typeof(AssetEditorViewModel), registeredAssetEditorViewModelsTypes.Select(x => x.Value)); - assetEditorViewModelTypes.AddRange(registeredAssetEditorViewModelsTypes); + editorViewModelTypes.AddRange(registeredAssetEditorViewModelsTypes); // Asset editor view types var registeredAssetEditorViewTypes = new Dictionary(); editorPlugin.RegisterAssetEditorViewTypes(registeredAssetEditorViewTypes); AssertType(typeof(AssetEditorViewModel), registeredAssetEditorViewTypes.Select(x => x.Key)); AssertType(typeof(IAssetEditorView), registeredAssetEditorViewTypes.Select(x => x.Value)); - assetEditorViewTypes.AddRange(registeredAssetEditorViewTypes); + editorViewTypes.AddRange(registeredAssetEditorViewTypes); + + // Asset preview view model types + var registeredAssetPreviewViewModelTypes = new Dictionary(); + editorPlugin.RegisterAssetPreviewViewModelTypes(registeredAssetPreviewViewModelTypes); + AssertType(typeof(IAssetPreview), registeredAssetPreviewViewModelTypes.Select(x => x.Key)); + AssertType(typeof(IAssetPreviewViewModel), registeredAssetPreviewViewModelTypes.Select(x => x.Value)); + previewViewModelTypes.AddRange(registeredAssetPreviewViewModelTypes); + + // Asset preview view types + var registeredAssetPreviewViewTypes = new Dictionary(); + editorPlugin.RegisterAssetPreviewViewTypes(registeredAssetPreviewViewTypes); + AssertType(typeof(IAssetPreview), registeredAssetPreviewViewTypes.Select(x => x.Key)); + AssertType(typeof(IPreviewView), registeredAssetPreviewViewTypes.Select(x => x.Value)); + previewViewViewTypes.AddRange(registeredAssetPreviewViewTypes); } } } + public Type? GetAssetViewModelType(Type assetType) => TypeHelpers.TryGetTypeOrBase(assetType, assetViewModelTypes); + + public Type? GetEditorViewModelType(Type viewModelType) => TypeHelpers.TryGetTypeOrBase(viewModelType, editorViewModelTypes); + + public Type? GetEditorViewType(Type editorViewModelType) => TypeHelpers.TryGetTypeOrBase(editorViewModelType, editorViewTypes); + + public Type? GetPreviewViewModelType(Type previewType) => TypeHelpers.TryGetTypeOrBase(previewType, previewViewModelTypes); + + public Type? GetPreviewViewType(Type previewType) => TypeHelpers.TryGetTypeOrBase(previewType, previewViewViewTypes); + private static void AssertType(Type baseType, Type specificType) { if (!baseType.IsAssignableFrom(specificType)) diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Stride.GameStudio.Avalonia.csproj b/sources/xplat-editor/Stride.GameStudio.Avalonia/Stride.GameStudio.Avalonia.csproj index 6fdb32812e..729cf966ef 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Stride.GameStudio.Avalonia.csproj +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Stride.GameStudio.Avalonia.csproj @@ -8,11 +8,17 @@ + + + + + + From 58fd6ac942b8fd9ea086ab22b7a8bea61dbf4c3c Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 29 Oct 2023 23:01:36 +0100 Subject: [PATCH 216/247] [Editor] Previews --- .../Preview/AnimationPreview.cs | 161 +++++ .../Preview/EntityPreview.cs | 29 + .../Preview/FontPreview.cs | 77 +++ .../Preview/HeightmapPreview.cs | 94 +++ .../Preview/ITextureBasePreview.cs | 25 + .../Preview/MaterialPreview.cs | 92 +++ .../Preview/ModelPreview.cs | 35 + .../Preview/PrecompiledSpriteFontPreview.cs | 20 + .../Preview/PrefabModelPreview.cs | 35 + .../Preview/PrefabPreview.cs | 31 + .../Preview/PreviewFromEntity.cs | 318 +++++++++ .../Preview/PreviewFromSpriteBatch.cs | 205 ++++++ .../Preview/ProceduralModelPreview.cs | 32 + .../Preview/ScenePreview.cs | 13 + .../Preview/SkyboxPreview.cs | 130 ++++ .../Preview/SoundPreview.cs | 150 ++++ .../Preview/SpriteFontPreview.cs | 20 + .../Preview/SpriteSheetPreview.cs | 181 +++++ .../Preview/SpriteStudioSheetPreview.cs | 36 + .../Preview/TextureCubePreviewMode.cs | 45 ++ .../Preview/TexturePreview.cs | 247 +++++++ .../Shaders/PreviewTexture.sdfx | 25 + .../Shaders/PreviewTexture.sdfx.cs | 46 ++ .../Shaders/SceneEditorParameters.sdfx | 16 + .../Shaders/SceneEditorParameters.sdfx.cs | 26 + .../Shaders/SelectedSprite.sdfx | 12 + .../Shaders/SelectedSprite.sdfx.cs | 37 + .../StrideEditorForwardShadingEffect.sdfx | 54 ++ .../StrideEditorForwardShadingEffect.sdfx.cs | 101 +++ .../StrideEditorHighlightingEffect.sdfx | 14 + .../StrideEditorHighlightingEffect.sdfx.cs | 38 ++ .../StrideEditorMaterialPreviewEffect.sdfx | 14 + .../StrideEditorMaterialPreviewEffect.sdfx.cs | 39 ++ .../Stride.Assets.Editor.csproj | 2 +- .../StrideEditorPlugin.cs | 9 +- .../EditorGraphicsCompositorHelper.cs | 10 + .../Preview/AnimationPreviewViewModel.cs | 90 +++ .../Preview/AssetPreviewViewModel.cs | 32 + .../Preview/EntityPreviewViewModel.cs | 22 + .../Preview/HeightmapPreviewViewModel.cs | 39 ++ .../Preview/IAnimatedPreviewViewModel.cs | 15 + .../Preview/MaterialPreviewViewModel.cs | 35 + .../Preview/ModelPreviewViewModel.cs | 33 + .../ProceduralModelPreviewViewModel.cs | 33 + .../Preview/SkyboxPreviewViewModel.cs | 60 ++ .../Preview/SoundPreviewViewModel.cs | 81 +++ .../Preview/SpriteFontPreviewViewModel.cs | 32 + .../Preview/SpriteSheetPreviewViewModel.cs | 44 ++ .../SpriteStudioSheetPreviewViewModel.cs | 34 + .../Preview/TextureBasePreviewViewModel.cs | 95 +++ .../Preview/TexturePreviewViewModel.cs | 61 ++ .../Stride.Assets.Presentation.csproj | 2 +- .../Services/IAssetPreviewService.cs | 19 + .../Build/AnonymousAssetBuildUnit.cs | 23 + .../ContentLoader/ContentLoadEventArgs.cs | 14 + .../ContentLoader/EditorContentLoader.cs | 643 ++++++++++++++++++ .../ContentLoader/IEditorContentLoader.cs | 58 ++ .../ContentLoader/LoaderReferenceManager.cs | 194 ++++++ .../EditorGame/Game/EditorGameServiceBase.cs | 82 +++ .../Game/EditorGameServiceRegistry.cs | 43 ++ .../EditorGame/Game/EditorServiceGame.cs | 246 +++++++ .../EditorGame/Game/IEditorGameService.cs | 46 ++ .../ViewModels/IEditorGameViewModelService.cs | 11 + .../Stride.Editor/Engine/EmbeddedGame.cs | 46 ++ .../Extensions/EditorGameExtensions.cs | 44 ++ sources/editor/Stride.Editor/Module.cs | 17 + .../AnimationAssetEditorGameCompiler.cs | 18 + .../Preview/AnimationAssetPreviewCompiler.cs | 25 + .../Stride.Editor/Preview/AssetPreview.cs | 252 +++++++ .../Preview/AssetPreviewFactory.cs | 8 + .../Preview/BuildAssetPreview.cs | 125 ++++ .../Stride.Editor/Preview/IAssetPreview.cs | 49 ++ .../Stride.Editor/Preview/IPreviewBuilder.cs | 49 ++ .../Preview/PrefabAssetPreviewCompiler.cs | 45 ++ .../Stride.Editor/Preview/PreviewEntity.cs | 24 + .../Stride.Editor/Preview/PreviewGame.cs | 215 ++++++ .../Preview/SoundAssetEditorGameCompiler.cs | 18 + .../Preview/VideoAssetEditorGameCompiler.cs | 17 + .../Preview/Views/IPreviewView.cs | 12 + .../editor/Stride.Editor/Stride.Editor.csproj | 5 + .../Properties/AssemblyInfo.cs | 1 + .../Properties/AssemblyInfo.cs | 1 + .../Stride.Assets.Editor.Avalonia/Module.cs | 3 + .../Views/Preview/AnimationPreviewView.cs | 13 + .../Views/Preview/EntityPreviewView.cs | 12 + .../Views/Preview/HeightmapPreviewView.cs | 13 + .../Views/Preview/MaterialPreviewView.cs | 13 + .../Views/Preview/ModelPreviewView.cs | 17 + .../Views/Preview/ScenePreviewView.cs | 13 + .../Views/Preview/SkyboxPreviewView.cs | 13 + .../Views/Preview/SoundPreviewView.cs | 13 + .../Views/Preview/SpriteFontPreviewView.cs | 13 + .../Views/Preview/SpriteSheetPreviewView.cs | 13 + .../Views/Preview/TexturePreviewView.cs | 13 + .../Controls/GameEngineHost.cs | 371 ++++++++++ .../Engine/EmbeddedGameForm.cs | 47 ++ .../Preview/GameStudioPreviewService.cs | 330 +++++++++ .../Preview/Views/StridePreviewView.cs | 68 ++ .../Stride.Editor.Avalonia.csproj | 2 +- 99 files changed, 6437 insertions(+), 7 deletions(-) create mode 100644 sources/editor/Stride.Assets.Editor/Preview/AnimationPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/EntityPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/FontPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/HeightmapPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/ITextureBasePreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/MaterialPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/ModelPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/PrecompiledSpriteFontPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/PrefabModelPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/PrefabPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/PreviewFromEntity.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/PreviewFromSpriteBatch.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/ProceduralModelPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/ScenePreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/SkyboxPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/SoundPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/SpriteFontPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/SpriteSheetPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/SpriteStudioSheetPreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/TextureCubePreviewMode.cs create mode 100644 sources/editor/Stride.Assets.Editor/Preview/TexturePreview.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx create mode 100644 sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/EditorGraphicsCompositorHelper.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/AnimationPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/AssetPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/EntityPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/HeightmapPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/IAnimatedPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/MaterialPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/ModelPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/ProceduralModelPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/SkyboxPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/SoundPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteFontPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteSheetPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteStudioSheetPreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/TextureBasePreviewViewModel.cs create mode 100644 sources/editor/Stride.Assets.Editor/ViewModels/Preview/TexturePreviewViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/IAssetPreviewService.cs create mode 100644 sources/editor/Stride.Editor/Build/AnonymousAssetBuildUnit.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/ContentLoader/ContentLoadEventArgs.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/ContentLoader/EditorContentLoader.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/ContentLoader/IEditorContentLoader.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/ContentLoader/LoaderReferenceManager.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceBase.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceRegistry.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/Game/IEditorGameService.cs create mode 100644 sources/editor/Stride.Editor/EditorGame/ViewModels/IEditorGameViewModelService.cs create mode 100644 sources/editor/Stride.Editor/Engine/EmbeddedGame.cs create mode 100644 sources/editor/Stride.Editor/Extensions/EditorGameExtensions.cs create mode 100644 sources/editor/Stride.Editor/Module.cs create mode 100644 sources/editor/Stride.Editor/Preview/AnimationAssetEditorGameCompiler.cs create mode 100644 sources/editor/Stride.Editor/Preview/AnimationAssetPreviewCompiler.cs create mode 100644 sources/editor/Stride.Editor/Preview/AssetPreview.cs create mode 100644 sources/editor/Stride.Editor/Preview/AssetPreviewFactory.cs create mode 100644 sources/editor/Stride.Editor/Preview/BuildAssetPreview.cs create mode 100644 sources/editor/Stride.Editor/Preview/IPreviewBuilder.cs create mode 100644 sources/editor/Stride.Editor/Preview/PrefabAssetPreviewCompiler.cs create mode 100644 sources/editor/Stride.Editor/Preview/PreviewEntity.cs create mode 100644 sources/editor/Stride.Editor/Preview/PreviewGame.cs create mode 100644 sources/editor/Stride.Editor/Preview/SoundAssetEditorGameCompiler.cs create mode 100644 sources/editor/Stride.Editor/Preview/VideoAssetEditorGameCompiler.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/AnimationPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/EntityPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/HeightmapPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/MaterialPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ModelPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ScenePreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SkyboxPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SoundPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteFontPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteSheetPreviewView.cs create mode 100644 sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/TexturePreviewView.cs create mode 100644 sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/GameEngineHost.cs create mode 100644 sources/xplat-editor/Stride.Editor.Avalonia/Engine/EmbeddedGameForm.cs create mode 100644 sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs create mode 100644 sources/xplat-editor/Stride.Editor.Avalonia/Preview/Views/StridePreviewView.cs diff --git a/sources/editor/Stride.Assets.Editor/Preview/AnimationPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/AnimationPreview.cs new file mode 100644 index 0000000000..5290af80ca --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/AnimationPreview.cs @@ -0,0 +1,161 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Animations; +using Stride.Assets.Editor.ViewModels.Preview; +using Stride.Assets.Models; +using Stride.Core.Assets.Compiler; +using Stride.Editor.Annotations; +using Stride.Editor.Build; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +[AssetPreview] +public class AnimationPreview : PreviewFromEntity +{ + private readonly object animLock = new(); + private PlayingAnimation? playingAnim; + private float timeFactor; + + private string? compiledModelUrl; + + public Action UpdateViewModelTime { get; set; } + + public bool IsPlaying { get; private set; } + + protected override async Task Initialize() + { + await base.Initialize(); + + Game.Script.AddTask(UpdateAnimationTime); + } + + protected override void PrepareLoadedEntity() + { + lock (animLock) + playingAnim = null; + + base.PrepareLoadedEntity(); + + var animComponent = PreviewEntity?.Entity.Get(); + + // Don't play, if the animation is invalid + if (animComponent?.Animations["preview"] != null) + { + animComponent.Play("preview"); + + lock (animLock) + { + playingAnim = animComponent.PlayingAnimations.Last(); + playingAnim.RepeatMode = AnimationRepeatMode.LoopInfinite; + } + } + } + + private async Task UpdateAnimationTime() + { + var noAnimTimeUpdated = false; + while (IsRunning) + { + // Await two frames to reduce overhead + await Game.Script.NextFrame(); + await Game.Script.NextFrame(); + if (UpdateViewModelTime != null) + { + if (playingAnim != null) + { + UpdateViewModelTime(true, (float)playingAnim.CurrentTime.TotalSeconds, (float)playingAnim.Clip.Duration.TotalSeconds); + noAnimTimeUpdated = false; + } + else if (!noAnimTimeUpdated) + { + UpdateViewModelTime(false, 0.0f, 0.0f); + noAnimTimeUpdated = true; + } + } + } + } + + protected override PreviewEntity? CreatePreviewEntity() + { + if (compiledModelUrl == null) + return null; + + // load the created material and the model from the data base + var model = LoadAsset(compiledModelUrl); + var anim = LoadAsset(AssetItem.Location); + var animSrc = LoadAsset(AssetItem.Location + AnimationAssetCompiler.SrcClipSuffix); + + // create the entity, create and set the model component + var entity = new Entity { Name = "Preview Entity of animation: " + AssetItem.Location }; + entity.Add(new ModelComponent { Model = model }); + // In case of additive animation, play the original source (we can't play the additive animation itself) + entity.Add(new AnimationComponent { Animations = { { "preview", animSrc ?? anim } } }); + + var previewEntity = new PreviewEntity(entity); + previewEntity.Disposed += () => UnloadAsset(model); + previewEntity.Disposed += () => UnloadAsset(anim); + if (animSrc != null) previewEntity.Disposed += () => UnloadAsset(animSrc); + + return previewEntity; + } + + protected override void UpdateBuildAssetResults(AnonymousAssetBuildUnit buildUnit, AssetCompilerResult compilationResult) + { + base.UpdateBuildAssetResults(buildUnit, compilationResult); + + // Save aside model url + // TODO: we should technically store it in a temp var during Compile(), and set it only now (in case it changed during compile) + compiledModelUrl = buildUnit.Succeeded ? AnimationPreviewViewModel.FindModelForPreview(AssetItem)?.Location : null; + } + + public async void SetTimeScale(float value) + { + timeFactor = value; + await IsInitialized(); + lock (animLock) + { + if (playingAnim != null) + playingAnim.TimeFactor = timeFactor; + } + } + + public void SetCurrentTime(float value) + { + lock (animLock) + { + if (playingAnim != null) + { + value = (float)Math.Min(value, playingAnim.Clip.Duration.TotalSeconds - 0.001f); + // Remove one tick so the modulo in CurrentTime management won't mess up when we try to set the max value + playingAnim.CurrentTime = TimeSpan.FromSeconds(value); + } + } + } + + public void Play() + { + IsPlaying = true; + lock (animLock) + { + if (playingAnim != null) + { + playingAnim.TimeFactor = timeFactor; + playingAnim.Enabled = IsPlaying; + } + } + } + + public void Pause() + { + IsPlaying = false; + lock (animLock) + { + if (playingAnim != null) + playingAnim.Enabled = IsPlaying; + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/EntityPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/EntityPreview.cs new file mode 100644 index 0000000000..9d7ff46fc6 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/EntityPreview.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Assets.Entities; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview entities. +/// +// DO NOT REACTIVATE THIS PREVIEW WITHOUT MAKING A DISTINCT PREVIEW BETWEEN ENTITIES AND SCENE! SCENE IS LOADED (AND NOW UNLOADED) at initialization, we absolutely don't want to do that +//[AssetPreview] +public class EntityPreview : PreviewFromEntity +{ + /// + protected override PreviewEntity CreatePreviewEntity() + { + // create the preview entity from the entity build on the database + var entity = LoadAsset(AssetItem.Location); + var previewEntity = new PreviewEntity(entity); + + // ensure that the model is correctly unloaded after used + previewEntity.Disposed += () => UnloadAsset(previewEntity.Entity); + + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/FontPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/FontPreview.cs new file mode 100644 index 0000000000..2082c72d05 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/FontPreview.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Graphics; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +public abstract class FontPreview : PreviewFromSpriteBatch + where T : Asset +{ + private string? previewText = ""; + + private BlendStateDescription adequateBlendState; + + private Graphics.SpriteFont? spriteFont; + + private static EffectInstance? sdfFontEffect; + + private static EffectInstance GetSDFFontEffect(GraphicsDevice device) + { + return sdfFontEffect ?? (sdfFontEffect = new EffectInstance(new Graphics.Effect(device, SpriteSignedDistanceFieldFontShader.Bytecode) { Name = "SDFFontEffectAssetPreview" })); + } + + /// + /// Sets a string to preview using the sprite font. If the given string is null or empty, it will display the + /// sprite font texture instead. + /// + /// The string to preview. + public void SetPreviewString(string? str) + { + previewText = str; + } + + protected abstract bool IsFontNotPremultiplied(); + + protected override void LoadContent() + { + // determine the adequate blend state to render the font + adequateBlendState = IsFontNotPremultiplied() ? BlendStates.NonPremultiplied : BlendStates.AlphaBlend; + + // load the sprite font + spriteFont = LoadAsset(AssetItem.Location); + + // Always use LDR for fonts + RenderingMode = RenderingMode.LDR; + } + + protected override void UnloadContent() + { + if (spriteFont != null) + { + UnloadAsset(spriteFont); + spriteFont = null; + } + } + + protected override void RenderSprite() + { + if (spriteFont == null || SpriteBatch == null) + return; + + var textToDisplay = string.IsNullOrEmpty(previewText) ? "Enter the text to preview" : previewText; + + var textSize = spriteFont.MeasureString(textToDisplay); + var windowSize = new Vector2(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height); + var position = SpriteOffsets + (windowSize - textSize) / 2; + + var effectInstance = spriteFont.FontType == SpriteFontType.SDF ? GetSDFFontEffect(Game.GraphicsDevice) : null; + + SpriteBatch.Begin(Game.GraphicsContext, SpriteSortMode.Texture, adequateBlendState, null, null, null, effectInstance); + SpriteBatch.DrawString(spriteFont, textToDisplay, position, Color.White); + SpriteBatch.End(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/HeightmapPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/HeightmapPreview.cs new file mode 100644 index 0000000000..4eddf690c6 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/HeightmapPreview.cs @@ -0,0 +1,94 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Physics; +using Stride.Core.Mathematics; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Graphics; +using Stride.Physics; + +namespace Stride.Assets.Editor.Preview; + +[AssetPreview] +public class HeightmapPreview : PreviewFromSpriteBatch +{ + private Heightmap? heightmap; + private Texture? heightmapTexture; + private BlendStateDescription adequateBlendState; + + public int Width => heightmap?.Size.X ?? 0; + public int Length => heightmap?.Size.Y ?? 0; + + /// + /// Gets or sets a callback that will be invoked when the texture is loaded. + /// + public Action? NotifyHeightmapLoaded { get; set; } + + protected override Vector2 SpriteSize + { + get + { + if (heightmapTexture == null) + return base.SpriteSize; + + return new Vector2(heightmapTexture.Width, heightmapTexture.Height); + } + } + + protected virtual Vector2 ImageCenter + { + get + { + if (heightmapTexture == null) + return Vector2.Zero; + + var imageSize = new Vector2(heightmapTexture.Width, heightmapTexture.Height); + + return imageSize / 2f; + } + } + + protected override void LoadContent() + { + heightmap = LoadAsset(AssetItem.Location); + + heightmapTexture = heightmap?.CreateTexture(Game.GraphicsDevice); + + adequateBlendState = BlendStates.Opaque; + + NotifyHeightmapLoaded?.Invoke(); + + // Always use LDR + RenderingMode = RenderingMode.LDR; + } + + protected override void UnloadContent() + { + if (heightmapTexture != null) + { + heightmapTexture.Dispose(); + heightmapTexture = null; + } + + if (heightmap != null) + { + UnloadAsset(heightmap); + heightmap = null; + } + } + + protected override void RenderSprite() + { + if (heightmapTexture == null || SpriteBatch == null) + return; + + var origin = ImageCenter - SpriteOffsets; + var region = new RectangleF(0, 0, heightmapTexture.Width, heightmapTexture.Height); + var orientation = ImageOrientation.AsIs; + + SpriteBatch.Begin(Game.GraphicsContext, SpriteSortMode.Texture, adequateBlendState); + SpriteBatch.Draw(heightmapTexture, WindowSize / 2, region, Color.White, 0, origin, SpriteScale, SpriteEffects.None, orientation, swizzle: SwizzleMode.RRR1); + SpriteBatch.End(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/ITextureBasePreview.cs b/sources/editor/Stride.Assets.Editor/Preview/ITextureBasePreview.cs new file mode 100644 index 0000000000..78e12357f4 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/ITextureBasePreview.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Mathematics; + +namespace Stride.Assets.Editor.Preview; + +public interface ITextureBasePreview +{ + float SpriteScale { get; set; } + + event EventHandler? SpriteScaleChanged; + + IEnumerable GetAvailableMipMaps(); + + void DisplayMipMap(int parseMipMapLevel); + + void ZoomIn(Vector2? centerPosition); + + void ZoomOut(Vector2? centerPosition); + + void FitOnScreen(); + + void ScaleToRealSize(); +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/MaterialPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/MaterialPreview.cs new file mode 100644 index 0000000000..a54b0df8e2 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/MaterialPreview.cs @@ -0,0 +1,92 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Mathematics; +using Stride.Assets.Materials; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Graphics.GeometricPrimitives; +using Stride.Rendering.ProceduralModels; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +public enum MaterialPreviewPrimitive +{ + Sphere, + Cube, + Cylinder, + Torus, + Plane, + Teapot, + Cone, + Capsule +} + +/// +/// An implementation of the that can preview materials. +/// +[AssetPreview] +public class MaterialPreview : PreviewFromEntity +{ + public const string EditorMaterialPreviewEffect = "StrideEditorMaterialPreviewEffect"; + + private MaterialPreviewPrimitive previewPrimitive; + + public MaterialPreview() : base(EditorMaterialPreviewEffect) + { + } + + protected override async Task Initialize() + { + CameraScript.DefaultYaw = MathUtil.Pi; // want to see the center of the texture by default (and not corners). + + await base.Initialize(); + } + + public async void SetPrimitive(MaterialPreviewPrimitive primitive) + { + if (previewPrimitive != primitive) + { + previewPrimitive = primitive; + await Update(); + } + } + + /// + protected override PreviewEntity CreatePreviewEntity() + { + // load the material from the data base + var material = LoadAsset(AssetItem.Location); + + // create a sphere model to display the material + var proceduralModel = CreatePrimitiveModel(previewPrimitive); + var model = proceduralModel.GenerateModel(Game.Services); // TODO: should dispose those resources at some points! + model.Add(material); + + // create the entity, create and set the model component + var materialEntity = new Entity { Name = BuildName() }; + materialEntity.Add(new ModelComponent { Model = model }); + + var previewEntity = new PreviewEntity(materialEntity); + previewEntity.Disposed += () => UnloadAsset(material); + return previewEntity; + } + + internal static ProceduralModelDescriptor CreatePrimitiveModel(MaterialPreviewPrimitive primitive) + { + return primitive switch + { + MaterialPreviewPrimitive.Sphere => new ProceduralModelDescriptor { Type = new SphereProceduralModel() }, + MaterialPreviewPrimitive.Cube => new ProceduralModelDescriptor { Type = new CubeProceduralModel() }, + MaterialPreviewPrimitive.Cylinder => new ProceduralModelDescriptor { Type = new CylinderProceduralModel() }, + MaterialPreviewPrimitive.Torus => new ProceduralModelDescriptor { Type = new TorusProceduralModel() }, + MaterialPreviewPrimitive.Plane => new ProceduralModelDescriptor { Type = new PlaneProceduralModel { GenerateBackFace = true, Normal = NormalDirection.UpZ } }, + MaterialPreviewPrimitive.Teapot => new ProceduralModelDescriptor { Type = new TeapotProceduralModel() }, + MaterialPreviewPrimitive.Cone => new ProceduralModelDescriptor { Type = new ConeProceduralModel() }, + MaterialPreviewPrimitive.Capsule => new ProceduralModelDescriptor { Type = new CapsuleProceduralModel() }, + _ => throw new ArgumentOutOfRangeException(nameof(primitive)), + }; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/ModelPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/ModelPreview.cs new file mode 100644 index 0000000000..b3a069f6f4 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/ModelPreview.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Models; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview models. +/// +[AssetPreview] +public class ModelPreview : PreviewFromEntity +{ + /// + protected override PreviewEntity CreatePreviewEntity() + { + var modelLocation = AssetItem.Location; + // load the created material and the model from the data base + var model = LoadAsset(modelLocation); + + // create the entity, create and set the model component + var entity = new Entity { Name = "Preview Entity of model: " + modelLocation }; + entity.Add(new ModelComponent { Model = model }); + + var previewEntity = new PreviewEntity(entity); + + previewEntity.Disposed += () => UnloadAsset(model); + + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/PrecompiledSpriteFontPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/PrecompiledSpriteFontPreview.cs new file mode 100644 index 0000000000..1b1d598ec3 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/PrecompiledSpriteFontPreview.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.SpriteFont; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview precompiled sprite fonts. +/// +[AssetPreview] +public class PrecompiledSpriteFontPreview : FontPreview +{ + protected override bool IsFontNotPremultiplied() + { + return !Asset.IsPremultiplied; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/PrefabModelPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/PrefabModelPreview.cs new file mode 100644 index 0000000000..290d148b34 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/PrefabModelPreview.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Models; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview prefab models. +/// +[AssetPreview] +public class PrefabModelPreview : PreviewFromEntity +{ + /// + protected override PreviewEntity CreatePreviewEntity() + { + var modelLocation = AssetItem.Location; + // load the created material and the model from the data base + var model = LoadAsset(modelLocation); + + // create the entity, create and set the model component + var entity = new Entity { Name = "Preview Entity of model: " + modelLocation }; + entity.Add(new ModelComponent { Model = model }); + + var previewEntity = new PreviewEntity(entity); + + previewEntity.Disposed += () => UnloadAsset(model); + + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/PrefabPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/PrefabPreview.cs new file mode 100644 index 0000000000..aa9486b9f2 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/PrefabPreview.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Entities; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; + +namespace Stride.Assets.Editor.Preview; + +[AssetPreview] +public class PrefabPreview : PreviewFromEntity +{ + protected override PreviewEntity CreatePreviewEntity() + { + var entity = new Entity { Name = "Preview Entity of model: " + AssetItem.Location }; + + var prefab = LoadAsset(AssetItem.Location); + if (prefab != null) + { + foreach (var prefabEntity in prefab.Entities) + { + entity.AddChild(prefabEntity); + } + } + + var previewEntity = new PreviewEntity(entity); + previewEntity.Disposed += () => UnloadAsset(prefab); + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/PreviewFromEntity.cs b/sources/editor/Stride.Assets.Editor/Preview/PreviewFromEntity.cs new file mode 100644 index 0000000000..40f846ad2d --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/PreviewFromEntity.cs @@ -0,0 +1,318 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.ViewModels; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Editor.EditorGame.Game; +using Stride.Editor.Engine; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Input; +using Stride.Particles.Rendering; +using Stride.Rendering; +using Stride.Rendering.Compositing; +using Stride.Rendering.Lights; +using Stride.SpriteStudio.Runtime; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the class that simply build an asset and create an entity for it +/// during the initialization and updates. This class can be inherited to make preview class for assets that does not +/// require more that a compilation and an entity creation. +/// +/// The type of asset this preview can display. +public abstract class PreviewFromEntity : BuildAssetPreview where T : Asset +{ + protected PreviewEntity? PreviewEntity { get; set; } + + protected string modelEffectName; + + private readonly Scene entityScene; + + private readonly Entity camera; + + /// + /// The script updating the position of the preview camera + /// + protected CameraUpdateScript CameraScript { get; } + + protected PreviewFromEntity(string modelEffectName = EditorGraphicsCompositorHelper.EditorForwardShadingEffect) + { + this.modelEffectName = modelEffectName; + var cameraComponent = new CameraComponent(); + + // create the entity preview scene + entityScene = new Scene(); + + // setup the camera + CameraScript = new CameraUpdateScript(); + camera = new Entity("Preview Camera") { cameraComponent, CameraScript }; + entityScene.Entities.Add(camera); + } + + protected override async Task Initialize() + { + await base.Initialize(); + + SetupLighting(camera); + + CameraScript.ResetViewAngle(); + } + + protected virtual void SetupLighting(Entity camera) + { + // Depending on RenderingMode, use HDR or LDR settings for the lights + var factor = 1.0f; + if (RenderingMode == RenderingMode.HDR) + { + factor = 2.0f / 0.9f; + } + + var ambientLight = new Entity("Preview Ambient Light1") { new LightComponent { Type = new LightAmbient(), Intensity = 0.02f * factor } }; + var frontDirectionalLight = new Entity("Preview Directional Front") { new LightComponent { Intensity = 0.07f * factor } }; + var topDirectionalLight = new Entity("Preview Directional Top") { new LightComponent { Intensity = 0.8f * factor } }; + topDirectionalLight.Transform.Rotation = Quaternion.RotationX(MathUtil.DegreesToRadians(-80)); + camera.AddChild(ambientLight); + camera.AddChild(frontDirectionalLight); + camera.AddChild(topDirectionalLight); + } + + protected override Scene CreatePreviewScene() + { + return entityScene; + } + + protected override GraphicsCompositor GetGraphicsCompositor() + { + var graphicsCompositor = GraphicsCompositorHelper.CreateDefault(RenderingMode == RenderingMode.HDR, modelEffectName, + camera.Get(), RenderingMode == RenderingMode.HDR ? EditorServiceGame.EditorBackgroundColorHdr : EditorServiceGame.EditorBackgroundColorLdr); + + var opaqueStage = graphicsCompositor.RenderStages.First(x => x.Name.Equals("Opaque")); + var transparentStage = graphicsCompositor.RenderStages.First(x => x.Name.Equals("Transparent")); + + // Add particles, UI and SpriteStudio renderers + graphicsCompositor.RenderFeatures.Add( + new ParticleEmitterRenderFeature() + { + RenderStageSelectors = + { + new ParticleEmitterTransparentRenderStageSelector + { + EffectName = "Particles", + OpaqueRenderStage = opaqueStage, + TransparentRenderStage = transparentStage, + } + }, + }); + + graphicsCompositor.RenderFeatures.Add( + new SpriteStudioRenderFeature() + { + RenderStageSelectors = + { + new SimpleGroupToRenderStageSelector() + { + EffectName = "SpriteStudio", + RenderStage = transparentStage, + } + } + }); + + return graphicsCompositor; + } + + protected override void LoadContent() + { + base.LoadContent(); + + PreviewEntity = CreatePreviewEntity(); + + if (PreviewEntity?.Entity != null) + { + PrepareLoadedEntity(); + entityScene.Entities.Add(PreviewEntity.Entity); + + // Start script manually since ScriptProcessor is not enabled (in case some entity has script we don't want to run) + Game.Script.Add(CameraScript); + } + } + + protected override void UnloadContent() + { + base.UnloadContent(); + + if (PreviewEntity == null) + return; + + // Start script manually since ScriptProcessor is not enabled (in case some entity has script we don't want to run) + Game.Script.Remove(CameraScript); + + // remove the entity from the scene. + entityScene.Entities.Remove(PreviewEntity.Entity); + + PreviewEntity.Disposed?.Invoke(); + + PreviewEntity = null; + } + + protected virtual void PrepareLoadedEntity() + { + CameraScript.AdjustViewTarget(PreviewEntity?.Entity); + } + + protected class CameraUpdateScript : SyncScript + { + // ReSharper disable once StaticFieldInGenericType + private static readonly BoundingSphere InvalidBoundingSphere = new(Vector3.Zero, MathUtil.ZeroTolerance); + + public float DefaultYaw; + public float DefaultPitch; + + private float yaw; + private float pitch; + private float distance; + private Vector3 target; + + private CameraComponent? cameraComponent; + private BoundingSphere previousBoundingSphere = InvalidBoundingSphere; + + public override void Start() + { + cameraComponent = Entity.Get(); + } + + public void ResetViewAngle() + { + yaw = DefaultYaw; + pitch = DefaultPitch; + + UpdateComponents(); + } + + public void ResetViewTarget(Entity? targetEntity) + { + target = Vector3.Zero; + previousBoundingSphere = InvalidBoundingSphere; + AdjustViewTarget(targetEntity); + } + + public void AdjustViewTarget(Entity? targetEntity) + { + if (targetEntity == null) + return; + + cameraComponent = Entity.Get(); + + // reset the target and the distance only if the bounding sphere has changed. + var boundingSphere = targetEntity.CalculateBoundSphere(); + if (boundingSphere != previousBoundingSphere) + { + var radius = boundingSphere.Radius; + + // calculate the distance to the target needed in order to see it fully + // Note: we want the front face of the element to be fully visible (not only center) + distance = radius + radius / MathF.Tan(MathUtil.DegreesToRadians(cameraComponent.VerticalFieldOfView / 2)); + // Make sure the distance is greater than zero + distance = Math.Max(distance, 2 * MathUtil.ZeroTolerance); + + target = boundingSphere.Center; + } + previousBoundingSphere = boundingSphere; + + UpdateComponents(); + } + + public void SetViewDistance(float distance) + { + this.distance = Math.Max(0, distance); + UpdateComponents(); + } + + public override void Update() + { + // if the user is pressing or releasing a new mouse button the action change and we reset the mouse origin position + if (Input.HasReleasedMouseButtons || Input.HasPressedMouseButtons) + { + if (Input.HasDownMouseButtons) + { + Input.LockMousePosition(); + Game.IsMouseVisible = false; + } + else + { + Input.UnlockMousePosition(); + Game.IsMouseVisible = true; + } + } + + // if the user is not pushing any mouse button the camera does not change + if (!Input.HasDownMouseButtons && Math.Abs(Input.MouseWheelDelta) < MathUtil.ZeroTolerance) + return; + + var translation = Input.MouseDelta; + if (Input.IsMouseButtonDown(MouseButton.Right)) // translation + { + var viewMatrixInv = Entity.Transform.WorldMatrix; + target -= distance * (translation.X * viewMatrixInv.Row1.XYZ() - translation.Y * viewMatrixInv.Row2.XYZ()); + } + else if (Input.IsMouseButtonDown(MouseButton.Left)) // orbital rotation + { + yaw -= 4 * translation.X; + pitch -= 3 * translation.Y; + } + + distance *= 1 / (1 + Input.MouseWheelDelta / 8); + + UpdateComponents(); + } + + public void UpdateComponents() + { + cameraComponent = Entity.Get(); + if (cameraComponent != null) + { + var maxPitch = MathUtil.PiOverTwo - 0.01f; + pitch = MathUtil.Clamp(pitch, -maxPitch, maxPitch); + + var offset = new Vector3(0, 0, distance); + var cameraPosition = target + Vector3.Transform(offset, Quaternion.RotationYawPitchRoll(yaw, pitch, 0)); + + Entity.Transform.UseTRS = false; + Entity.Transform.LocalMatrix = Matrix.Invert(Matrix.LookAtRH(cameraPosition, target, Vector3.UnitY)); + cameraComponent.FarClipPlane = 2.5f * Math.Max(distance, previousBoundingSphere.Radius); + cameraComponent.NearClipPlane = cameraComponent.FarClipPlane / 1000f; + } + } + } + + /// + /// Reset the camera to its default state. + /// + public async void ResetCamera() + { + await IsInitialized(); + + CameraScript.ResetViewAngle(); + CameraScript.ResetViewTarget(PreviewEntity?.Entity); + } + + /// + /// Generates a string that can be used as a name for the entity that will be created for this preview. + /// + /// + protected string BuildName() + { + var description = DisplayAttribute.GetDisplay(typeof(T)); + var typeDisplayName = description != null && !string.IsNullOrEmpty(description.Name) ? description.Name : typeof(T).Name; + return string.Format("Preview entity for {0} '{1}'", typeDisplayName, AssetItem.Location); + } + + /// + /// Creates an entity that will be used for the preview. + /// + /// An instance of the class + protected abstract PreviewEntity? CreatePreviewEntity(); +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/PreviewFromSpriteBatch.cs b/sources/editor/Stride.Assets.Editor/Preview/PreviewFromSpriteBatch.cs new file mode 100644 index 0000000000..638468f85e --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/PreviewFromSpriteBatch.cs @@ -0,0 +1,205 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Core.Presentation.Core; +using Stride.Editor.EditorGame.Game; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering; +using Stride.Rendering.Compositing; + +namespace Stride.Assets.Editor.Preview; + +/// +/// A base class to implement sprite batch rendering previews. +/// +public abstract class PreviewFromSpriteBatch : BuildAssetPreview, ITextureBasePreview + where T : Asset +{ + private readonly Scene spriteScene; + + public float SpriteScale { get { return spriteScale; } set { spriteScale = value; SpriteScaleChanged?.Invoke(this, EventArgs.Empty); } } + + public event EventHandler? SpriteScaleChanged; + + protected SpriteBatch? SpriteBatch { get; private set; } + + protected Vector2 SpriteOffsets; + + private float spriteScale; + + protected PreviewFromSpriteBatch() + { + spriteScene = new Scene(); + } + + protected override async Task Initialize() + { + SpriteOffsets = Vector2.Zero; + SpriteBatch = new SpriteBatch(Game.GraphicsDevice); + + await base.Initialize(); + + Game.Script.AddTask(MoveAndScaleSpriteOnUserInput); + } + + protected virtual Vector2 SpriteSize + { + get { return Vector2.Zero; } + } + + protected Vector2 WindowSize + { + get + { + if (Game == null || Game.Window == null) + return Vector2.Zero; + + return new Vector2(Game.Window.ClientBounds.Width, Game.Window.ClientBounds.Height); + } + } + + protected override Scene CreatePreviewScene() + { + return spriteScene; + } + + protected override GraphicsCompositor GetGraphicsCompositor() + { + return new GraphicsCompositor + { + Game = new SceneRendererCollection + { + new ClearRenderer { Color = RenderingMode == RenderingMode.HDR ? EditorServiceGame.EditorBackgroundColorHdr : EditorServiceGame.EditorBackgroundColorLdr }, + new DelegateSceneRenderer(SafeRenderSprite), + } + }; + } + + private async Task MoveAndScaleSpriteOnUserInput() + { + var previousMousePosition = Vector2.Zero; + + while (IsRunning) + { + await Game.Script.NextFrame(); + + if (Game.Input.HasPressedMouseButtons) + previousMousePosition = Game.Input.MousePosition; + + var deltaPosition = Game.Input.MousePosition - previousMousePosition; + + if (Game.Input.HasDownMouseButtons) + SpriteOffsets += deltaPosition * WindowSize / SpriteScale; + + if (Game.Input.MouseWheelDelta < 0) + ZoomOut(Game.Input.MousePosition); + + if (Game.Input.MouseWheelDelta > 0) + ZoomIn(Game.Input.MousePosition); + + previousMousePosition = Game.Input.MousePosition; + } + } + + protected abstract void RenderSprite(); + + private void SafeRenderSprite(RenderDrawContext context) + { + if (SpriteBatch == null) + return; + + try + { + RenderSprite(); + } + catch (Exception e) + { + Builder.Logger.Error($"RenderSprite crashed for asset item [{AssetItem}].", e); + } + } + + public override async Task Dispose() + { + await base.Dispose(); + + SpriteBatch?.Dispose(); + } + + public virtual IEnumerable GetAvailableMipMaps() + { + // We don't need to provide mipmap preview (at least yet...) + return new[] { 0 }; + } + + public virtual void DisplayMipMap(int parseMipMapLevel) + { + // We don't need to provide mipmap preview (at least yet...) + // Intentionally does nothing + } + + public void ZoomIn(Vector2? centerPosition) + { + var newValue = (float)Utils.ZoomFactors.FirstOrDefault(x => (float)x > SpriteScale); + if (newValue < float.Epsilon) + newValue = (float)Utils.ZoomFactors.Last(); + + ChangeScale(newValue, centerPosition); + } + + public void ZoomOut(Vector2? centerPosition) + { + var newValue = (float)Utils.ZoomFactors.LastOrDefault(x => (float)x < SpriteScale); + if (newValue < float.Epsilon) + newValue = (float)Utils.ZoomFactors.First(); + + ChangeScale(newValue, centerPosition); + } + + public virtual void FitOnScreen() + { + SpriteOffsets = Vector2.Zero; + + if (SpriteSize == Vector2.Zero) + { + SpriteScale = 1; + return; + } + + // determine the best scale to display the texture + SpriteScale = Math.Min(WindowSize.X / SpriteSize.X, WindowSize.Y / SpriteSize.Y); + } + + public virtual void ScaleToRealSize() + { + SpriteScale = 1; + } + + public override void OnViewAttached() + { + base.OnViewAttached(); + + SpriteOffsets = Vector2.Zero; + if (SpriteSize == Vector2.Zero) + { + SpriteScale = 1; + return; + } + + // Choose the best match between realsize (if it fits) or fit-on-screen + var screenScale = Math.Min(WindowSize.X / SpriteSize.X, WindowSize.Y / SpriteSize.Y); + SpriteScale = screenScale < 1 ? screenScale : 1; + } + + private void ChangeScale(float newValue, Vector2? centerPoint) + { + if (centerPoint != null) + { + SpriteOffsets += (centerPoint.Value - new Vector2(0.5f)) * WindowSize * (1.0f / newValue - 1.0f / SpriteScale); + } + SpriteScale = newValue; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/ProceduralModelPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/ProceduralModelPreview.cs new file mode 100644 index 0000000000..781e00eba0 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/ProceduralModelPreview.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Models; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Rendering; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview materials. +/// +[AssetPreview] +public class ProceduralModelPreview : PreviewFromEntity +{ + /// + protected override PreviewEntity CreatePreviewEntity() + { + // load the material from the data base + var model = LoadAsset(AssetItem.Location); + + // create the entity, create and set the model component + var modelEntity = new Entity { Name = BuildName() }; + modelEntity.Add(new ModelComponent { Model = model }); + + var previewEntity = new PreviewEntity(modelEntity); + previewEntity.Disposed += () => UnloadAsset(model); + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/ScenePreview.cs b/sources/editor/Stride.Assets.Editor/Preview/ScenePreview.cs new file mode 100644 index 0000000000..6fe837619c --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/ScenePreview.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Entities; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; + +namespace Stride.Assets.Editor.Preview; + +[AssetPreview] +public class ScenePreview : AssetPreview +{ +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/SkyboxPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/SkyboxPreview.cs new file mode 100644 index 0000000000..c0f89cc1aa --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/SkyboxPreview.cs @@ -0,0 +1,130 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Mathematics; +using Stride.Assets.Skyboxes; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.Rendering.Skyboxes; +using Stride.Rendering; +using Stride.Rendering.Lights; +using Stride.Rendering.Materials; +using Stride.Rendering.Materials.ComputeColors; +using Stride.Rendering.ProceduralModels; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview models. +/// +[AssetPreview] +public class SkyboxPreview : PreviewFromEntity +{ + private Entity? targetEntity; + private ModelComponent? previewModel; + + public float Metalness { get; private set; } = 1.0f; + public float Glossiness { get; private set; } = 1.0f; + + public new async void ResetCamera() + { + await IsInitialized(); + + // Hint view distance + CameraScript.ResetViewTarget(targetEntity); + CameraScript.ResetViewAngle(); + } + + public void SetMetalness(float metalness) + { + Metalness = metalness; + previewModel.Materials[0] = CreateMaterial(Metalness, Glossiness); + } + + public void SetGlossiness(float glossiness) + { + Glossiness = glossiness; + previewModel.Materials[0] = CreateMaterial(Metalness, Glossiness); + } + + protected override void SetupLighting(Entity camera) + { + // No default lighting + } + + protected override PreviewEntity CreatePreviewEntity() + { + var skybox = LoadAsset(AssetItem.Location); + + var rootEntity = new Entity(); + + // Create skybox lighting + var skyLight = new Entity + { + new LightComponent { Type = new LightSkybox { Skybox = skybox } } + }; + rootEntity.AddChild(skyLight); + + targetEntity = CreateMaterialPreview(rootEntity, Vector3.Zero, out previewModel); + + var previewEntity = new PreviewEntity(rootEntity); + previewEntity.Disposed += () => UnloadAsset(skybox); + return previewEntity; + } + + protected override async Task Initialize() + { + await base.Initialize(); + CameraScript.DefaultYaw = 0.0f; + CameraScript.DefaultPitch = MathUtil.DegreesToRadians(-40.0f); + ResetCamera(); + } + + protected override void PrepareLoadedEntity() + { + } + + private Entity CreateMaterialPreview(Entity root, Vector3 position, out ModelComponent previewModel) + { + // create a sphere model to display the material + var proceduralModel = new ProceduralModelDescriptor { Type = new SphereProceduralModel() }; + var model = proceduralModel.GenerateModel(Game.Services); // TODO: should dispose those resources at some points! + + // create the entity, create and set the model component + var materialEntity = new Entity { Name = BuildName() }; + materialEntity.Add(previewModel = new ModelComponent + { + Model = model, + Materials = { [0] = CreateMaterial(Metalness, Glossiness) } + }); + materialEntity.Transform.Position = position; + + root.AddChild(materialEntity); + return materialEntity; + } + + private Material CreateMaterial(float metalness, float glossiness) + { + return Material.New(Game.GraphicsDevice, new MaterialDescriptor + { + Attributes = new MaterialAttributes + { + Diffuse = new MaterialDiffuseMapFeature + { + DiffuseMap = new ComputeColor(Color4.White) + }, + MicroSurface = new MaterialGlossinessMapFeature + { + GlossinessMap = new ComputeFloat(glossiness) + }, + DiffuseModel = new MaterialDiffuseLambertModelFeature(), + Specular = new MaterialMetalnessMapFeature + { + MetalnessMap = new ComputeFloat(metalness) + }, + SpecularModel = new MaterialSpecularMicrofacetModelFeature() + } + }); + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/SoundPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/SoundPreview.cs new file mode 100644 index 0000000000..ba418eb1ef --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/SoundPreview.cs @@ -0,0 +1,150 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.IO; +using Stride.Core.Presentation.Services; +using Stride.Assets.Media; +using Stride.Audio; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Media; + +namespace Stride.Assets.Editor.Preview; + +[AssetPreview] +public class SoundPreview : BuildAssetPreview +{ + private IDispatcherService? dispatcher; + + private Sound? sound; + private SoundInstance? instance; + + private TimeSpan startTime = TimeSpan.Zero; + + public Action? UpdateViewModelTime { get; set; } + + public bool IsPlaying { get; private set; } + + private volatile bool loaded; + + public void ProvideDispatcher(IDispatcherService dispatcherService) + { + dispatcher = dispatcherService; + } + + protected bool VerifyAssetConsistency() + { + // Get absolute path of asset source on disk + var assetDirectory = (string)AssetItem.Location.GetParent(); + var assetSource = UPath.Combine(assetDirectory, Asset.Source ?? ""); + + // Check that the source file exist on the disk. + return File.Exists(assetSource); + } + + protected override async Task Initialize() + { + await base.Initialize(); + await Task.Run(() => UpdatePlaybackTime()); + } + + protected override async Task PrepareContent() + { + loaded = false; + + startTime = TimeSpan.Zero; + + if (sound != null) + { + instance?.Stop(); + instance?.Dispose(); + UnloadAsset(sound); + } + + await base.PrepareContent(); + + sound = LoadAsset(AssetItem.Location); + if (sound == null) + return false; + instance = sound.CreateInstance(null, true); + instance.SetRange(new PlayRange(TimeSpan.Zero, TimeSpan.Zero)); + + loaded = true; + return true; + } + + protected override void UnloadContent() + { + if (!loaded) + return; + + UpdateViewModelTime?.Invoke(false, false, TimeSpan.Zero, TimeSpan.Zero); + instance?.Stop(); + instance?.Dispose(); + instance = null; + UnloadAsset(sound); + + loaded = false; + } + + private async void UpdatePlaybackTime() + { + try + { + while (IsRunning) + { + await Task.Delay(50); + if (UpdateViewModelTime != null) + { + await dispatcher.InvokeAsync(() => + { + if (loaded) + { + IsPlaying = instance?.PlayState == PlayState.Playing; + var position = instance.Position; + UpdateViewModelTime(true, IsPlaying, position + startTime, sound.TotalLength); + } + else + { + UpdateViewModelTime(false, false, TimeSpan.Zero, TimeSpan.Zero); + } + }); + } + } + } + catch (TaskCanceledException) + { + //Cool! + } + } + + public void SetCurrentTime(TimeSpan value) + { + var wasPlaying = instance?.PlayState == PlayState.Playing; + + instance?.Stop(); + startTime = value; + instance?.SetRange(new PlayRange(value, sound.TotalLength)); + + if (wasPlaying) + instance?.Play(); + } + + public void Play() + { + instance?.Play(); + } + + public void Pause() + { + instance?.Pause(); + } + + public void SetMasterVolume(double value) + { + if (instance != null) + { + instance.Volume = (float)value; + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/SpriteFontPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/SpriteFontPreview.cs new file mode 100644 index 0000000000..c2755a5f28 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/SpriteFontPreview.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.SpriteFont; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview sprite fonts. +/// +[AssetPreview] +public class SpriteFontPreview : FontPreview +{ + protected override bool IsFontNotPremultiplied() + { + return !Asset.FontType.IsPremultiplied; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/SpriteSheetPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/SpriteSheetPreview.cs new file mode 100644 index 0000000000..37cb5b8fb7 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/SpriteSheetPreview.cs @@ -0,0 +1,181 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Sprite; +using Stride.Assets.Textures; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Mathematics; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Graphics; + +namespace Stride.Assets.Editor.Preview; + +public enum SpriteSheetDisplayMode +{ + [Display("Sprites")] + Sprites, + [Display("Sprite textures")] + SpriteTextures, +} + +/// +/// A base preview implementation for all asset inheriting from . +/// +[AssetPreview] +public class SpriteSheetPreview : PreviewFromSpriteBatch +{ + protected SpriteSheet? SpriteSheet; + private BlendStateDescription adequateBlendState; + private SpriteSheetDisplayMode mode; + private List spriteTextures; + + /// + /// Gets or sets the mode of the display of the preview + /// + public SpriteSheetDisplayMode Mode + { + get { return mode; } + set + { + mode = value; + CurrentFrame = Math.Min(CurrentFrame, FrameCount - 1); + FitOnScreen(); + } + } + + /// + /// Gets the number of frames in the sprite. + /// + public int FrameCount + { + get + { + if (SpriteSheet?.Sprites == null) + return 0; + + switch (Mode) + { + case SpriteSheetDisplayMode.Sprites: + return SpriteSheet.Sprites.Count; + case SpriteSheetDisplayMode.SpriteTextures: + return spriteTextures.Count; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + + /// + /// Gets or sets the index of the frame being currently previewed. + /// + public int CurrentFrame { get; set; } + + protected override Vector2 SpriteSize + { + get + { + if (SpriteSheet?.Sprites == null || SpriteSheet.Sprites.Count == 0) + return base.SpriteSize; + + // return the max of all sprites of the group in order to be able to pass + // from one element to other in the preview without having to change zooming + var max = Vector2.Zero; + foreach (var image in SpriteSheet.Sprites) + { + var width = 0f; + var height = 0f; + + if (Mode == SpriteSheetDisplayMode.Sprites) + { + width = image.SizeInPixels.X; + height = image.SizeInPixels.Y; + } + else + { + if (image.Texture != null) + { + width = image.Texture.Width; + height = image.Texture.Height; + } + } + + max[0] = Math.Max(max[0], width); + max[1] = Math.Max(max[1], height); + } + return max; + } + } + + protected virtual Vector2 ImageCenter + { + get + { + if (SpriteSheet?.Sprites == null || SpriteSheet.Sprites.Count == 0) + return Vector2.Zero; + + Vector2 imageSize; + if (Mode == SpriteSheetDisplayMode.Sprites) + { + var image = SpriteSheet.Sprites[CurrentFrame]; + imageSize = new Vector2(image.Region.Width, image.Region.Height); + } + else + { + var image = spriteTextures[CurrentFrame]; + imageSize = new Vector2(image.Width, image.Height); + } + + return imageSize / 2f; + } + } + + protected override void LoadContent() + { + SpriteSheet = LoadAsset(AssetItem.Location); + spriteTextures = new List(); + if (SpriteSheet?.Sprites != null) + spriteTextures.AddRange(SpriteSheet.Sprites.Select(s => s.Texture).Distinct()); + + // look for the texture asset to determine the blend state + var spriteGroupItem = AssetItem.Package.Session.FindAsset(Asset.Id); + var spriteGroupAsset = (SpriteSheetAsset)spriteGroupItem.Asset; + adequateBlendState = BlendStates.Opaque; + if (spriteGroupAsset.Alpha != AlphaFormat.None) + adequateBlendState = spriteGroupAsset.PremultiplyAlpha ? BlendStates.AlphaBlend : BlendStates.NonPremultiplied; + + // Always use LDR + RenderingMode = RenderingMode.LDR; + } + + protected override void UnloadContent() + { + if (SpriteSheet != null) + { + UnloadAsset(SpriteSheet); + SpriteSheet = null; + spriteTextures = null; + } + } + + protected override void RenderSprite() + { + if (SpriteSheet?.Sprites == null || SpriteSheet.Sprites.Count == 0 || SpriteBatch == null) + return; + + // check that the current texture is not null + var texture = Mode == SpriteSheetDisplayMode.Sprites ? SpriteSheet.Sprites[CurrentFrame].Texture : spriteTextures[CurrentFrame]; + if (texture == null) + return; + + var origin = ImageCenter - SpriteOffsets; + var image = SpriteSheet.Sprites[CurrentFrame]; + var region = Mode == SpriteSheetDisplayMode.Sprites ? image.Region : new RectangleF(0, 0, texture.Width, texture.Height); + var orientation = Mode == SpriteSheetDisplayMode.Sprites ? image.Orientation : ImageOrientation.AsIs; + + SpriteBatch.Begin(Game.GraphicsContext, SpriteSortMode.Texture, adequateBlendState); + SpriteBatch.Draw(texture, WindowSize / 2, region, Color.White, 0, origin, SpriteScale * Vector2.One, SpriteEffects.None, orientation); + SpriteBatch.End(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/SpriteStudioSheetPreview.cs b/sources/editor/Stride.Assets.Editor/Preview/SpriteStudioSheetPreview.cs new file mode 100644 index 0000000000..04f3519aca --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/SpriteStudioSheetPreview.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Engine; +using Stride.SpriteStudio.Offline; +using Stride.SpriteStudio.Runtime; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview models. +/// +// FIXME: this view model should be in the SpriteStudio offline assembly! Can't be done now, because of a circular reference in CompilerApp referencing SpriteStudio, and Editor referencing CompilerApp +[AssetPreview] +public class SpriteStudioSheetPreview : PreviewFromEntity +{ + /// + protected override PreviewEntity CreatePreviewEntity() + { + var spriteStudioSheetLocation = AssetItem.Location; + // load the created material and the model from the data base + var sheet = LoadAsset(spriteStudioSheetLocation); + + // create the entity, create and set the model component + var entity = new Entity { Name = "Preview entity of SpriteStudio sheet: " + spriteStudioSheetLocation }; + entity.Add(new SpriteStudioComponent { Sheet = sheet }); + + var previewEntity = new PreviewEntity(entity); + + previewEntity.Disposed += () => UnloadAsset(sheet); + + return previewEntity; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/TextureCubePreviewMode.cs b/sources/editor/Stride.Assets.Editor/Preview/TextureCubePreviewMode.cs new file mode 100644 index 0000000000..99d548c892 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/TextureCubePreviewMode.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Assets.Editor.Preview; + +/// +/// Specify the different modes of preview of the TextureCube +/// +public enum TextureCubePreviewMode +{ + /// + /// Display only the Right texture + /// + Right = 0, + + /// + /// Display only the Left texture + /// + Left = 1, + + /// + /// Display only the Top texture + /// + Top = 2, + + /// + /// Display only the Bottom texture + /// + Bottom = 3, + + /// + /// Display only the front texture + /// + Front = 4, + + /// + /// Display only the Back texture + /// + Back = 5, + + /// + /// Display all the texture cube as template + /// + Full = 6, +} diff --git a/sources/editor/Stride.Assets.Editor/Preview/TexturePreview.cs b/sources/editor/Stride.Assets.Editor/Preview/TexturePreview.cs new file mode 100644 index 0000000000..3ce40d4e3d --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Preview/TexturePreview.cs @@ -0,0 +1,247 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.BuildEngine; +using Stride.Core.Mathematics; +using Stride.Core.Serialization.Contents; +using Stride.Assets.Textures; +using Stride.Editor.Annotations; +using Stride.Editor.Preview; +using Stride.Rendering; +using Stride.Graphics; +using StrideEffects; + +namespace Stride.Assets.Editor.Preview; + +/// +/// An implementation of the that can preview textures. +/// +[AssetPreview] +public class TexturePreview : PreviewFromSpriteBatch +{ + private readonly Dictionary textureCubeViews = new(); + private readonly DynamicEffectInstance currentEffect; + + private Texture texture; + private BlendStateDescription adequateBlendState; + private SamplerState specificMipmapSamplerState; + private Rectangle textureSourceRegion; + + private TextureCubePreviewMode textureCubePreviewMode = TextureCubePreviewMode.Full; + + public int TextureWidth => texture?.Width ?? 0; + + public int TextureHeight => texture?.Height ?? 0; + + public int TextureDepth => texture?.Depth ?? 0; + + /// + /// Gets or sets a callback that will be invoked when the texture is loaded. + /// + public Action NotifyTextureLoaded { get; set; } + + public TextureDimension Dimension => texture?.ViewDimension ?? default; + + public int SliceCount => texture?.ArraySize ?? -1; + + public TexturePreview() + { + currentEffect = new DynamicEffectInstance("PreviewTexture"); + } + + protected override Task Initialize() + { + specificMipmapSamplerState = SamplerState.New(Game.GraphicsDevice, new SamplerStateDescription(TextureFilter.ComparisonPoint, TextureAddressMode.Clamp)); + + currentEffect.Initialize(Game.Services); + + return base.Initialize(); + } + + public override IEnumerable GetAvailableMipMaps() + { + yield return 0; + + if (texture == null) + yield break; + + for (var i = 1; i < texture.Description.MipLevels; ++i) + yield return i; + } + + public override void DisplayMipMap(int level) + { + var samplerDescrition = new SamplerStateDescription(TextureFilter.ComparisonPoint, TextureAddressMode.Clamp) + { + MinMipLevel = level, + MaxMipLevel = level + }; + + specificMipmapSamplerState.Dispose(); + specificMipmapSamplerState = SamplerState.New(Game.GraphicsDevice, samplerDescrition); + } + + /// + protected override ColorSpace DetermineColorSpace() + { + // Project color space + var colorSpace = base.DetermineColorSpace(); + + // Compute color space to use during rendering with hint and color space set on texture + var textureAsset = (TextureAsset)AssetItem.Asset; + colorSpace = textureAsset.Type.IsSRgb(colorSpace) ? ColorSpace.Linear : ColorSpace.Gamma; + + return colorSpace; + } + + protected SwizzleMode Swizzle() + { + var textureAsset = (TextureAsset)AssetItem.Asset; + + if (textureAsset.Type.Hint == TextureHint.Grayscale) + return SwizzleMode.RRR1; + + if (textureAsset.Type.Hint == TextureHint.NormalMap) + return SwizzleMode.NormalMap; + + return SwizzleMode.None; + } + + protected override Vector2 SpriteSize + { + get + { + if (texture == null) + return base.SpriteSize; + + var textureSize = new Vector2(texture.Width, texture.Height); + + if (texture.ViewDimension == TextureDimension.TextureCube && textureCubePreviewMode == TextureCubePreviewMode.Full) + { + textureSize.X *= 4; + textureSize.Y *= 3; + } + + return textureSize; + } + } + + public void SetCubePreviewMode(TextureCubePreviewMode mode) + { + textureCubePreviewMode = mode; + } + + public void SetDepthToPreview(float depthValue) + { + var sliceCoordinate = 0f; + + if (texture != null && texture.Depth > 1) + sliceCoordinate = Math.Min(1f, depthValue / (texture.Depth - 1f)); + + currentEffect.Parameters.Set(Sprite3DBaseKeys.SliceCoordinate, sliceCoordinate); + } + + protected override void LoadContent() + { + // determine the adequate blend state to render the font + adequateBlendState = Asset.Type.PremultiplyAlpha ? BlendStates.AlphaBlend : BlendStates.NonPremultiplied; + + // Load the texture (but don't use streaming so it will be fully loaded) + texture = LoadAsset(AssetItem.Location, ContentManagerLoaderSettings.StreamingDisabled); + NotifyTextureLoaded?.Invoke(); + + // Update the effect + MicrothreadLocalDatabases.MountCommonDatabase(); + currentEffect.Parameters.Set(PreviewTextureParameters.Is3D, texture.ViewDimension == TextureDimension.Texture3D); + + // create texture views for cube textures. + if (texture.ViewDimension == TextureDimension.TextureCube) + { + for (var i = 0; i < texture.ArraySize; i++) + textureCubeViews[(TextureCubePreviewMode)i] = texture.ToTextureView(new TextureViewDescription { ArraySlice = i, Type = ViewType.ArrayBand }); + } + + textureSourceRegion = new Rectangle(0, 0, TextureWidth, TextureHeight); + + // TODO: Return LDR or HDR depending on texture bits (16bits is most likely HDR) + RenderingMode = RenderingMode.LDR; + } + + protected override void UnloadContent() + { + foreach (var textureView in textureCubeViews.Values) + { + textureView.Dispose(); + } + textureCubeViews.Clear(); + + if (texture != null) + { + UnloadAsset(texture); + texture = null; + } + } + + public override async Task Dispose() + { + await base.Dispose(); + + specificMipmapSamplerState.Dispose(); + } + + protected override void RenderSprite() + { + if (texture == null) + return; + + var origin = SpriteSize / 2 - SpriteOffsets; + var position = WindowSize / 2; + var color = new Color(1f, 1f, 1f, 1f); + + var swizzle = Swizzle(); + var colorAdd = new Color4(0, 0, 0, 0); + var layerDepth = 0f; + + SpriteBatch.Begin(Game.GraphicsContext, SpriteSortMode.Texture, adequateBlendState, specificMipmapSamplerState, effect: currentEffect); + + if (texture.ViewDimension == TextureDimension.Texture2D) + { + SpriteBatch.Draw(texture, position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + } + else if (texture.ViewDimension == TextureDimension.TextureCube) + { + if (textureCubePreviewMode == TextureCubePreviewMode.Full) + { + origin.X -= TextureWidth; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Top], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + + origin.X += TextureWidth; + origin.Y -= TextureHeight; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Left], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + + origin.X -= TextureWidth; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Front], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + + origin.X -= TextureWidth; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Right], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + + origin.X -= TextureWidth; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Back], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + + origin.X += 2 * TextureWidth; + origin.Y -= TextureHeight; + SpriteBatch.Draw(textureCubeViews[TextureCubePreviewMode.Bottom], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + } + else + { + SpriteBatch.Draw(textureCubeViews[textureCubePreviewMode], position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + } + } + else if (texture.ViewDimension == TextureDimension.Texture3D) + { + SpriteBatch.Draw(texture, position, textureSourceRegion, color, 0, origin, SpriteScale, SpriteEffects.None, ImageOrientation.AsIs, layerDepth, colorAdd, swizzle); + } + + SpriteBatch.End(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx new file mode 100644 index 0000000000..66f32a74b6 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; + +namespace StrideEffects +{ + params PreviewTextureParameters + { + bool Is3D; + }; + + effect PreviewTexture + { + using params PreviewTextureParameters; + + if(PreviewTextureParameters.Is3D) + { + mixin Sprite3DBase; + } + + mixin SpriteBatch; + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx.cs new file mode 100644 index 0000000000..2cda3407e1 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/PreviewTexture.sdfx.cs @@ -0,0 +1,46 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; +namespace StrideEffects +{ + [DataContract]public partial class PreviewTextureParameters : ShaderMixinParameters + { + public static readonly PermutationParameterKey Is3D = ParameterKeys.NewPermutation(); + }; + internal static partial class ShaderMixins + { + internal partial class PreviewTexture : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + if (context.GetParam(PreviewTextureParameters.Is3D)) + { + context.Mixin(mixin, "Sprite3DBase"); + } + context.Mixin(mixin, "SpriteBatch"); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("PreviewTexture", new PreviewTexture()); + } + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx new file mode 100644 index 0000000000..f6218fed35 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; + +namespace StrideEffects +{ + params SceneEditorParameters + { + //bool IsSelected; + bool IsEffectCompiling; + bool IsEffectError; + //bool IsMetaEntity; + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx.cs new file mode 100644 index 0000000000..4c3fd42923 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/SceneEditorParameters.sdfx.cs @@ -0,0 +1,26 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; +namespace StrideEffects +{ + [DataContract]public partial class SceneEditorParameters : ShaderMixinParameters + { + public static readonly PermutationParameterKey IsEffectCompiling = ParameterKeys.NewPermutation(); + public static readonly PermutationParameterKey IsEffectError = ParameterKeys.NewPermutation(); + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx new file mode 100644 index 0000000000..995b2fce96 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +namespace StrideEffects +{ + effect SelectedSprite + { + using params SpriteBaseKeys; + + mixin SpriteBatchShader; + mixin SelectedSpriteShader; + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx.cs new file mode 100644 index 0000000000..2488a0401d --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/SelectedSprite.sdfx.cs @@ -0,0 +1,37 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +namespace StrideEffects +{ + internal static partial class ShaderMixins + { + internal partial class SelectedSprite : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + context.Mixin(mixin, "SpriteBatchShader", context.GetParam(SpriteBaseKeys.ColorIsSRgb)); + context.Mixin(mixin, "SelectedSpriteShader"); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("SelectedSprite", new SelectedSprite()); + } + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx new file mode 100644 index 0000000000..1b7e509104 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; + +namespace StrideEffects +{ + effect StrideEditorForwardShadingEffect + { + using params SceneEditorParameters; + + // TODO: This file is similar to StrideEditorWireframeShadingEffect. We should try to look if we can merge them into a single one. + + // Early failover in case there was an effect compilation error + // We later could do a two level error detection: + // - first time at the end of effect (that is ran with nearly empty CompilerParameters) + // - if this one fails too, use this early failover which should have only very few basic shaders + if (SceneEditorParameters.IsEffectError) + { + mixin ShaderBase; + mixin ShadingBase; + mixin TransformationBase; + mixin TransformationWAndVP; + mixin CompilationErrorShader; + discard; + } + + // Include the standard forward shading effect + mixin StrideForwardShadingEffect; + + mixin child Picking; + mixin child Wireframe; + mixin child Highlight; + + // Add an effect compiling if it is not ready + if (SceneEditorParameters.IsEffectCompiling) + { + mixin EffectCompiling; + } + }; + + effect Wireframe + { + using params MaterialFrontBackBlendShaderKeys; + + mixin MaterialFrontBackBlendShader; + } + + effect Highlight + { + mixin HighlightShader; + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx.cs new file mode 100644 index 0000000000..68b28e3927 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorForwardShadingEffect.sdfx.cs @@ -0,0 +1,101 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; +namespace StrideEffects +{ + internal static partial class ShaderMixins + { + internal partial class StrideEditorForwardShadingEffect : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + if (context.GetParam(SceneEditorParameters.IsEffectError)) + { + context.Mixin(mixin, "ShaderBase"); + context.Mixin(mixin, "ShadingBase"); + context.Mixin(mixin, "TransformationBase"); + context.Mixin(mixin, "TransformationWAndVP"); + context.Mixin(mixin, "CompilationErrorShader"); + context.Discard(); + ; + } + context.Mixin(mixin, "StrideForwardShadingEffect"); + if (context.ChildEffectName == "Picking") + { + context.Mixin(mixin, "Picking"); + return; + } + if (context.ChildEffectName == "Wireframe") + { + context.Mixin(mixin, "Wireframe"); + return; + } + if (context.ChildEffectName == "Highlight") + { + context.Mixin(mixin, "Highlight"); + return; + } + if (context.GetParam(SceneEditorParameters.IsEffectCompiling)) + { + context.Mixin(mixin, "EffectCompiling"); + } + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("StrideEditorForwardShadingEffect", new StrideEditorForwardShadingEffect()); + } + } + } + internal static partial class ShaderMixins + { + internal partial class Wireframe : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + context.Mixin(mixin, "MaterialFrontBackBlendShader", context.GetParam(MaterialFrontBackBlendShaderKeys.UseNormalBackFace)); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("Wireframe", new Wireframe()); + } + } + } + internal static partial class ShaderMixins + { + internal partial class Highlight : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + context.Mixin(mixin, "HighlightShader"); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("Highlight", new Highlight()); + } + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx new file mode 100644 index 0000000000..6d699fc1be --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; + +namespace StrideEffects +{ + effect StrideEditorHighlightingEffect + { + mixin StrideForwardShadingEffect; + //mixin MaterialHighlightingShader; + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx.cs new file mode 100644 index 0000000000..a30b8d8731 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorHighlightingEffect.sdfx.cs @@ -0,0 +1,38 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; +namespace StrideEffects +{ + internal static partial class ShaderMixins + { + internal partial class StrideEditorHighlightingEffect : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + context.Mixin(mixin, "StrideForwardShadingEffect"); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("StrideEditorHighlightingEffect", new StrideEditorHighlightingEffect()); + } + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx new file mode 100644 index 0000000000..812d2fc7e6 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; + +namespace StrideEffects +{ + effect StrideEditorMaterialPreviewEffect + { + mixin StrideEditorForwardShadingEffect; + mixin SharedTextureCoordinate; + }; +} diff --git a/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx.cs b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx.cs new file mode 100644 index 0000000000..95cbfdec77 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/Shaders/StrideEditorMaterialPreviewEffect.sdfx.cs @@ -0,0 +1,39 @@ +// +// Do not edit this file yourself! +// +// This code was generated by Stride Shader Mixin Code Generator. +// To generate it yourself, please install Stride.VisualStudio.Package .vsix +// and re-save the associated .sdfx. +// + +using System; +using Stride.Core; +using Stride.Rendering; +using Stride.Graphics; +using Stride.Shaders; +using Stride.Core.Mathematics; +using Buffer = Stride.Graphics.Buffer; + +using Stride.Rendering.Data; +using Stride.Shaders.Compiler; +namespace StrideEffects +{ + internal static partial class ShaderMixins + { + internal partial class StrideEditorMaterialPreviewEffect : IShaderMixinBuilder + { + public void Generate(ShaderMixinSource mixin, ShaderMixinContext context) + { + context.Mixin(mixin, "StrideEditorForwardShadingEffect"); + context.Mixin(mixin, "SharedTextureCoordinate"); + } + + [ModuleInitializer] + internal static void __Initialize__() + + { + ShaderMixinManager.Register("StrideEditorMaterialPreviewEffect", new StrideEditorMaterialPreviewEffect()); + } + } + } +} diff --git a/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj b/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj index c7a1e6cc0d..42168affd8 100644 --- a/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj +++ b/sources/editor/Stride.Assets.Editor/Stride.Assets.Editor.csproj @@ -10,7 +10,7 @@ true - --auto-module-initializer + --auto-module-initializer --serialization diff --git a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs index 3c1c96cbff..f2c5d0fd93 100644 --- a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs +++ b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs @@ -48,11 +48,12 @@ public override void InitializeSession(ISessionViewModel session) var settingsProvider = new GameSettingsProviderService(session); session.ServiceProvider.RegisterService(settingsProvider); - var builderService = new GameStudioBuilderService(session, settingsProvider, buildDirectory); - session.ServiceProvider.RegisterService(builderService); + // FIXME xplat-editor broken for now + //var builderService = new GameStudioBuilderService(session, settingsProvider, buildDirectory); + //session.ServiceProvider.RegisterService(builderService); - var thumbnailService = new GameStudioThumbnailService(session, settingsProvider, builderService); - session.ServiceProvider.RegisterService(thumbnailService); + //var thumbnailService = new GameStudioThumbnailService(session, settingsProvider, builderService); + //session.ServiceProvider.RegisterService(thumbnailService); } public override void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes) diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/EditorGraphicsCompositorHelper.cs b/sources/editor/Stride.Assets.Editor/ViewModels/EditorGraphicsCompositorHelper.cs new file mode 100644 index 0000000000..a51393c93a --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/EditorGraphicsCompositorHelper.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Assets.Editor.ViewModels; + +public static class EditorGraphicsCompositorHelper +{ + public const string EditorForwardShadingEffect = "StrideEditorForwardShadingEffect"; + public const string EditorHighlightingEffect = "StrideEditorHighlightingEffect"; +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AnimationPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AnimationPreviewViewModel.cs new file mode 100644 index 0000000000..2c8620e8ba --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AnimationPreviewViewModel.cs @@ -0,0 +1,90 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Assets.Models; +using Stride.Core.Assets; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public sealed class AnimationPreviewViewModel : AssetPreviewViewModel, IAnimatedPreviewViewModel +{ + private AnimationPreview animationPreview; + private float timeScale = 1.0f; + private float currentTime; + private float duration; + private bool isValid; + private volatile bool updatingFromGame; + + public AnimationPreviewViewModel(ISessionViewModel session) + : base(session) + { + PlayCommand = new AnonymousCommand(ServiceProvider, Play); + PauseCommand = new AnonymousCommand(ServiceProvider, Pause); + } + + public float TimeScale { get { return timeScale; } set { SetValue(ref timeScale, value, () => animationPreview.SetTimeScale(value)); } } + + public float CurrentTime { get { return currentTime; } set { SetValue(ref currentTime, value); if (!updatingFromGame) animationPreview.SetCurrentTime(value); } } + + public float Duration { get { return duration; } private set { SetValue(ref duration, value); } } + + public bool IsValid { get { return isValid; } private set { SetValue(ref isValid, value); } } + + public ICommandBase PlayCommand { get; } + + public ICommandBase PauseCommand { get; } + + public bool IsPlaying => animationPreview?.IsPlaying ?? false; + + protected override void OnAttachPreview(AnimationPreview preview) + { + animationPreview = preview; + animationPreview.SetTimeScale(timeScale); + currentTime = 0.0f; + animationPreview.UpdateViewModelTime = UpdateViewModelTime; + + // Automatically play the animation + Play(); + } + + public static AssetItem? FindModelForPreview(AssetItem assetItem) + { + var animationAsset = (AnimationAsset)assetItem.Asset; + var previewModelAsset = animationAsset?.PreviewModel != null ? assetItem.Package.FindAssetFromProxyObject(animationAsset.PreviewModel) : null; + return previewModelAsset?.Asset is ModelAsset { Skeleton: not null } ? previewModelAsset : null; + } + + private void UpdateViewModelTime(bool isTimeValid, float current, float animDuration) + { + if (updatingFromGame) + return; + + updatingFromGame = true; + Dispatcher.LowPriorityInvokeAsync(() => + { + IsValid = isTimeValid; + CurrentTime = current; + Duration = animDuration; + updatingFromGame = false; + }); + } + + private void Play() + { + animationPreview.Play(); + PlayCommand.IsEnabled = false; + PauseCommand.IsEnabled = true; + } + + private void Pause() + { + animationPreview.Pause(); + PlayCommand.IsEnabled = true; + PauseCommand.IsEnabled = false; + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AssetPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AssetPreviewViewModel.cs new file mode 100644 index 0000000000..7d58984f72 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/AssetPreviewViewModel.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.ViewModels; +using Stride.Editor.Preview; +using Stride.Editor.Preview.ViewModels; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +/// +/// Base implementation of . +/// +public abstract class AssetPreviewViewModel : DispatcherViewModel, IAssetPreviewViewModel + where TPreview : IAssetPreview +{ + public ISessionViewModel Session { get; } + + protected AssetPreviewViewModel(ISessionViewModel session) + : base(session.ServiceProvider) + { + Session = session; + } + + /// + public void AttachPreview(IAssetPreview preview) + { + OnAttachPreview((TPreview)preview); + } + + protected abstract void OnAttachPreview(TPreview preview); +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/EntityPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/EntityPreviewViewModel.cs new file mode 100644 index 0000000000..7785ff5b5e --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/EntityPreviewViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public sealed class EntityPreviewViewModel : AssetPreviewViewModel +{ + public EntityPreviewViewModel(ISessionViewModel session) + : base(session) + { + } + + protected override void OnAttachPreview(EntityPreview preview) + { + // Nothing for now + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/HeightmapPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/HeightmapPreviewViewModel.cs new file mode 100644 index 0000000000..85de746d82 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/HeightmapPreviewViewModel.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public sealed class HeightmapPreviewViewModel : TextureBasePreviewViewModel +{ + private HeightmapPreview heightmapPreview; + private int previewHeightmapLength; + private int previewHeightmapWidth; + + public HeightmapPreviewViewModel(ISessionViewModel session) + : base(session) + { + } + + public int PreviewHeightmapLength { get { return previewHeightmapLength; } private set { SetValue(ref previewHeightmapLength, value); } } + + public int PreviewHeightmapWidth { get { return previewHeightmapWidth; } private set { SetValue(ref previewHeightmapWidth, value); } } + + protected override void OnAttachPreview(HeightmapPreview preview) + { + heightmapPreview = preview; + heightmapPreview.NotifyHeightmapLoaded += UpdateHeightmapInfo; + UpdateHeightmapInfo(); + AttachPreviewTexture(preview); + } + + private void UpdateHeightmapInfo() + { + PreviewHeightmapWidth = heightmapPreview.Width; + PreviewHeightmapLength = heightmapPreview.Length; + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/IAnimatedPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/IAnimatedPreviewViewModel.cs new file mode 100644 index 0000000000..6e6f78a2b9 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/IAnimatedPreviewViewModel.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Presentation.Commands; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +public interface IAnimatedPreviewViewModel +{ + ICommandBase PlayCommand { get; } + + ICommandBase PauseCommand { get; } + + bool IsPlaying { get; } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/MaterialPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/MaterialPreviewViewModel.cs new file mode 100644 index 0000000000..9743137a47 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/MaterialPreviewViewModel.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Editor.Annotations; +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public sealed class MaterialPreviewViewModel : AssetPreviewViewModel +{ + private MaterialPreview materialPreview; + private MaterialPreviewPrimitive selectedPrimitive; + + public MaterialPreviewViewModel(ISessionViewModel session) + : base(session) + { + } + + public Type PrimitiveTypes => typeof(MaterialPreviewPrimitive); + + public MaterialPreviewPrimitive SelectedPrimitive { get { return selectedPrimitive; } set { SetValue(ref selectedPrimitive, value); SetPrimitive(value); } } + + protected override void OnAttachPreview(MaterialPreview preview) + { + materialPreview = preview; + SetPrimitive(SelectedPrimitive); + } + + private void SetPrimitive(MaterialPreviewPrimitive primitive) + { + materialPreview.SetPrimitive(primitive); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ModelPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ModelPreviewViewModel.cs new file mode 100644 index 0000000000..a19305221e --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ModelPreviewViewModel.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public sealed class ModelPreviewViewModel : AssetPreviewViewModel +{ + private ModelPreview? modelPreview; + + public ModelPreviewViewModel(ISessionViewModel session) + : base(session) + { + ResetModelCommand = new AnonymousCommand(ServiceProvider, ResetModel); + } + + public ICommandBase ResetModelCommand { get; } + + protected override void OnAttachPreview(ModelPreview preview) + { + modelPreview = preview; + } + + private void ResetModel() + { + modelPreview?.ResetCamera(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ProceduralModelPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ProceduralModelPreviewViewModel.cs new file mode 100644 index 0000000000..faf3dcdba5 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/ProceduralModelPreviewViewModel.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class ProceduralModelPreviewViewModel : AssetPreviewViewModel +{ + private ProceduralModelPreview? modelPreview; + + public ProceduralModelPreviewViewModel(ISessionViewModel session) + : base(session) + { + ResetModelCommand = new AnonymousCommand(ServiceProvider, ResetModel); + } + + public ICommandBase ResetModelCommand { get; } + + protected override void OnAttachPreview(ProceduralModelPreview preview) + { + modelPreview = preview; + } + + private void ResetModel() + { + modelPreview?.ResetCamera(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SkyboxPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SkyboxPreviewViewModel.cs new file mode 100644 index 0000000000..e5db7e23e7 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SkyboxPreviewViewModel.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class SkyboxPreviewViewModel : AssetPreviewViewModel +{ + private SkyboxPreview? skyboxPreview; + private float glossiness = 0.6f; + private float metalness = 1.0f; + + public SkyboxPreviewViewModel(ISessionViewModel session) + : base(session) + { + ResetModelCommand = new AnonymousCommand(ServiceProvider, ResetModel); + } + + public ICommandBase ResetModelCommand { get; } + + public float Glossiness + { + get { return glossiness; } + set + { + SetValue(ref glossiness, value); + skyboxPreview?.SetGlossiness(value); + } + } + + public float Metalness + { + get { return metalness; } + set + { + SetValue(ref metalness, value); + skyboxPreview?.SetMetalness(value); + } + } + + protected override void OnAttachPreview(SkyboxPreview preview) + { + skyboxPreview = preview; + if (skyboxPreview != null) + { + SetValue(ref glossiness, skyboxPreview.Glossiness); + SetValue(ref metalness, skyboxPreview.Metalness); + } + } + + private void ResetModel() + { + skyboxPreview?.ResetCamera(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SoundPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SoundPreviewViewModel.cs new file mode 100644 index 0000000000..a573cf9287 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SoundPreviewViewModel.cs @@ -0,0 +1,81 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class SoundPreviewViewModel : AssetPreviewViewModel +{ + private SoundPreview? soundPreview; + private TimeSpan currentTime; + private TimeSpan duration; + private double masterVolume = 1.0; + private bool isAudioValid; + private volatile bool updatingFromGame; + + public SoundPreviewViewModel(ISessionViewModel session) + : base(session) + { + PlayCommand = new AnonymousCommand(ServiceProvider, Play); + PauseCommand = new AnonymousCommand(ServiceProvider, Pause); + } + + public bool IsAudioValid { get { return isAudioValid; } set { SetValue(ref isAudioValid, value); } } + + public double MasterVolume { get { return masterVolume; } set { SetValue(ref masterVolume, value); soundPreview?.SetMasterVolume(value); } } + + public double CurrentValue { get { return CurrentTime.TotalSeconds; } set { CurrentTime = TimeSpan.FromSeconds(value); } } + + public TimeSpan CurrentTime { get { return currentTime; } private set { SetValue(ref currentTime, value, nameof(CurrentTime), nameof(CurrentValue)); if (!updatingFromGame) soundPreview?.SetCurrentTime(value); } } + + public TimeSpan Duration { get { return duration; } private set { SetValue(ref duration, value); } } + + public ICommandBase PlayCommand { get; } + + public ICommandBase PauseCommand { get; } + + protected override void OnAttachPreview(SoundPreview preview) + { + soundPreview = preview; + soundPreview.ProvideDispatcher(Dispatcher); + PlayCommand.IsEnabled = !soundPreview.IsPlaying; + PauseCommand.IsEnabled = soundPreview.IsPlaying; + soundPreview.UpdateViewModelTime += UpdateViewModelTime; + } + + private void UpdateViewModelTime(bool hasAudio, bool isPlaying, TimeSpan current, TimeSpan soundDuration) + { + if (updatingFromGame) + return; + + updatingFromGame = true; + Dispatcher.InvokeAsync(() => + { + PlayCommand.IsEnabled = !isPlaying; + PauseCommand.IsEnabled = isPlaying; + CurrentTime = current; + IsAudioValid = hasAudio; + Duration = soundDuration; + updatingFromGame = false; + }); + } + + private void Play() + { + soundPreview?.Play(); + PlayCommand.IsEnabled = false; + PauseCommand.IsEnabled = true; + } + + private void Pause() + { + soundPreview?.Pause(); + PlayCommand.IsEnabled = true; + PauseCommand.IsEnabled = false; + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteFontPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteFontPreviewViewModel.cs new file mode 100644 index 0000000000..689e317a34 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteFontPreviewViewModel.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class SpriteFontPreviewViewModel : AssetPreviewViewModel +{ + private SpriteFontPreview? spriteFontPreview; + private string? previewString; + + public SpriteFontPreviewViewModel(ISessionViewModel session) + : base(session) + { + ClearTextCommand = new AnonymousCommand(ServiceProvider, () => PreviewString = string.Empty); + } + + public string? PreviewString { get { return previewString; } set { SetValue(ref previewString, value, () => spriteFontPreview?.SetPreviewString(value)); } } + + public ICommandBase ClearTextCommand { get; } + + protected override void OnAttachPreview(SpriteFontPreview preview) + { + spriteFontPreview = preview; + spriteFontPreview.SetPreviewString(PreviewString); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteSheetPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteSheetPreviewViewModel.cs new file mode 100644 index 0000000000..50bf5e598b --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteSheetPreviewViewModel.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class SpriteSheetPreviewViewModel : TextureBasePreviewViewModel +{ + private SpriteSheetPreview spriteSheetPreview; + private readonly int previewCurrentFrame = 1; + private SpriteSheetDisplayMode displayMode; + + public SpriteSheetPreviewViewModel(ISessionViewModel session) + : base(session) + { + PreviewPreviousFrameCommand = new AnonymousCommand(ServiceProvider, () => { if (PreviewFrameCount > 0) PreviewCurrentFrame = 1 + (PreviewCurrentFrame + PreviewFrameCount - 2) % PreviewFrameCount; }); + PreviewNextFrameCommand = new AnonymousCommand(ServiceProvider, () => { if (PreviewFrameCount > 0) PreviewCurrentFrame = 1 + (PreviewCurrentFrame + PreviewFrameCount) % PreviewFrameCount; }); + DependentProperties.Add(nameof(DisplayMode), new[] { nameof(PreviewCurrentFrame), nameof(PreviewFrameCount) }); + } + + public int PreviewCurrentFrame { get { return Math.Min(PreviewFrameCount, spriteSheetPreview.CurrentFrame + 1); } set { SetValue(value - 1 != spriteSheetPreview.CurrentFrame, () => spriteSheetPreview.CurrentFrame = value - 1); } } + + public int PreviewFrameCount => spriteSheetPreview.FrameCount; + + public SpriteSheetDisplayMode DisplayMode { get { return displayMode; } set { SetValue(ref displayMode, value, () => { spriteSheetPreview.Mode = value; }); } } + + public CommandBase PreviewPreviousFrameCommand { get; } + + public CommandBase PreviewNextFrameCommand { get; } + + protected override void OnAttachPreview(SpriteSheetPreview preview) + { + AttachPreviewTexture(preview); + spriteSheetPreview = preview; + DisplayMode = spriteSheetPreview.Mode; + // Reset the current frame from the view model into the preview + PreviewCurrentFrame = previewCurrentFrame; + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteStudioSheetPreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteStudioSheetPreviewViewModel.cs new file mode 100644 index 0000000000..d3f094149b --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/SpriteStudioSheetPreviewViewModel.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +// FIXME: this view model should be in the SpriteStudio offline assembly! Can't be done now, because of a circular reference in CompilerApp referencing SpriteStudio, and Editor referencing CompilerApp +[AssetPreviewViewModel] +public class SpriteStudioSheetPreviewViewModel : AssetPreviewViewModel +{ + private SpriteStudioSheetPreview? spriteStudioSheetPreview; + + public SpriteStudioSheetPreviewViewModel(ISessionViewModel session) + : base(session) + { + ResetModelCommand = new AnonymousCommand(ServiceProvider, ResetModel); + } + + public ICommandBase ResetModelCommand { get; } + + protected override void OnAttachPreview(SpriteStudioSheetPreview preview) + { + spriteStudioSheetPreview = preview; + } + + private void ResetModel() + { + spriteStudioSheetPreview?.ResetCamera(); + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TextureBasePreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TextureBasePreviewViewModel.cs new file mode 100644 index 0000000000..23a211c095 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TextureBasePreviewViewModel.cs @@ -0,0 +1,95 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Assets.Editor.Preview; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +public abstract class TextureBasePreviewViewModel : AssetPreviewViewModel + where TPreview : IAssetPreview, ITextureBasePreview +{ + private ITextureBasePreview textureBasePreview; + private string previewSelectedMipMap; + + private float spriteScale; + + protected TextureBasePreviewViewModel(ISessionViewModel session) + : base(session) + { + // Initialize texture preview + PreviewAvailableMipMaps = new ObservableList(); + previewSelectedMipMap = "Level 0"; + PreviewZoomInCommand = new AnonymousCommand(ServiceProvider, ZoomIn); + PreviewZoomOutCommand = new AnonymousCommand(ServiceProvider, ZoomOut); + PreviewFitOnScreenCommand = new AnonymousCommand(ServiceProvider, FitOnScreen); + PreviewScaleToRealSizeCommand = new AnonymousCommand(ServiceProvider, ScaleToRealSize); + } + + public string PreviewSelectedMipMap + { + get { return previewSelectedMipMap; } + set + { + if (value != null) + { + SetValue(ref previewSelectedMipMap, value); + textureBasePreview?.DisplayMipMap(ParseMipMapLevel(value)); + } + } + } + + public ObservableList PreviewAvailableMipMaps { get; } + + public float SpriteScale { get { return spriteScale; } set { SetValue(ref spriteScale, value); } } + + public ICommandBase PreviewZoomInCommand { get; } + + public ICommandBase PreviewZoomOutCommand { get; } + + public ICommandBase PreviewFitOnScreenCommand { get; } + + public ICommandBase PreviewScaleToRealSizeCommand { get; } + + protected void AttachPreviewTexture(IAssetPreview preview) + { + textureBasePreview = (ITextureBasePreview)preview; + var availableMipMaps = textureBasePreview.GetAvailableMipMaps(); + PreviewAvailableMipMaps.Clear(); + PreviewAvailableMipMaps.AddRange(availableMipMaps.Select(x => $"Level {x}")); + textureBasePreview.DisplayMipMap(ParseMipMapLevel(PreviewSelectedMipMap)); + textureBasePreview.SpriteScaleChanged += (s, e) => SpriteScale = textureBasePreview.SpriteScale; + } + + private void ZoomIn() + { + textureBasePreview.ZoomIn(null); + } + + private void ZoomOut() + { + textureBasePreview.ZoomOut(null); + } + + private void FitOnScreen() + { + textureBasePreview.FitOnScreen(); + } + + private void ScaleToRealSize() + { + textureBasePreview.ScaleToRealSize(); + } + + private static int ParseMipMapLevel(string level) + { + if (level == null) + return 0; + + int.TryParse(level["Level ".Length..], out var result); + return result; + } +} diff --git a/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TexturePreviewViewModel.cs b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TexturePreviewViewModel.cs new file mode 100644 index 0000000000..13f1d3ae55 --- /dev/null +++ b/sources/editor/Stride.Assets.Editor/ViewModels/Preview/TexturePreviewViewModel.cs @@ -0,0 +1,61 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.Commands; +using Stride.Editor.Annotations; +using Stride.Graphics; + +namespace Stride.Assets.Editor.ViewModels.Preview; + +[AssetPreviewViewModel] +public class TexturePreviewViewModel : TextureBasePreviewViewModel +{ + private TexturePreview texturePreview; + private int previewTextureDepth; + private TextureDimension previewDimension; + private int previewTextureHeight; + private int previewTextureWidth; + private TextureCubePreviewMode selectedCubePreviewMode = TextureCubePreviewMode.Full; + private float selectedDepth; + + public TexturePreviewViewModel(ISessionViewModel session) + : base(session) + { + PreviewPreviousDepthCommand = new AnonymousCommand(ServiceProvider, () => SelectedDepth = (float)(Math.Ceiling(SelectedDepth + PreviewTextureDepth - 1) % PreviewTextureDepth)); + PreviewNextDepthCommand = new AnonymousCommand(ServiceProvider, () => SelectedDepth = (float)(Math.Floor(SelectedDepth + 1) % PreviewTextureDepth)); + } + + public int PreviewTextureDepth { get { return previewTextureDepth; } private set { SetValue(ref previewTextureDepth, value); } } + + public TextureDimension PreviewDimension { get { return previewDimension; } private set { SetValue(ref previewDimension, value); } } + + public int PreviewTextureHeight { get { return previewTextureHeight; } private set { SetValue(ref previewTextureHeight, value); } } + + public int PreviewTextureWidth { get { return previewTextureWidth; } private set { SetValue(ref previewTextureWidth, value); } } + + public TextureCubePreviewMode SelectedCubePreviewMode { get { return selectedCubePreviewMode; } set { SetValue(ref selectedCubePreviewMode, value, () => texturePreview.SetCubePreviewMode(value)); } } + + public float SelectedDepth { get { return selectedDepth; } set { SetValue(ref selectedDepth, value, () => texturePreview.SetDepthToPreview(value)); } } + + public ICommandBase PreviewPreviousDepthCommand { get; } + + public ICommandBase PreviewNextDepthCommand { get; } + + protected override void OnAttachPreview(TexturePreview preview) + { + texturePreview = preview; + texturePreview.NotifyTextureLoaded += UpdateTextureInfo; + UpdateTextureInfo(); + AttachPreviewTexture(preview); + } + + private void UpdateTextureInfo() + { + PreviewDimension = texturePreview.Dimension; + PreviewTextureWidth = texturePreview.TextureWidth; + PreviewTextureHeight = texturePreview.TextureHeight; + PreviewTextureDepth = texturePreview.TextureDepth; + } +} diff --git a/sources/editor/Stride.Assets.Presentation/Stride.Assets.Presentation.csproj b/sources/editor/Stride.Assets.Presentation/Stride.Assets.Presentation.csproj index 304b69fc26..626c480aed 100644 --- a/sources/editor/Stride.Assets.Presentation/Stride.Assets.Presentation.csproj +++ b/sources/editor/Stride.Assets.Presentation/Stride.Assets.Presentation.csproj @@ -10,7 +10,7 @@ true - --auto-module-initializer + --auto-module-initializer --serialization diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IAssetPreviewService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetPreviewService.cs new file mode 100644 index 0000000000..843f64daa3 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IAssetPreviewService.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.Services; + +public interface IAssetPreviewService : IDisposable +{ + void SetAssetToPreview(AssetViewModel asset); + + object GetCurrentPreviewView(); + + event EventHandler PreviewAssetUpdated; + + void OnShowPreview(); + + void OnHidePreview(); +} diff --git a/sources/editor/Stride.Editor/Build/AnonymousAssetBuildUnit.cs b/sources/editor/Stride.Editor/Build/AnonymousAssetBuildUnit.cs new file mode 100644 index 0000000000..1d04a7d750 --- /dev/null +++ b/sources/editor/Stride.Editor/Build/AnonymousAssetBuildUnit.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; + +namespace Stride.Editor.Build; + +public class AnonymousAssetBuildUnit : AssetBuildUnit +{ + private readonly Func compile; + + public AnonymousAssetBuildUnit(AssetBuildUnitIdentifier identifier, Func compile) + : base(identifier) + { + this.compile = compile; + } + + protected override ListBuildStep Prepare() + { + return compile(); + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/ContentLoader/ContentLoadEventArgs.cs b/sources/editor/Stride.Editor/EditorGame/ContentLoader/ContentLoadEventArgs.cs new file mode 100644 index 0000000000..e36df88e33 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/ContentLoader/ContentLoadEventArgs.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Editor.EditorGame.ContentLoader; + +public class ContentLoadEventArgs : EventArgs +{ + public ContentLoadEventArgs(int contentLoadingCount) + { + ContentLoadingCount = contentLoadingCount; + } + + public int ContentLoadingCount { get; } +} diff --git a/sources/editor/Stride.Editor/EditorGame/ContentLoader/EditorContentLoader.cs b/sources/editor/Stride.Editor/EditorGame/ContentLoader/EditorContentLoader.cs new file mode 100644 index 0000000000..64e1939e19 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/ContentLoader/EditorContentLoader.cs @@ -0,0 +1,643 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + + +using System.Diagnostics.Contracts; +using System.Threading.Tasks.Dataflow; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.MicroThreading; +using Stride.Core.Serialization.Contents; +using Stride.Core.Storage; +using Stride.Core.Presentation.Services; +using Stride.Assets; +using Stride.Assets.Entities; +using Stride.Assets.Materials; +using Stride.Assets.Navigation; +using Stride.Assets.Textures; +using Stride.Editor.Build; +using Stride.Editor.EditorGame.Game; +using Stride.Graphics; +using Stride.Navigation; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Assets.Editor.ViewModels; + +namespace Stride.Editor.EditorGame.ContentLoader; + +/// +/// A class that handles loading/unloading referenced resources for a game used in an editor. +/// +public sealed class EditorContentLoader : IEditorContentLoader +{ + private readonly ILogger logger; + private readonly GameStudioDatabase database; + private readonly GameSettingsProviderService settingsProvider; + private RenderingMode currentRenderingMode; + private ColorSpace currentColorSpace; + private ObjectId currentNavigationGroupsHash; + private int loadingAssetCount; +#if DEBUG + private ContentManagerStats? debugStats; + private bool enableReferenceLogging = true; +#endif + + /// + /// RW lock for and . + /// + private readonly object assetsToReloadLock = new(); + + /// + /// The assets currently waiting for a reload to be done. + /// + private readonly Queue assetsToReloadQueue = new(); + + /// + /// Fast lookup to know what is in . + /// + private readonly Dictionary assetsToReloadMapping = new(); + + /// + /// Initializes a new instance of the class + /// + /// The dispatcher to the game thread. + /// The logger to use to log operations. + /// The asset associated with this instance. + /// The editor game associated with this instance. + public EditorContentLoader(IDispatcherService gameDispatcher, ILogger logger, AssetEditorViewModel editor, EditorServiceGame game) + { + GameDispatcher = gameDispatcher; + this.logger = logger; + Editor = editor; + Game = game; + + Session.AssetPropertiesChanged += AssetPropertiesChanged; + Manager = new LoaderReferenceManager(GameDispatcher, this); + database = editor.ServiceProvider.Get(); + settingsProvider = editor.ServiceProvider.Get(); + settingsProvider.GameSettingsChanged += GameSettingsChanged; + currentRenderingMode = settingsProvider.CurrentGameSettings.GetOrCreate().RenderingMode; + currentColorSpace = settingsProvider.CurrentGameSettings.GetOrCreate().ColorSpace; + currentNavigationGroupsHash = settingsProvider.CurrentGameSettings.GetOrDefault().ComputeGroupsHash(); + } + + public LoaderReferenceManager Manager { get; } + + /// + /// The asset view model associated to this instance. + /// + private AssetViewModel Asset => Editor.Asset; + + private AssetEditorViewModel Editor { get; } + + /// + /// A dictionary storing the urls used to load an asset, to use the same at unload, in case the asset has been renamed. + /// + private Dictionary AssetLoadingTimeUrls { get; } = new Dictionary(); + + /// + /// A dispatcher to the game thread. + /// + private IDispatcherService GameDispatcher { get; } + + /// + /// The editor associated to this instance. + /// + private ISessionViewModel Session => Asset.Session; + + /// + /// Types that support fast reloading (ie. updating existing object instead of loading a new one and updating references). + /// + // TODO: add an Attribute on Assets to specify if they are fast-reloadable (plugin approach) + private static ICollection FastReloadTypes => new[] { typeof(MaterialAsset), typeof(TextureAsset) }; + + /// + /// The associated with this instance. + /// + private EditorServiceGame Game { get; } + + /// + /// Raised when an asset start to be compiled and loaded. + /// + public event EventHandler? AssetLoading; + + /// + /// Raised when an asset has been loaded. + /// + public event EventHandler? AssetLoaded; + + public void BuildAndReloadAsset(AssetId assetId) + { + var assetToRebuild = Session.GetAssetById(assetId)?.AssetItem; + if (assetToRebuild == null) + return; + + if (assetToRebuild.Asset is SceneAsset) + { + // We never build the SceneAsset directly. Its content is build separately. + // Note that the only case where this could happen is if a script or component references a scene directly, which is a bad design (should use scene streaming or prefabs instead). + return; + } + + Session.Dispatcher.InvokeAsync(() => BuildAndReloadAssets(assetToRebuild.Yield())); + } + + public T GetRuntimeObject(AssetItem assetItem) where T : class + { + if (assetItem == null) throw new ArgumentNullException(nameof(assetItem)); + + var url = GetLoadingTimeUrl(assetItem); + return !string.IsNullOrEmpty(url) ? Game.Content.Get(url) : default(T); + } + + public Task ReserveDatabaseSyncLock() + { + return database.ReserveSyncLock(); + } + + public Task LockDatabaseAsynchronously() + { + return database.LockAsync(); + } + + /// + void IDisposable.Dispose() + { + Cleanup(); + } + + private void Cleanup() + { + settingsProvider.GameSettingsChanged -= GameSettingsChanged; + Session.AssetPropertiesChanged -= AssetPropertiesChanged; + } + + private async Task> BuildAndReloadAssets(IEnumerable assetsToRebuild) + { + var assetList = assetsToRebuild.ToList(); + if (assetList.Count == 0) + return null; + + logger?.Debug($"Starting BuildAndReloadAssets for assets {string.Join(", ", assetList.Select(x => x.Location))}"); + var value = Interlocked.Increment(ref loadingAssetCount); + AssetLoading?.Invoke(this, new ContentLoadEventArgs(value)); + try + { + // Rebuild the assets + await Task.WhenAll(assetList.Select(x => database.Build(x))); + + logger?.Debug("Assets have been built"); + // Unload the previous versions of assets and (re)load the newly build ones. + var reloadedObjects = await UnloadAndReloadAssets(assetList); + + Game.TriggerActiveRenderStageReevaluation(); + return reloadedObjects; + } + finally + { + value = Interlocked.Decrement(ref loadingAssetCount); + AssetLoaded?.Invoke(this, new ContentLoadEventArgs(value)); + logger?.Debug($"Completed BuildAndReloadAssets for assets {string.Join(", ", assetList.Select(x => x.Location))}"); + } + } + + /// + /// Generates the list of assets that are referenced directly and indirectly by entities + /// + [Pure] + private async Task>> ComputeReferences() + { + var dependencyManager = Session.DependencyManager; + var references = new Dictionary>(); + var ids = (await Manager.ComputeReferencedAssets()).ToList(); + foreach (var id in ids) + { + var referencedAsset = Session.GetAssetById(id); + if (referencedAsset == null) + continue; + + var dependencies = dependencyManager.ComputeDependencies(referencedAsset.AssetItem.Id, AssetDependencySearchOptions.Out | AssetDependencySearchOptions.Recursive, ContentLinkType.Reference); + if (dependencies != null) + { + var entry = references.GetOrCreateValue(referencedAsset.Id); + entry.Add(referencedAsset.Id); + foreach (var dependency in dependencies.LinksOut) + { + entry = references.GetOrCreateValue(dependency.Item.Id); + entry.Add(referencedAsset.Id); + } + } + } + + return references; + } + + private string GetLoadingTimeUrl(AssetItem assetItem) + { + return GetLoadingTimeUrl(assetItem.Id) ?? assetItem.Location; + } + + private string GetLoadingTimeUrl(AssetId assetId) + { + string url; + AssetLoadingTimeUrls.TryGetValue(assetId, out url); + return url; + } + + private bool IsCurrentlyLoaded(AssetId assetId, bool loadedManuallyOnly = false) + { + string url; + return AssetLoadingTimeUrls.TryGetValue(assetId, out url) && Game.Content.IsLoaded(url, loadedManuallyOnly); + } + + private Task> UnloadAndReloadAssets(ICollection assets) + { + var reloadingAssets = new List(); + + // Add assets to assetsToReloadQueue + lock (assetsToReloadLock) + { + foreach (var asset in assets) + { + ReloadingAsset reloadingAsset; + + // Make sure it is not already in the queue (otherwise reuse it) + if (!assetsToReloadMapping.TryGetValue(asset, out reloadingAsset)) + { + assetsToReloadQueue.Enqueue(reloadingAsset = new ReloadingAsset(asset)); + assetsToReloadMapping.Add(asset, reloadingAsset); + } + + reloadingAssets.Add(reloadingAsset); + } + } + + // Ask Game thread to check our collection + // Note: if there was many requests during same frame, they will be grouped and only first invocation of this method will process all of them in a batch + CheckAssetsToReload().Forget(); + + // Wait for all of the currently requested assets to be processed + return Task.WhenAll(reloadingAssets.Select(x => x.Result.Task)) + .ContinueWith(task => + { + // Convert to expected output format + return reloadingAssets.Where(x => x.Result.Task.Result != null).ToDictionary(x => x.AssetItem, x => x.Result.Task.Result); + }); + } + + private Task CheckAssetsToReload() + { + return GameDispatcher.InvokeTask(async () => + { + List assets; + + // Get assets to reload from queue + lock (assetsToReloadLock) + { + // Nothing left, early exit + if (assetsToReloadQueue.Count == 0) + return; + + // Copy locally and clear queue + assets = assetsToReloadQueue.ToList(); + assetsToReloadQueue.Clear(); + assetsToReloadMapping.Clear(); + } + + // Update the colorspace + Game.UpdateColorSpace(currentColorSpace); + + var objToFastReload = new Dictionary(); + + using (await database.MountInCurrentMicroThread()) + { + // First, unload assets + foreach (var assetToUnload in assets) + { + if (FastReloadTypes.Contains(assetToUnload.AssetItem.Asset.GetType()) && IsCurrentlyLoaded(assetToUnload.AssetItem.Asset.Id)) + { + // If this type supports fast reload, retrieve the current (old) value via a load + var type = AssetRegistry.GetContentType(assetToUnload.AssetItem.Asset.GetType()); + string url = GetLoadingTimeUrl(assetToUnload.AssetItem); + var oldValue = Game.Content.Get(type, url); + if (oldValue != null) + { + logger?.Debug($"Preparing fast-reload of {assetToUnload.AssetItem.Location}"); + objToFastReload.Add(url, oldValue); + } + } + else if (IsCurrentlyLoaded(assetToUnload.AssetItem.Asset.Id, true)) + { + // Unload this object if it has already been loaded. + logger?.Debug($"Unloading {assetToUnload.AssetItem.Location}"); + await UnloadAsset(assetToUnload.AssetItem.Asset.Id); + } + } + + // Process fast-reload objects + var nonFastReloadAssets = new List(); + foreach (var assetToLoad in assets) + { + object oldValue; + string url = GetLoadingTimeUrl(assetToLoad.AssetItem); + if (FastReloadTypes.Contains(assetToLoad.AssetItem.Asset.GetType()) && objToFastReload.TryGetValue(url, out oldValue)) + { + // Fill oldValue with the values from the database without reloading the object. + // As a result, no reference needs to be updated. + logger?.Debug($"Fast-reloading {assetToLoad.AssetItem.Location}"); + ReloadContent(oldValue, assetToLoad.AssetItem); + var loadedObject = oldValue; + + // This fast-reloaded content might have been already loaded through private reference, but if we're reloading it here, + // it means that we expect a public reference (eg. it has just been referenced publicly). Reload() won't increase public reference count + // so we have to do it manually. + if (!IsCurrentlyLoaded(assetToLoad.AssetItem.Id, true)) + { + var type = AssetRegistry.GetContentType(assetToLoad.AssetItem.Asset.GetType()); + LoadContent(type, url); + } + + await Manager.ReplaceContent(assetToLoad.AssetItem.Asset.Id, loadedObject); + + assetToLoad.Result.SetResult(loadedObject); + } + else + { + nonFastReloadAssets.Add(assetToLoad); + } + } + + // Load all async object in a separate task + // We avoid Game.Content.LoadAsync, which would wait next frame between every loaded asset + var microThread = Scheduler.CurrentMicroThread; + var bufferBlock = new BufferBlock>(); + var task = Task.Run(() => + { + var initialContext = SynchronizationContext.Current; + // This synchronization context gives access to any MicroThreadLocal values. The database to use might actually be micro thread local. + SynchronizationContext.SetSynchronizationContext(new MicrothreadProxySynchronizationContext(microThread)); + + foreach (var assetToLoad in nonFastReloadAssets) + { + var type = AssetRegistry.GetContentType(assetToLoad.AssetItem.Asset.GetType()); + string url = GetLoadingTimeUrl(assetToLoad.AssetItem); + + object loadedObject = null; + try + { + loadedObject = LoadContent(type, url); + } + catch (Exception e) + { + logger?.Error($"Unable to load asset [{assetToLoad.AssetItem.Location}].", e); + } + + // Post it in BufferBlock so that the game-side loop can process results incrementally + bufferBlock.Post(new KeyValuePair(assetToLoad, loadedObject)); + } + + bufferBlock.Complete(); + + SynchronizationContext.SetSynchronizationContext(initialContext); + }); + + while (await bufferBlock.OutputAvailableAsync()) + { + var item = await bufferBlock.ReceiveAsync(); + + var assetToLoad = item.Key; + var loadedObject = item.Value; + + if (loadedObject != null) + { + // If it's the first load of this asset, keep its loading url + if (!AssetLoadingTimeUrls.ContainsKey(assetToLoad.AssetItem.Asset.Id)) + AssetLoadingTimeUrls.Add(assetToLoad.AssetItem.Asset.Id, assetToLoad.AssetItem.Location); + + // Add assets that were not previously loaded to the assetLoadingTimeUrls map. + var dependencyManager = Asset.AssetItem.Package.Session.DependencyManager; + var dependencies = dependencyManager.ComputeDependencies(Asset.AssetItem.Id, AssetDependencySearchOptions.Out | AssetDependencySearchOptions.Recursive, ContentLinkType.Reference); + if (dependencies != null) + { + foreach (var dependency in dependencies.LinksOut) + { + if (!AssetLoadingTimeUrls.ContainsKey(dependency.Item.Id)) + AssetLoadingTimeUrls.Add(dependency.Item.Id, dependency.Item.Location); + } + } + + // Remove assets that were previously loaded but are not anymore from the assetLoadingTimeUrls map. + foreach (var loadedUrls in AssetLoadingTimeUrls.Where(x => !Game.Content.IsLoaded(x.Value)).ToList()) + { + AssetLoadingTimeUrls.Remove(loadedUrls.Key); + } + } + + await Manager.ReplaceContent(assetToLoad.AssetItem.Asset.Id, loadedObject); + + assetToLoad.Result.SetResult(loadedObject); + } + + // Make sure everything is complete before we return + await task; + } + }); + } + + public async Task UnloadAsset(AssetId id) + { + GameDispatcher.EnsureAccess(); + + // Unload this object if it has already been loaded. + using (await database.LockAsync()) + { + string url; + if (AssetLoadingTimeUrls.TryGetValue(id, out url)) + { + UnloadContent(url); + // Remove assets that were previously loaded but are not anymore from the assetLoadingTimeUrls map. + foreach (var loadedUrls in AssetLoadingTimeUrls.Where(x => !Game.Content.IsLoaded(x.Value)).ToList()) + { + AssetLoadingTimeUrls.Remove(loadedUrls.Key); + } + } + } + } + + private async void AssetPropertiesChanged(object? sender, AssetChangedEventArgs e) + { + // Get the list of assets directly referenced by entities, that reference one of the modified asset. (eg. get models when a material is changed) + var allAssetsToRebuild = new HashSet(); + + // Don't propagate property changes until we're fully initialized. + // FIXME xplat-editor + //await Asset.EditorInitialized; + + // If GameSettingsAssets.ColorSpace was changed, rebuild the whole scene + var assets = e.Assets.ToList(); + + var references = await ComputeReferences(); + var assetsToProcess = new Queue(assets); + var processedAssets = new HashSet(assets); + + // Recurse through assets that depend on this one (recursively) + while (assetsToProcess.Count > 0) + { + var assetToProcess = assetsToProcess.Dequeue(); + HashSet modifiedAssetReferencers; + + // Check if the asset is referenced in the scene. + if (!references.TryGetValue(assetToProcess.Id, out modifiedAssetReferencers)) + continue; + + // We wait for a lock of the database. The lock we retrieve is synchronous, do not await in this using block! + using ((await database.ReserveSyncLock()).Lock()) + { + // There is two patterns: + // - Object is a fast-reloadable & already loaded object: we can replace its content internally without loading a new object and recreating any of its referencers + // Note that we still need to process referencers in case it is used as a compile-time dependency (i.e. Material layer) + // - Object is not a fast-reloadable object: we need to find its referencers (recursively) until we find node directly referenced by the scene (part of modifiedAssetReferencers) and reload this one + var isFastReloadCurrentlyLoaded = FastReloadTypes.Contains(assetToProcess.AssetType) && IsCurrentlyLoaded(assetToProcess.Id); + if (modifiedAssetReferencers.Contains(assetToProcess.Id) || isFastReloadCurrentlyLoaded) + { + allAssetsToRebuild.Add(assetToProcess); + } + + // Find dependent assets + foreach (var referencer in assetToProcess.Dependencies.ReferencerAssets) + { + var node = database.AssetDependenciesCompiler.BuildDependencyManager.FindOrCreateNode(referencer.AssetItem, typeof(AssetCompilationContext)); + node.Analyze(database.CompilerContext); + foreach (var reference in node.References) + { + // Check if this reference is actually a compile-time dependency + // Or if it's not a fast reloadable type (in which case we also need to process its references) + if (reference.Target.AssetItem.Id == assetToProcess.Id && (reference.HasOne(BuildDependencyType.CompileContent | BuildDependencyType.CompileAsset) || !isFastReloadCurrentlyLoaded)) + { + // If yes, process this asset later + if (processedAssets.Add(referencer)) + { + assetsToProcess.Enqueue(referencer); + } + } + } + } + } + } + + await BuildAndReloadAssets(allAssetsToRebuild.Select(x => x.AssetItem)); + } + + private async void GameSettingsChanged(object? sender, GameSettingsChangedEventArgs e) + { + // Remark: we assume that GameStudioDatabase has already updated the compiler game settings, + // which is the case because this service is registered before the creation of this EditorContentLoader + if (e.GameSettings.GetOrCreate().ColorSpace != currentColorSpace || e.GameSettings.GetOrCreate().RenderingMode != currentRenderingMode) + { + currentRenderingMode = e.GameSettings.GetOrCreate().RenderingMode; + currentColorSpace = e.GameSettings.GetOrCreate().ColorSpace; + + await BuildAndReloadAssets(Asset.Dependencies.ReferencedAssets.Select(x => x.AssetItem)); + } + + // Update navigation meshes that are previewed inside the current scene when the game settings's group settings for navigation meshes change + var navigationGroupsHash = e.GameSettings.GetOrDefault().ComputeGroupsHash(); + if (navigationGroupsHash != currentNavigationGroupsHash) + { + currentNavigationGroupsHash = navigationGroupsHash; + + await BuildAndReloadAssets(Session.AllAssets.Where(x => x.AssetType == typeof(NavigationMeshAsset)).Select(x=>x.AssetItem)); + } + } + + private object LoadContent(Type type, string url) + { +#if DEBUG + if (enableReferenceLogging) + { + debugStats = debugStats ?? Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Loading {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + var result = Game.Content.Load(type, url); +#if DEBUG + if (enableReferenceLogging) + { + debugStats = Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Loaded {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + return result; + } + + private void UnloadContent(string url) + { +#if DEBUG + if (enableReferenceLogging) + { + debugStats = debugStats ?? Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Unloading {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + Game.Content.Unload(url); +#if DEBUG + if (enableReferenceLogging) + { + debugStats = Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Unloaded {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + } + + private void ReloadContent(object obj, AssetItem assetItem) + { + var url = assetItem.Location; +#if DEBUG + if (enableReferenceLogging) + { + debugStats = debugStats ?? Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Reloading {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + Game.Content.Reload(obj, url); + AssetLoadingTimeUrls[assetItem.Id] = url; + +#if DEBUG + if (enableReferenceLogging) + { + debugStats = Game.Content.GetStats(); + var entry = debugStats.LoadedAssets.FirstOrDefault(x => x.Url == url); + logger?.Debug($"Reloaded {url} (Pub: {entry?.PublicReferenceCount ?? 0}, Priv:{entry?.PrivateReferenceCount ?? 0})"); + } +#endif + } + + /// + /// Represents an asset being reloaded asynchronously. + /// + class ReloadingAsset + { + public ReloadingAsset(AssetItem assetItem) + { + AssetItem = assetItem; + } + + /// + /// The asset being reloaded. + /// + public AssetItem AssetItem { get; } + + /// + /// The task containg the runtime value of the reloaded asset. + /// + public TaskCompletionSource Result { get; } = new TaskCompletionSource(); + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/ContentLoader/IEditorContentLoader.cs b/sources/editor/Stride.Editor/EditorGame/ContentLoader/IEditorContentLoader.cs new file mode 100644 index 0000000000..5b993c634f --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/ContentLoader/IEditorContentLoader.cs @@ -0,0 +1,58 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core; + +namespace Stride.Editor.EditorGame.ContentLoader; + +// TODO: Unexpose this interface from view models. Expose only GetRuntimeObject for services +public interface IEditorContentLoader : IDisposable +{ + LoaderReferenceManager Manager { get; } + + /// + /// Raised when an asset start to be compiled and loaded. + /// + event EventHandler AssetLoading; + + /// + /// Raised when an asset has been loaded. + /// + event EventHandler AssetLoaded; + + /// + /// Builds and reloads if necessary the asset corresponding to the given id. + /// + /// The id of the asset to build and reload. + void BuildAndReloadAsset(AssetId assetId); + + /// + /// Unloads the asset corresponding to the given id. This must be called from the game thread. + /// + /// The id of the asset to unload. + /// A task that completes when the asset has been unloaded. + Task UnloadAsset(AssetId id); + + /// + /// Locks synchronously (using a ) the database until the returned object is disposed. + /// This method must be called out of a micro-thread. + /// + /// A task that completes when the lock is acquired. + Task ReserveDatabaseSyncLock(); + + /// + /// Locks asynchronously the database until the returned object is disposed. + /// This method must be called from a micro-thread. + /// + /// A task that completes when the lock is acquired. + Task LockDatabaseAsynchronously(); + + /// + /// Retrieves the run-time object corresponding to the given asset item. + /// + /// The expected type of the run-time object. + /// The asset corresponding to the run-time object to retrieve. + /// The run-time object corresponding to the given asset item if it exists, null otherwise. + T GetRuntimeObject(AssetItem assetItem) where T : class; +} diff --git a/sources/editor/Stride.Editor/EditorGame/ContentLoader/LoaderReferenceManager.cs b/sources/editor/Stride.Editor/EditorGame/ContentLoader/LoaderReferenceManager.cs new file mode 100644 index 0000000000..1c6a6a5009 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/ContentLoader/LoaderReferenceManager.cs @@ -0,0 +1,194 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Presentation.Services; +using Stride.Core.Quantum; + +namespace Stride.Editor.EditorGame.ContentLoader; + +public class LoaderReferenceManager +{ + private struct ReferenceAccessor + { + private readonly IGraphNode contentNode; + private readonly NodeIndex index; + + public ReferenceAccessor(IGraphNode contentNode, NodeIndex index) + { + this.contentNode = contentNode; + this.index = index; + } + + public void Update(object newValue) + { + if (index == NodeIndex.Empty) + { + ((IMemberNode)contentNode).Update(newValue); + } + else + { + ((IObjectNode)contentNode).Update(newValue, index); + } + } + + public Task Clear(LoaderReferenceManager manager, AbsoluteId referencerId, AssetId contentId) + { + return manager.ClearContentReference(referencerId, contentId, contentNode, index); + } + } + + private readonly IDispatcherService gameDispatcher; + private readonly IEditorContentLoader loader; + private readonly Dictionary>> references = new(); + private readonly Dictionary contents = new(); + private readonly HashSet buildPending = new(); + + public LoaderReferenceManager(IDispatcherService gameDispatcher, IEditorContentLoader loader) + { + this.gameDispatcher = gameDispatcher; + this.loader = loader; + } + + public async Task RegisterReferencer(AbsoluteId referencerId) + { + gameDispatcher.EnsureAccess(); + using (await loader.LockDatabaseAsynchronously()) + { + if (references.ContainsKey(referencerId)) + throw new InvalidOperationException("The given referencer is already registered."); + + references.Add(referencerId, new Dictionary>()); + } + } + + public async Task RemoveReferencer(AbsoluteId referencerId) + { + gameDispatcher.EnsureAccess(); + using (await loader.LockDatabaseAsynchronously()) + { + if (!references.ContainsKey(referencerId)) + throw new InvalidOperationException("The given referencer is not registered."); + + var referencer = references[referencerId]; + // Properly clear all reference first + foreach (var content in referencer.ToDictionary(x => x.Key, x => x.Value)) + { + foreach (var reference in content.Value.ToList()) + { + // Ok to await in the loop, Clear should never yield because we already own the lock. + await reference.Clear(this, referencerId, content.Key); + } + } + references.Remove(referencerId); + } + } + + public async Task PushContentReference(AbsoluteId referencerId, AssetId contentId, IGraphNode contentNode, NodeIndex index) + { + gameDispatcher.EnsureAccess(); + using (await loader.LockDatabaseAsynchronously()) + { + if (!references.ContainsKey(referencerId)) + throw new InvalidOperationException("The given referencer is not registered."); + + var referencer = references[referencerId]; + List accessors; + if (!referencer.TryGetValue(contentId, out accessors)) + { + accessors = new List(); + referencer[contentId] = accessors; + } + var accessor = new ReferenceAccessor(contentNode, index); + if (accessors.Contains(accessor)) + { + // If the reference already exists, clear it and re-enter + await ClearContentReference(referencerId, contentId, contentNode, index); + await PushContentReference(referencerId, contentId, contentNode, index); + return; + } + + accessors.Add(accessor); + + object value; + if (contents.TryGetValue(contentId, out value)) + { + accessor.Update(value); + } + else + { + // Build only if not requested yet (otherwise we just need to wait for ReplaceContent() to be called, it will also replace this reference since it was added just before) + if (buildPending.Add(contentId)) + loader.BuildAndReloadAsset(contentId); + } + } + } + + public async Task ClearContentReference(AbsoluteId referencerId, AssetId contentId, IGraphNode contentNode, NodeIndex index) + { + gameDispatcher.EnsureAccess(); + using (await loader.LockDatabaseAsynchronously()) + { + if (!references.ContainsKey(referencerId)) + throw new InvalidOperationException("The given referencer is not registered."); + + var referencer = references[referencerId]; + if (!referencer.ContainsKey(contentId)) + throw new InvalidOperationException("The given content is not registered to the given referencer."); + + var accessors = referencer[contentId]; + var accessor = new ReferenceAccessor(contentNode, index); + var accesorIndex = accessors.IndexOf(accessor); + if (accesorIndex < 0) + throw new InvalidOperationException("The given reference is not registered for the given content and referencer."); + + accessors.RemoveAt(accesorIndex); + if (accessors.Count == 0) + { + referencer.Remove(contentId); + // Unload the content if nothing else is referencing it anymore + var unloadContent = references.Values.SelectMany(x => x.Keys).All(x => x != contentId); + if (unloadContent) + { + await loader.UnloadAsset(contentId); + contents.Remove(contentId); + } + } + } + } + + public async Task ReplaceContent(AssetId contentId, object newValue) + { + gameDispatcher.EnsureAccess(); + using (await loader.LockDatabaseAsynchronously()) + { + buildPending.Remove(contentId); + + // In case content was not properly loaded, just keep existing one + if (newValue != null) + { + foreach (var referencer in references.Values) + { + List accessors; + if (referencer.TryGetValue(contentId, out accessors)) + { + foreach (var accessor in accessors) + { + accessor.Update(newValue); + } + } + } + contents[contentId] = newValue; + } + } + } + + public async Task> ComputeReferencedAssets() + { + using ((await loader.ReserveDatabaseSyncLock()).Lock()) + { + return new HashSet(references.Values.SelectMany(x => x.Keys)); + } + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceBase.cs b/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceBase.cs new file mode 100644 index 0000000000..031855b8bb --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceBase.cs @@ -0,0 +1,82 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Engine; + +namespace Stride.Editor.EditorGame.Game; + +/// +/// Base class for the interface. +/// +public abstract class EditorGameServiceBase : IEditorGameService +{ + /// + public bool IsInitialized { get; private set; } + + /// + public virtual bool IsActive { get { return true; } set { throw new InvalidOperationException("This service cannot be deactivated."); } } + + /// + public virtual IEnumerable Dependencies => Enumerable.Empty(); + + public EditorGameServiceRegistry Services { get; } = new EditorGameServiceRegistry(); + + /// + /// Gets whether this service has been disposed. + /// + protected bool IsDisposed { get; private set; } + + /// + public virtual Task DisposeAsync() + { + IsDisposed = true; + return Task.CompletedTask; + } + + /// + public async Task InitializeService(EditorServiceGame game) + { + EnsureNotDestroyed(nameof(EditorGameServiceBase)); + + foreach (var dependency in Dependencies) + { + Services.Add(game.EditorServices.Get(dependency)); + } + IsInitialized = await Initialize(game); + return IsInitialized; + } + + /// + public virtual void RegisterScene(Scene scene) + { + EnsureNotDestroyed(nameof(EditorGameServiceBase)); + // Do nothing by default. + } + + /// + /// Checks whether this service has been disposed, and throws an if it is the case. + /// + /// The name to supply to the . + protected void EnsureNotDestroyed(string? name = null) + { + if (IsDisposed) + { + throw new ObjectDisposedException(name ?? nameof(EditorGameServiceBase), "This service has already been disposed."); + } + } + + /// + /// Initializes the service. This method is invoked by . + /// + /// + /// + protected abstract Task Initialize(EditorServiceGame game); + + /// + /// Called when the game graphics compositor is updated. + /// + /// + public virtual void UpdateGraphicsCompositor(EditorServiceGame game) + { + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceRegistry.cs b/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceRegistry.cs new file mode 100644 index 0000000000..f4162e04f4 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/Game/EditorGameServiceRegistry.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Reflection; + +namespace Stride.Editor.EditorGame.Game; + +public sealed class EditorGameServiceRegistry : Core.IAsyncDisposable +{ + public List Services { get; } = new List(); + + public T? Get() + { + return Services.OfType().FirstOrDefault(); + } + + public IEditorGameService? Get(Type serviceType) + { + if (serviceType == null) throw new ArgumentNullException(nameof(serviceType)); + if (!serviceType.HasInterface(typeof(IEditorGameService))) + throw new ArgumentException($@"The given type must be a type that implement {nameof(IEditorGameService)}", nameof(serviceType)); + + return Services.FirstOrDefault(serviceType.IsInstanceOfType); + } + + public void Add(T service) + where T : IEditorGameService + { + if (service == null) throw new ArgumentNullException(nameof(service)); + Services.Add(service); + } + + /// + public async Task DisposeAsync() + { + for (var index = Services.Count - 1; index >= 0; index--) + { + var service = Services[index]; + await service.DisposeAsync(); + } + Services.Clear(); + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs b/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs new file mode 100644 index 0000000000..ff813a9e48 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/Game/EditorServiceGame.cs @@ -0,0 +1,246 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.BuildEngine; +using Stride.Core.IO; +using Stride.Core.Mathematics; +using Stride.Editor.Build; +using Stride.Editor.Engine; +using Stride.Games; +using Stride.Games.Time; +using Stride.Graphics; +using Stride.Rendering.Compositing; + +namespace Stride.Editor.EditorGame.Game; + +/// +/// Represents the arguments of the event. +/// +public class ExceptionThrownEventArgs : EventArgs +{ + public ExceptionThrownEventArgs(Exception exception) + { + Exception = exception; + } + + /// + /// The exception that was thrown. + /// + public Exception Exception { get; } + + /// + /// Gets or sets a value that indicates the present state of the event handling. + /// + /// + /// If true the game will go to the faulted state; otherwise, the exception will be rethrown. + /// + public bool Handled { get; set; } +} + +public abstract class EditorServiceGame : EmbeddedGame +{ + public static readonly Color EditorBackgroundColorLdr = new(51, 51, 51, 255); + + public static readonly Color EditorBackgroundColorHdr = new(61, 61, 61, 255); + + private static readonly TimeSpan GameHiddenNextUpdateTime = TimeSpan.FromSeconds(1); + + private readonly TimerTick gameHiddenUpdateTimer = new(); + private TimeSpan gameHiddenUpdateTimeElapsed = TimeSpan.Zero; + private bool isEditorHidden = false; + private bool prevForceOneUpdatePerDraw; + private bool isFirstDrawCall = true; // We need to call BeginDraw at least once to ensure graphics context is generated + + public EditorGameServiceRegistry EditorServices { get; private set; } + + public IGameSettingsAccessor PackageSettings { get; set; } + + /// + /// True if the game is not visible in the editor which will stop rendering and + /// throttle game updates. + /// + /// + /// Used when game is not visible in the editor (eg. hidden inside a tab control). + /// We only throttle updates instead of completely suspending the game because a game exit command is done + /// within the ScriptSystem, so we must be able to run this system. + /// + public bool IsEditorHidden + { + get { return isEditorHidden; } + set + { + gameHiddenUpdateTimer.Reset(); + gameHiddenUpdateTimeElapsed = TimeSpan.Zero; + isEditorHidden = value; + if (isEditorHidden) + { + // In case the editor is set to IsFixedTimeStep, we need to ensure when the game + // updates while we're throttling it only updates once instead of trying to 'catch up' on update calls. + prevForceOneUpdatePerDraw = ForceOneUpdatePerDraw; + ForceOneUpdatePerDraw = true; + } + else + { + ForceOneUpdatePerDraw = prevForceOneUpdatePerDraw; // Restore the previous setting + } + } + } + + /// + /// True if game is faulted (not running). + /// + /// + /// Game won't resume until cleared. + /// + public bool Faulted { get; set; } + + public event EventHandler ExceptionThrown; + + /// + /// Calculates and returns the position of the mouse in the scene. + /// + /// The position of the mouse. + /// The position in the scene world space. + public abstract Vector3 GetPositionInScene(Vector2 mousePosition); + + public void RegisterServices(EditorGameServiceRegistry serviceRegistry) + { + EditorServices = serviceRegistry; + } + + public abstract void TriggerActiveRenderStageReevaluation(); + + public void UpdateColorSpace(ColorSpace colorSpace) + { + // Change the color space if necessary + if (GraphicsDeviceManager.PreferredColorSpace != colorSpace) + { + GraphicsDeviceManager.PreferredColorSpace = colorSpace; + GraphicsDeviceManager.ApplyChanges(); + } + } + + public virtual void UpdateGraphicsCompositor(GraphicsCompositor graphicsCompositor) + { + SceneSystem.GraphicsCompositor = graphicsCompositor; + SceneSystem.GraphicsCompositor.Game = new EditorTopLevelCompositor { Child = SceneSystem.GraphicsCompositor.Editor, PreviewGame = SceneSystem.GraphicsCompositor.Game }; + + foreach (var service in EditorServices.Services) + { + service.UpdateGraphicsCompositor(this); + } + } + + protected override void PrepareContext() + { + Services.RemoveService(); + Services.AddService(MicrothreadLocalDatabases.ProviderService); + + base.PrepareContext(); + } + + /// + protected override void Initialize() + { + // Database is needed by effect compiler cache + MicrothreadLocalDatabases.MountCommonDatabase(); + + base.Initialize(); + + // TODO: the physics system should not be registered by default here! + Physics.Simulation.DisableSimulation = true; + } + + /// + protected override void Update(GameTime gameTime) + { + // Keep going only if last exception has been "resolved" + if (Faulted) + return; + + if (IsEditorHidden) + { + gameHiddenUpdateTimer.Tick(); + gameHiddenUpdateTimeElapsed += gameHiddenUpdateTimer.ElapsedTime; + if (gameHiddenUpdateTimeElapsed < GameHiddenNextUpdateTime) + { + return; + } + else + { + gameHiddenUpdateTimeElapsed = TimeSpan.Zero; // It doesn't matter how much it exceeded the threshold, taking longer than one second is ok since it's hidden + } + } + + try + { + base.Update(gameTime); + } + catch (Exception ex) + { + if (!OnFault(ex)) + { + // Exception was no handled, rethrow + throw; + } + // Caught exception, turning game into faulted state + Faulted = true; + } + } + + protected override bool BeginDraw() + { + if (IsEditorHidden && !isFirstDrawCall) + { + // While it's hidden do not prepare the graphics context + return false; + } + isFirstDrawCall = false; + return base.BeginDraw(); + } + + /// + protected override void Draw(GameTime gameTime) + { + // Keep going only if last exception has been "resolved" + if (Faulted || IsEditorHidden) + return; + + try + { + base.Draw(gameTime); + } + catch (Exception ex) + { + if (!OnFault(ex)) + { + // Exception was no handled, rethrow + throw; + } + // Caught exception, turning game into faulted state + Faulted = true; + } + } + + /// + /// Called whenever an exception occured in the game. + /// + /// The exception. + /// + /// true if the exception was handled and the game should transitioned to the faulted state; otherwise, false and the exception will be rethrown. + /// + /// + /// The exception can be handled by listener to the event. + /// + protected virtual bool OnFault(Exception ex) + { + var handler = ExceptionThrown; + if (handler == null) + { + return false; + } + var args = new ExceptionThrownEventArgs(ex); + ExceptionThrown?.Invoke(this, args); + return args.Handled; + } +} diff --git a/sources/editor/Stride.Editor/EditorGame/Game/IEditorGameService.cs b/sources/editor/Stride.Editor/EditorGame/Game/IEditorGameService.cs new file mode 100644 index 0000000000..873a6bf225 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/Game/IEditorGameService.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Engine; + +namespace Stride.Editor.EditorGame.Game; + +/// +/// Base interface for services that handle specific features of a instantiated for an asset editor. +/// +public interface IEditorGameService : Core.IAsyncDisposable +{ + /// + /// Gets whether this service has been initialized. + /// + bool IsInitialized { get; } + + /// + /// Gets whether this service is currently active. + /// + bool IsActive { get; set; } + + /// + /// Gets the type of services that are required for this service to work. + /// + IEnumerable Dependencies { get; } + + /// + /// Initializes this service, allowing it to register scripts and modify the graphics compositor. + /// + /// The game for which this service is initialized. + /// This method is invoked after the game is fully initialized/ + Task InitializeService(EditorServiceGame game); + + /// + /// Registers the given scene to this service, as the scene containing the objects being edited. + /// + /// The scene to register. + void RegisterScene(Scene scene); + + /// + /// Called when the game graphics compositor is updated. + /// + /// + void UpdateGraphicsCompositor(EditorServiceGame game); +} diff --git a/sources/editor/Stride.Editor/EditorGame/ViewModels/IEditorGameViewModelService.cs b/sources/editor/Stride.Editor/EditorGame/ViewModels/IEditorGameViewModelService.cs new file mode 100644 index 0000000000..6a0c387eb8 --- /dev/null +++ b/sources/editor/Stride.Editor/EditorGame/ViewModels/IEditorGameViewModelService.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Editor.EditorGame.ViewModels; + +/// +/// Base interface for services accessible from a view model class, that allows to interact with a instance of a . +/// +public interface IEditorGameViewModelService +{ +} diff --git a/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs b/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs new file mode 100644 index 0000000000..ca53103582 --- /dev/null +++ b/sources/editor/Stride.Editor/Engine/EmbeddedGame.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Diagnostics; +using Stride.Engine; +using Stride.Graphics; + +namespace Stride.Editor.Engine; + +/// +/// Represents a Game that is embedded in a external window. +/// +public class EmbeddedGame : Game +{ + /// + /// All created embedded games (preview, scene, etc...) will have set. + /// + public static bool DebugMode { get; set; } + + public EmbeddedGame() + { + GraphicsDeviceManager.PreferredGraphicsProfile = new [] { GraphicsProfile.Level_11_0, GraphicsProfile.Level_10_1, GraphicsProfile.Level_10_0 }; + GraphicsDeviceManager.PreferredBackBufferWidth = 64; + GraphicsDeviceManager.PreferredBackBufferHeight = 64; + GraphicsDeviceManager.PreferredDepthStencilFormat = PixelFormat.D24_UNorm_S8_UInt; + GraphicsDeviceManager.DeviceCreationFlags = DebugMode ? DeviceCreationFlags.Debug : DeviceCreationFlags.None; + + AutoLoadDefaultSettings = false; + } + + /// + protected override void Initialize() + { + base.Initialize(); + + Window.IsBorderLess = true; + Window.IsMouseVisible = true; + } + + /// + protected sealed override LogListener GetLogListener() + { + // We don't want the embedded games to log in the console + return null; + } +} diff --git a/sources/editor/Stride.Editor/Extensions/EditorGameExtensions.cs b/sources/editor/Stride.Editor/Extensions/EditorGameExtensions.cs new file mode 100644 index 0000000000..493489c17a --- /dev/null +++ b/sources/editor/Stride.Editor/Extensions/EditorGameExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Editor.EditorGame.Game; + +namespace Stride.Editor.Extensions; + +public static class EditorGameExtensions +{ + /// + /// Sorts the services of this by dependency order. + /// + /// The service registry. + /// + public static IEnumerable OrderByDependency(this EditorGameServiceRegistry serviceRegistry) + { + var visited = new HashSet(); + return serviceRegistry.Services.SelectMany(s => OrderByDependency(s, visited, serviceRegistry)); + } + + /// + /// Recursively sorts the dependencies of the provided in deepest order first. + /// + /// + /// + /// + /// + private static IEnumerable OrderByDependency(IEditorGameService service, ISet visited, EditorGameServiceRegistry serviceRegistry) + { + if (!visited.Add(service)) + yield break; + + foreach (var dependencyType in service.Dependencies) + { + var dependency = serviceRegistry.Get(dependencyType); + if (dependency == null) + throw new InvalidOperationException($"The service [{service.GetType().Name}] requires a service of type [{dependencyType.Name}]."); + + foreach (var item in OrderByDependency(dependency, visited, serviceRegistry)) + yield return item; + } + yield return service; + } +} diff --git a/sources/editor/Stride.Editor/Module.cs b/sources/editor/Stride.Editor/Module.cs new file mode 100644 index 0000000000..6b77013019 --- /dev/null +++ b/sources/editor/Stride.Editor/Module.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core; +using Stride.Core.Reflection; + +namespace Stride.Editor; + +internal class Module +{ + [ModuleInitializer] + public static void Initialize() + { + // Currently, we are adding this assembly as part of the "assets" in order for the thumbnail types to be accessible through the AssemblyRegistry + AssemblyRegistry.Register(typeof(Module).Assembly, AssemblyCommonCategories.Assets); + } +} diff --git a/sources/editor/Stride.Editor/Preview/AnimationAssetEditorGameCompiler.cs b/sources/editor/Stride.Editor/Preview/AnimationAssetEditorGameCompiler.cs new file mode 100644 index 0000000000..f0468c21b7 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/AnimationAssetEditorGameCompiler.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Animations; +using Stride.Assets.Models; + +namespace Stride.Editor.Preview; + +[AssetCompiler(typeof(AnimationAsset), typeof(EditorGameCompilationContext))] +public class AnimationAssetEditorGameCompiler : AssetCompilerBase +{ + protected override void Prepare(AssetCompilerContext context, AssetItem assetItem, string targetUrlInStorage, AssetCompilerResult result) + { + result.BuildSteps.Add(new DummyAssetCommand(assetItem)); + } +} diff --git a/sources/editor/Stride.Editor/Preview/AnimationAssetPreviewCompiler.cs b/sources/editor/Stride.Editor/Preview/AnimationAssetPreviewCompiler.cs new file mode 100644 index 0000000000..607e012488 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/AnimationAssetPreviewCompiler.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Assets.Materials; +using Stride.Assets.Models; + +namespace Stride.Editor.Preview; + +[AssetCompiler(typeof(AnimationAsset), typeof(PreviewCompilationContext))] +public class AnimationAssetPreviewCompiler : AnimationAssetCompiler +{ + public override IEnumerable GetInputTypes(AssetItem assetItem) + { + foreach (var inputFile in base.GetInputTypes(assetItem)) + { + yield return inputFile; + } + + yield return new BuildDependencyInfo(typeof(ModelAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime | BuildDependencyType.CompileContent); + yield return new BuildDependencyInfo(typeof(MaterialAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime | BuildDependencyType.CompileContent); + } +} diff --git a/sources/editor/Stride.Editor/Preview/AssetPreview.cs b/sources/editor/Stride.Editor/Preview/AssetPreview.cs new file mode 100644 index 0000000000..58a52f07c9 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/AssetPreview.cs @@ -0,0 +1,252 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core.Assets; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.ViewModels; +using Stride.Editor.Preview.Views; +using Stride.Editor.Preview.ViewModels; +using Stride.Engine; +using Stride.Graphics; +using Stride.Rendering.Compositing; + +namespace Stride.Editor.Preview; + +/// +/// A base implementation for the . +/// +public abstract class AssetPreview : IAssetPreview +{ + private TaskCompletionSource initCompletionSource = new(); + + protected IPreviewBuilder Builder; + protected PreviewGame Game; + protected AssetItem AssetItem; + + private IPreviewView? previewView; + + private bool needSceneUpdate; + + /// + /// Gets the value indicating if the preview is still running. + /// + protected bool IsRunning { get; private set; } + + /// + public IAssetPreviewViewModel? PreviewViewModel { get; protected set; } + + /// + public AssetViewModel AssetViewModel { get; private set; } + + public RenderingMode RenderingMode + { + get; protected set; + } + + public static Type? DefaultViewType { get; set; } + + /// + /// Initializes the preview of an asset. This method will invoke the protected virtual method . + /// + /// The view model of the asset to preview. + /// The preview builder that is initializing this preview. + /// A task returning an object that is the view associated to the preview. + public async Task Initialize(AssetViewModel asset, IPreviewBuilder builder) + { + IsRunning = true; + AssetViewModel = asset; + AssetItem = asset.AssetItem; + Builder = builder; + Game = builder.PreviewGame; + + // Copy ColorSpace to Game + // TODO: Move this code this method and find a better pluggable way to do this. + + var gameSettings = AssetItem.Package.GetGameSettingsAssetOrDefault(); + Game.GraphicsDeviceManager.PreferredColorSpace = DetermineColorSpace(); + Game.GraphicsDeviceManager.ApplyChanges(); + + RenderingMode = gameSettings.GetOrCreate().RenderingMode; + + await Initialize(); + PreviewViewModel = await ProvideViewModel(asset.ServiceProvider); + previewView = await ProvideView(asset.ServiceProvider); + FinalizeInitialization(); + return previewView; + } + + /// + public async Task Update() + { + await IsInitialized(); + await PrepareContentInternal(); + await Builder.Dispatcher.InvokeAsync(() => previewView.UpdateView(this)); + } + + /// + /// Determine the color space to be used by the when generating the preview. + /// + /// The color space to use. + protected virtual ColorSpace DetermineColorSpace() + { + var gameSettings = AssetItem.Package.GetGameSettingsAssetOrDefault(); + var renderingSettings = gameSettings.GetOrCreate(); + return renderingSettings.ColorSpace; + } + + /// + /// Update the scene. Note: this function is called from the PreviewGame thread! + /// + protected RenderingMode UpdateScene() + { + if (needSceneUpdate) + { + UnloadContent(); + LoadContentSafe(); + Game.TriggerActiveRenderStageReevaluation(); + needSceneUpdate = false; + } + + return RenderingMode; + } + + /// + public async Task IsInitialized() + { + await initCompletionSource.Task; + } + + /// + public virtual async Task Dispose() + { + IsRunning = false; + initCompletionSource = new TaskCompletionSource(); + + // ReSharper disable once DelegateSubtraction + Game.UpdateSceneCallback -= UpdateScene; + + await Game.UnloadPreviewScene(Builder.Logger); + + UnloadContent(); + } + + /// + /// Get or create the preview scene to load into the preview game. + /// + /// The preview scene + protected virtual Scene? CreatePreviewScene() + { + return null; + } + + protected virtual GraphicsCompositor? GetGraphicsCompositor() + { + return null; + } + + public virtual void OnViewAttached() + { + } + + /// + /// Initializes the preview of the asset in classes that inherits from . + /// + /// + protected virtual async Task Initialize() + { + await Game.LoadPreviewScene(CreatePreviewScene(), GetGraphicsCompositor(), Builder.Logger); + Game.UpdateSceneCallback += UpdateScene; + + await PrepareContentInternal(); + } + + private async Task PrepareContentInternal() + { + if (await PrepareContent()) + needSceneUpdate = true; + } + + /// + /// Prepare the content used by the preview. + /// + /// True if content could be prepared correctly, False otherwise + protected virtual Task PrepareContent() + { + needSceneUpdate = true; + + return Task.FromResult(true); + } + + private void LoadContentSafe() + { + try + { + LoadContent(); + } + catch (Exception e) + { + // TODO: In PreviewFromEntity, the preview scene is created during load, leaving the scene in an invalid state when throwing. + // This can lead to crashes when removing the preview entity (e.g. during Script removal) + Builder.Logger.Error($"Failed to load the content for the preview of asset '{AssetItem.Location}'.", e); + } + } + + /// + /// Load the content of the preview + /// + protected virtual void LoadContent() + { + } + + /// + /// Unload the content of the preview. + /// + protected virtual void UnloadContent() + { + + } + + private void FinalizeInitialization() + { + initCompletionSource.SetResult(0); + } + + private async Task ProvideView(IViewModelServiceProvider serviceProvider) + { + var pluginService = serviceProvider.Get(); + var viewType = pluginService.GetPreviewViewType(GetType()) ?? DefaultViewType; + + return viewType != null + ? await Builder.Dispatcher.InvokeAsync(() => + { + var view = (IPreviewView?)Activator.CreateInstance(viewType); + view?.InitializeView(Builder, this); + return view; + }) + : null; + } + + private async Task ProvideViewModel(IViewModelServiceProvider serviceProvider) + { + var pluginService = serviceProvider.Get(); + var previewViewModelType = pluginService.GetPreviewViewModelType(GetType()); + + return previewViewModelType != null + ? await AssetViewModel.Dispatcher.InvokeAsync(() => + { + return (IAssetPreviewViewModel?)Activator.CreateInstance(previewViewModelType, AssetViewModel.Session); + }) + : null; + } +} + +/// +/// A specialization of the class that specifies the related asset type as a generic argument. +/// +/// The type of asset this class manages to preview. +public abstract class AssetPreview : AssetPreview where T : Asset +{ + protected T Asset => (T)AssetItem.Asset; +} diff --git a/sources/editor/Stride.Editor/Preview/AssetPreviewFactory.cs b/sources/editor/Stride.Editor/Preview/AssetPreviewFactory.cs new file mode 100644 index 0000000000..ad730cafd0 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/AssetPreviewFactory.cs @@ -0,0 +1,8 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; + +namespace Stride.Editor.Preview; + +public delegate IAssetPreview AssetPreviewFactory(IPreviewBuilder builder, PreviewGame game, AssetItem asset); diff --git a/sources/editor/Stride.Editor/Preview/BuildAssetPreview.cs b/sources/editor/Stride.Editor/Preview/BuildAssetPreview.cs new file mode 100644 index 0000000000..3b6b9cc7da --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/BuildAssetPreview.cs @@ -0,0 +1,125 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.BuildEngine; +using Stride.Core.Extensions; +using Stride.Core.MicroThreading; +using Stride.Core.Serialization.Contents; +using Stride.Editor.Build; + +namespace Stride.Editor.Preview; + +/// +/// An implementation of the class that provide utilities to build an asset. +/// This class can be inherited to make preview class for assets that requires asset builds. +/// +/// The type of asset this preview can display. +public abstract class BuildAssetPreview : AssetPreview + where T : Asset +{ + private readonly Guid previewContextId = Guid.NewGuid(); + + /// + /// The output objects generated by the build of the associated asset, when the build succeeded. + /// + protected IReadOnlyDictionary? OutputObjects; + + /// + /// Compiles the assets needed for this preview. + /// + /// The default implementation compile the related assets and its dependencies. + /// An instance of the class containing the generated build steps. + protected virtual AssetCompilerResult Compile() + { + return Builder.Compile(AssetItem); + } + + /// + /// Load an asset to the preview. + /// + /// The type of the asset to load + /// The path to the asset to load + /// The settings. If null, fallback to . + /// The loaded asset + public TAssetType? LoadAsset(string url, ContentManagerLoaderSettings? settings = null) + where TAssetType : class + { + TAssetType? result = null; + + try + { + // This method can be invoked both from a script and from a regular task. In the second case, it will use the out-of-microthread database which need to be locked. + // TODO: Ensure this method is always called from the preview game (it is not at least when a property is modified, currently), so we don't need to lock. Note: should be the case now, assume it is after GDC! + if (Scheduler.CurrentMicroThread == null) + Monitor.Enter(AssetBuilderService.OutOfMicrothreadDatabaseLock); + + MicrothreadLocalDatabases.MountDatabase(OutputObjects.Yield()); + + try + { + result = Game.Content.Load(url, settings); + } + finally + { + if (Scheduler.CurrentMicroThread == null) + { + MicrothreadLocalDatabases.UnmountDatabase(); + } + } + } + catch (Exception e) + { + Builder.Logger.Error($"An exception was triggered when trying to load the entity [{url}] for the preview of asset item [{AssetItem.Location}].", e); + } + finally + { + if (Scheduler.CurrentMicroThread == null) + Monitor.Exit(AssetBuilderService.OutOfMicrothreadDatabaseLock); + } + return result; + } + + /// + /// Unload an asset from the preview. + /// + /// The asset to unload + public void UnloadAsset(object? asset) + { + if (asset != null) + Game.Content.Unload(asset); + } + + protected override Task PrepareContent() + { + return BuildAsset(); + } + + protected async Task BuildAsset() + { + // get the build step required by the preview builder + AssetCompilerResult? compilationResult = null; + + var buildUnit = new AnonymousAssetBuildUnit(new AssetBuildUnitIdentifier(previewContextId, Asset.Id), () => { compilationResult = Compile(); return compilationResult.BuildSteps; }) + { + PriorityMajor = DefaultAssetBuilderPriorities.PreviewPriority + }; + Builder.AssetBuilderService.PushBuildUnit(buildUnit); + + await buildUnit.Wait(); + + UpdateBuildAssetResults(buildUnit, compilationResult!); + return buildUnit.Succeeded; + } + + protected virtual void UpdateBuildAssetResults(AnonymousAssetBuildUnit buildUnit, AssetCompilerResult compilationResult) + { + if (buildUnit.Succeeded) + { + // If successful, store the output objects so they can be mounted on a database when needed. + OutputObjects = compilationResult.BuildSteps.OutputObjects; + } + } +} diff --git a/sources/editor/Stride.Editor/Preview/IAssetPreview.cs b/sources/editor/Stride.Editor/Preview/IAssetPreview.cs index 24d98ae5a6..3d04d0e6ac 100644 --- a/sources/editor/Stride.Editor/Preview/IAssetPreview.cs +++ b/sources/editor/Stride.Editor/Preview/IAssetPreview.cs @@ -1,6 +1,9 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Assets; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Editor.Preview.ViewModels; namespace Stride.Editor.Preview; @@ -9,4 +12,50 @@ namespace Stride.Editor.Preview; /// public interface IAssetPreview { + /// + /// Gets the preview view model of the asset previewed. This property can be null. + /// + IAssetPreviewViewModel PreviewViewModel { get; } + + /// + /// Gets the view model of the asset previewed. + /// + AssetViewModel AssetViewModel { get; } + + /// + /// Gets the rendering mode for this preview; + /// + /// The rendering mode. + RenderingMode RenderingMode { get; } + + /// + /// Initializes the preview of an asset. + /// + /// The view model of the asset to preview. + /// The preview builder that is initializing this preview. + /// A task returning an object that is the view associated to the preview. + Task Initialize(AssetViewModel asset, IPreviewBuilder builder); + + /// + /// Waits for the preview to be initialized. + /// + /// A task that will complete when the preview is initialized. + Task IsInitialized(); + + /// + /// Updates the preview of an asset after a change in its property. + /// + /// A task that will complete when the update is done. + Task Update(); + + /// + /// Dispose the preview of the current asset. + /// + /// A task that will complete when the preview is disposed. + Task Dispose(); + + /// + /// Function called when the view corresponding to the preview has been inserted into the element hierarchy. + /// + void OnViewAttached(); } diff --git a/sources/editor/Stride.Editor/Preview/IPreviewBuilder.cs b/sources/editor/Stride.Editor/Preview/IPreviewBuilder.cs new file mode 100644 index 0000000000..cc9284e13c --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/IPreviewBuilder.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Services; +using Stride.Editor.Build; + +namespace Stride.Editor.Preview; + +/// +/// An interface that represents an object which is capable of building previews for assets. +/// +public interface IPreviewBuilder +{ + /// + /// Gets the asset builder service used to build asset. + /// + GameStudioBuilderService AssetBuilderService { get; } + + /// + /// Gets the to use to update UI. + /// + IDispatcherService Dispatcher { get; } + + /// + /// Gets the to use for preview logs. + /// + Logger Logger { get; } + + /// + /// Gets the instance of to use for preview. + /// + PreviewGame PreviewGame { get; } + + /// + /// Compiles the given asset (and its dependencies). + /// + /// The asset to compile. + /// An containing the generated build steps. + AssetCompilerResult Compile(AssetItem asset); + + /// + /// Gets the UI that hosts the stride viewport. + /// + /// + object GetStrideView(); +} diff --git a/sources/editor/Stride.Editor/Preview/PrefabAssetPreviewCompiler.cs b/sources/editor/Stride.Editor/Preview/PrefabAssetPreviewCompiler.cs new file mode 100644 index 0000000000..a9fd2d93d7 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/PrefabAssetPreviewCompiler.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Analysis; +using Stride.Core.Assets.Compiler; +using Stride.Animations; +using Stride.Assets.Entities; +using Stride.Assets.Materials; +using Stride.Assets.Navigation; +using Stride.Assets.Sprite; +using Stride.Assets.Textures; +using Stride.Rendering; + +namespace Stride.Editor.Preview; + +//do not compile sounds in the editor game + +//do not compile animations in the editor game + +[AssetCompiler(typeof(PrefabAsset), typeof(PreviewCompilationContext))] +public class PrefabAssetPreviewCompiler : PrefabAssetCompiler +{ + public override IEnumerable GetInputTypes(AssetItem assetItem) + { + foreach (var type in AssetRegistry.GetAssetTypes(typeof(Model))) + { + yield return new BuildDependencyInfo(type, typeof(AssetCompilationContext), BuildDependencyType.Runtime); + } + foreach (var type in AssetRegistry.GetAssetTypes(typeof(AnimationClip))) + { + yield return new BuildDependencyInfo(type, typeof(AssetCompilationContext), BuildDependencyType.Runtime); + } + yield return new BuildDependencyInfo(typeof(TextureAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime); + yield return new BuildDependencyInfo(typeof(PrefabAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime); + yield return new BuildDependencyInfo(typeof(MaterialAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime); + yield return new BuildDependencyInfo(typeof(SpriteSheetAsset), typeof(AssetCompilationContext), BuildDependencyType.Runtime); + } + + public override IEnumerable GetInputTypesToExclude(AssetItem assetItem) + { + yield return typeof(SceneAsset); + yield return typeof(NavigationMeshAsset); + } +} diff --git a/sources/editor/Stride.Editor/Preview/PreviewEntity.cs b/sources/editor/Stride.Editor/Preview/PreviewEntity.cs new file mode 100644 index 0000000000..1d28a71e72 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/PreviewEntity.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Engine; + +namespace Stride.Editor.Preview; + +public class PreviewEntity +{ + /// + /// The entity to preview. + /// + public Entity Entity; + + /// + /// The actions to undertake when the preview entity is not used anymore. + /// + public Action Disposed; + + public PreviewEntity(Entity entity) + { + Entity = entity; + } +} diff --git a/sources/editor/Stride.Editor/Preview/PreviewGame.cs b/sources/editor/Stride.Editor/Preview/PreviewGame.cs new file mode 100644 index 0000000000..dc9953ac7e --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/PreviewGame.cs @@ -0,0 +1,215 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Assets.SpriteFont; +using Stride.Assets.SpriteFont.Compiler; +using Stride.Core; +using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; +using Stride.Core.Mathematics; +using Stride.Editor.EditorGame.Game; +using Stride.Engine; +using Stride.Engine.Design; +using Stride.Graphics; +using Stride.Rendering.Compositing; +using Stride.Shaders.Compiler; + +namespace Stride.Editor.Preview; + +/// +/// A instance specialized to render previews and create thumbnails +/// +public class PreviewGame : EditorServiceGame +{ + private readonly IEffectCompiler effectCompiler; + + /// + /// The pending preview request to process. + /// + private PreviewRequest previewRequest; + + private readonly object requestLock = new(); + + /// + /// A default font that can be used when rendering previews or thumbnails + /// + public SpriteFont DefaultFont; + + /// + /// A callback that can be used to update the scene. + /// + public Func? UpdateSceneCallback; + + private Scene previewScene; + + public PreviewGame(IEffectCompiler effectCompiler) + { + this.effectCompiler = effectCompiler; + } + + /// + public override Vector3 GetPositionInScene(Vector2 mousePosition) + { + throw new NotSupportedException(); + } + + /// + public override void TriggerActiveRenderStageReevaluation() + { + var visgroups = SceneSystem.SceneInstance.VisibilityGroups; + if (visgroups != null) + { + foreach (var sceneInstanceVisibilityGroup in visgroups) + { + sceneInstanceVisibilityGroup.NeedActiveRenderStageReevaluation = true; + } + } + } + + /// + protected override void Initialize() + { + base.Initialize(); + + // Use a shared database for our shader system + // TODO: Shaders compiled on main thread won't actually be visible to MicroThread build engine (contentIndexMap are separate). + // It will still work and cache because EffectCompilerCache caches not only at the index map level, but also at the database level. + // Later, we probably want to have a GetSharedDatabase() allowing us to mutate it (or merging our results back with IndexFileCommand.AddToSharedGroup()), + // so that database created with MountDatabase also have all the newest shaders. + ((IReferencable)effectCompiler).AddReference(); + EffectSystem.Compiler = effectCompiler; + } + + /// + protected override async Task LoadContent() + { + await base.LoadContent(); + + // create the default fonts + var fontItem = OfflineRasterizedSpriteFontFactory.Create(); + fontItem.FontType.Size = 22; + DefaultFont = OfflineRasterizedFontCompiler.Compile(Font, fontItem, GraphicsDevice.ColorSpace == ColorSpace.Linear); + + previewScene = new Scene(); + + // create and set the main scene instance + SceneSystem.SceneInstance = new SceneInstance(Services, previewScene, ExecutionMode.Preview); + + // add thumbnail builder and preview script to scheduler + Script.AddTask(ProcessPreviewRequestsTask); + } + + /// + protected override bool OnFault(Exception ex) + { + base.OnFault(ex); + // Always handle the exception + return true; + } + + private async Task ProcessPreviewRequestsTask() + { + while (IsRunning) + { + await Script.NextFrame(); + + PreviewRequest request; + lock (requestLock) + { + request = previewRequest; + previewRequest = null; + } + + if (request != null) + { + try + { + MicrothreadLocalDatabases.MountCommonDatabase(); + + Faulted = false; + + previewScene.Children.Clear(); + + if (SceneSystem.GraphicsCompositor != request.GraphicsCompositor) + { + SceneSystem.GraphicsCompositor?.Dispose(); + SceneSystem.GraphicsCompositor = request.GraphicsCompositor; + } + + if (request.Scene != null) + previewScene.Children.Add(request.Scene); + + request.RequestCompletion.SetResult(ResultStatus.Successful); + } + catch (Exception e) + { + // end the thumbnail build task + request.Logger.Error("An exception occurred while loading the preview scene.", e); + request.RequestCompletion.SetResult(ResultStatus.Failed); + } + } + + if (previewScene.Children.Count != 0) + { + var handler = UpdateSceneCallback; + if (handler != null) + { + var renderingMode = handler(); + } + } + } + } + + /// + /// Load a preview scene into the preview game. + /// + /// The scene to load as preview + /// The logger to use in case of errors. + /// The result of the scene load + public async Task LoadPreviewScene(Scene previewScene, GraphicsCompositor graphicsCompositor, ILogger logger) + { + lock (requestLock) + { + previewRequest = new PreviewRequest(previewScene, graphicsCompositor, logger); + } + + return await previewRequest.RequestCompletion.Task; + } + + /// + /// Unload the current preview scene. + /// + /// The logger to use in case of errors. + public async Task UnloadPreviewScene(ILogger logger) + { + await LoadPreviewScene(null, null, null); + } + + private class PreviewRequest + { + /// + /// The signal triggered when the preview request is completed. + /// + public readonly TaskCompletionSource RequestCompletion = new(); + + /// + /// The log to use in case of error + /// + public readonly ILogger Logger; + + /// + /// The preview scene to display + /// + public readonly Scene Scene; + + public readonly GraphicsCompositor GraphicsCompositor; + + public PreviewRequest(Scene scene, GraphicsCompositor graphicsCompositor, ILogger logger) + { + Logger = logger; + Scene = scene; + GraphicsCompositor = graphicsCompositor; + } + } +} diff --git a/sources/editor/Stride.Editor/Preview/SoundAssetEditorGameCompiler.cs b/sources/editor/Stride.Editor/Preview/SoundAssetEditorGameCompiler.cs new file mode 100644 index 0000000000..39b10082a7 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/SoundAssetEditorGameCompiler.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Assets.Media; +using Stride.Audio; + +namespace Stride.Editor.Preview; + +[AssetCompiler(typeof(SoundAsset), typeof(EditorGameCompilationContext))] +public class SoundAssetEditorGameCompiler : AssetCompilerBase +{ + protected override void Prepare(AssetCompilerContext context, AssetItem assetItem, string targetUrlInStorage, AssetCompilerResult result) + { + result.BuildSteps.Add(new DummyAssetCommand(assetItem)); + } +} diff --git a/sources/editor/Stride.Editor/Preview/VideoAssetEditorGameCompiler.cs b/sources/editor/Stride.Editor/Preview/VideoAssetEditorGameCompiler.cs new file mode 100644 index 0000000000..385d8bdb92 --- /dev/null +++ b/sources/editor/Stride.Editor/Preview/VideoAssetEditorGameCompiler.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Assets.Media; + +namespace Stride.Editor.Preview; + +[AssetCompiler(typeof(VideoAsset), typeof(EditorGameCompilationContext))] +public class VideoAssetEditorGameCompiler : AssetCompilerBase +{ + protected override void Prepare(AssetCompilerContext context, AssetItem assetItem, string targetUrlInStorage, AssetCompilerResult result) + { + result.BuildSteps.Add(new DummyAssetCommand(assetItem)); + } +} diff --git a/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs b/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs index 78230b1b2b..771413ea78 100644 --- a/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs +++ b/sources/editor/Stride.Editor/Preview/Views/IPreviewView.cs @@ -8,4 +8,16 @@ namespace Stride.Editor.Preview.Views; /// public interface IPreviewView { + /// + /// Initializes the view with the given parameters. + /// + /// The preview builder used to build the preview. + /// The asset preview to display in the view. + void InitializeView(IPreviewBuilder previewBuilder, IAssetPreview assetPreview); + + /// + /// Updates the view, usually after regenerating a new instance. + /// + /// The asset preview to display in the view. + void UpdateView(IAssetPreview assetPreview); } diff --git a/sources/editor/Stride.Editor/Stride.Editor.csproj b/sources/editor/Stride.Editor/Stride.Editor.csproj index 336eaa8697..d0cb71c6ff 100644 --- a/sources/editor/Stride.Editor/Stride.Editor.csproj +++ b/sources/editor/Stride.Editor/Stride.Editor.csproj @@ -8,6 +8,11 @@ enable + + true + --auto-module-initializer --serialization + + Properties\SharedAssemblyInfo.cs diff --git a/sources/engine/Stride.Graphics/Properties/AssemblyInfo.cs b/sources/engine/Stride.Graphics/Properties/AssemblyInfo.cs index f4881c453d..c68c531096 100644 --- a/sources/engine/Stride.Graphics/Properties/AssemblyInfo.cs +++ b/sources/engine/Stride.Graphics/Properties/AssemblyInfo.cs @@ -22,5 +22,6 @@ [assembly: InternalsVisibleTo("Stride.Video" + Stride.PublicKeys.Default)] #if !STRIDE_SIGNED +[assembly: InternalsVisibleTo("Stride.Assets.Editor")] // FIXME xplat-editor [assembly: InternalsVisibleTo("Stride.Assets.Presentation.Wpf")] #endif diff --git a/sources/engine/Stride.Rendering/Properties/AssemblyInfo.cs b/sources/engine/Stride.Rendering/Properties/AssemblyInfo.cs index 0a3b92dedc..08d9522205 100644 --- a/sources/engine/Stride.Rendering/Properties/AssemblyInfo.cs +++ b/sources/engine/Stride.Rendering/Properties/AssemblyInfo.cs @@ -3,5 +3,6 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Stride.Engine" + Stride.PublicKeys.Default)] +[assembly: InternalsVisibleTo("Stride.Editor" + Stride.PublicKeys.Default)] [assembly: InternalsVisibleTo("Stride.Editor.Wpf" + Stride.PublicKeys.Default)] [assembly: InternalsVisibleTo("Stride.Assets.Presentation.Wpf" + Stride.PublicKeys.Default)] diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Module.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Module.cs index c473e9e1cb..c3ad130fc5 100644 --- a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Module.cs +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Module.cs @@ -2,6 +2,8 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using Stride.Core.Assets.Presentation; +using Stride.Editor.Avalonia.Preview.Views; +using Stride.Editor.Preview; namespace Stride.Assets.Editor.Avalonia; @@ -11,5 +13,6 @@ internal class Module public static void Initialize() { AssetsPlugin.RegisterPlugin(typeof(StrideEditorViewPlugin)); + AssetPreview.DefaultViewType = typeof(StridePreviewView); } } diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/AnimationPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/AnimationPreviewView.cs new file mode 100644 index 0000000000..7afbe3024f --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/AnimationPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class AnimationPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/EntityPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/EntityPreviewView.cs new file mode 100644 index 0000000000..6fb90f2b26 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/EntityPreviewView.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +// DO NOT REACTIVATE THIS PREVIEW WITHOUT MAKING A DISTINCT PREVIEW BETWEEN ENTITIES AND SCENE! SCENE IS LOADED (AND NOW UNLOADED) at initialization, we absolutely don't want to do that +//[AssetPreviewView] +public class EntityPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/HeightmapPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/HeightmapPreviewView.cs new file mode 100644 index 0000000000..b957fb32bc --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/HeightmapPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class HeightmapPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/MaterialPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/MaterialPreviewView.cs new file mode 100644 index 0000000000..44b9dfc927 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/MaterialPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class MaterialPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ModelPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ModelPreviewView.cs new file mode 100644 index 0000000000..c5d459033f --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ModelPreviewView.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +[AssetPreviewView] +[AssetPreviewView] +[AssetPreviewView] +[AssetPreviewView] +public class ModelPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ScenePreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ScenePreviewView.cs new file mode 100644 index 0000000000..3a5f616416 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/ScenePreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class ScenePreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SkyboxPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SkyboxPreviewView.cs new file mode 100644 index 0000000000..5ff8b07bad --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SkyboxPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class SkyboxPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SoundPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SoundPreviewView.cs new file mode 100644 index 0000000000..d38473f990 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SoundPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class SoundPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteFontPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteFontPreviewView.cs new file mode 100644 index 0000000000..37db2c95e2 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteFontPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class SpriteFontPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteSheetPreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteSheetPreviewView.cs new file mode 100644 index 0000000000..db629bc178 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/SpriteSheetPreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class SpriteSheetPreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/TexturePreviewView.cs b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/TexturePreviewView.cs new file mode 100644 index 0000000000..e2f3f8ea34 --- /dev/null +++ b/sources/xplat-editor/Stride.Assets.Editor.Avalonia/Views/Preview/TexturePreviewView.cs @@ -0,0 +1,13 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets.Editor.Preview; +using Stride.Editor.Annotations; +using Stride.Editor.Avalonia.Preview.Views; + +namespace Stride.Assets.Editor.Avalonia.Views.Preview; + +[AssetPreviewView] +public class TexturePreviewView : StridePreviewView +{ +} diff --git a/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/GameEngineHost.cs b/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/GameEngineHost.cs new file mode 100644 index 0000000000..c961cbb3e7 --- /dev/null +++ b/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/GameEngineHost.cs @@ -0,0 +1,371 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.VisualTree; +using Stride.Core.Mathematics; +using Point = Avalonia.Point; + +namespace Stride.Core.Presentation.Avalonia.Controls; + +/// +/// A that can host a game engine window. This control is faster than but might behave +/// a bit less nicely on certain cases (such as resize, etc.). +/// +public class GameEngineHost : Control, IDisposable /*, IWin32Window, IKeyboardInputSink*/ +{ + // FIXME xplat-editor + //private readonly List contextMenuSources = new List(); + private bool updateRequested; + private int mouseMoveCount; + private Point contextMenuPosition; + private Vector dpiScale; + private Int4 lastBoundingBox; + private bool attached; + private bool isDisposed; + + static GameEngineHost() + { + FocusableProperty.OverrideMetadata(typeof(GameEngineHost), new StyledPropertyMetadata(true)); + IsVisibleProperty.Changed.AddClassHandler(OnIsVisibleChanged); + } + + /// + /// Initializes a new instance of the class. + /// + /// The hwnd of the child (hosted) window. + public GameEngineHost(IntPtr childHandle) + { + Handle = childHandle; + MinWidth = 32; + MinHeight = 32; + Loaded += OnLoaded; + Unloaded += OnUnloaded; + LayoutUpdated += OnLayoutUpdated; + } + + public IntPtr Handle { get; } + + // FIXME xplat-editor + //IKeyboardInputSite IKeyboardInputSink.KeyboardInputSite { get; set; } + + public void Dispose() + { + if (isDisposed) + return; + + Loaded -= OnLoaded; + Unloaded -= OnUnloaded; + LayoutUpdated -= OnLayoutUpdated; + // TODO: This seems to be blocking when exiting the Game Studio, but doesn't seem to be necessary + //NativeHelper.SetParent(Handle, IntPtr.Zero); + // FIXME xplat-editor + //NativeHelper.DestroyWindow(Handle); + isDisposed = true; + } + + // FIXME xplat-editor + ///// + //protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) + //{ + // dpiScale = newDpi; + // UpdateWindowPosition(); + //} + + private void OnLoaded(object? sender, RoutedEventArgs routedEventArgs) + { + Attach(); + UpdateWindowPosition(); + } + + private void OnUnloaded(object? sender, RoutedEventArgs e) + { + Detach(); + } + + private void OnLayoutUpdated(object? sender, EventArgs e) + { + // Remark: this callback is invoked a lot. It is critical to do minimum work if no update is needed. + UpdateWindowPosition(); + } + + private static void OnIsVisibleChanged(GameEngineHost @this, AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool)e.NewValue; + if (newValue) + { + @this.Attach(); + @this.UpdateWindowPosition(); + } + else + { + @this.Detach(); + } + } + + private void Attach() + { + if (attached) + return; + + //var hwndSource = GetHwndSource(); + //if (hwndSource == null) + // return; + + //var hwndParent = hwndSource.Handle; + //if (hwndParent == IntPtr.Zero) + // return; + + //// Get current DPI + //dpiScale = VisualTreeHelper.GetDpi(this); + + //var style = NativeHelper.GetWindowLong(Handle, NativeHelper.GWL_STYLE); + //// Removes Caption bar and the sizing border + //// Must be a child window to be hosted + //style |= NativeHelper.WS_CHILD; + + //NativeHelper.SetWindowLong(Handle, NativeHelper.GWL_STYLE, style); + //NativeHelper.ShowWindow(Handle, NativeHelper.SW_HIDE); + + //// Update the parent to be the parent of the host + //NativeHelper.SetParent(Handle, hwndParent); + + //// Register keyboard sink to make shortcuts work + //((IKeyboardInputSink)this).KeyboardInputSite = ((IKeyboardInputSink)hwndSource).RegisterKeyboardInputSink(this); + attached = true; + } + + private void Detach() + { + if (!attached) + return; + + //// Hide window, clear parent + //NativeHelper.ShowWindow(Handle, NativeHelper.SW_HIDE); + //NativeHelper.SetParent(Handle, IntPtr.Zero); + + //// Unregister keyboard sink + //var site = ((IKeyboardInputSink)this).KeyboardInputSite; + //((IKeyboardInputSink)this).KeyboardInputSite = null; + //site?.Unregister(); + + // Make sure we will actually attach next time Attach() is called + lastBoundingBox = Int4.Zero; + attached = false; + } + + private void UpdateWindowPosition() + { + if (updateRequested || !attached) + return; + + updateRequested = true; + + Dispatcher.UIThread.InvokeAsync(() => + { + updateRequested = false; + Visual? root = null; + var shouldShow = true; + var parent = this.GetVisualParent(); + while (parent != null) + { + root = parent; + + if (parent is Control parentElement) + { + if (!parentElement.IsLoaded || !parentElement.IsVisible) + shouldShow = false; + } + + parent = root.GetVisualParent(); + } + + if (root == null) + return; + + // FIXME xplat-editor + //// Find proper position for the game + //var positionTransform = TransformToAncestor(root); + //var areaPosition = positionTransform.Transform(new Point(0, 0)); + //var boundingBox = new Int4((int)(areaPosition.X * dpiScale.DpiScaleX), (int)(areaPosition.Y * dpiScale.DpiScaleY), (int)(ActualWidth * dpiScale.DpiScaleX), (int)(ActualHeight * dpiScale.DpiScaleY)); + //if (boundingBox != lastBoundingBox) + //{ + // lastBoundingBox = boundingBox; + // // Move the window asynchronously, without activating it, without touching the Z order + // // TODO: do we want SWP_NOCOPYBITS? + // const int flags = NativeHelper.SWP_ASYNCWINDOWPOS | NativeHelper.SWP_NOACTIVATE | NativeHelper.SWP_NOZORDER; + // NativeHelper.SetWindowPos(Handle, NativeHelper.HWND_TOP, boundingBox.X, boundingBox.Y, boundingBox.Z, boundingBox.W, flags); + //} + + if (attached) + { + // FIXME xplat-editor + //NativeHelper.ShowWindow(Handle, shouldShow ? NativeHelper.SW_SHOWNOACTIVATE : NativeHelper.SW_HIDE); + } + }, DispatcherPriority.Input); // This code must be dispatched after the DispatcherPriority.Loaded to properly work since it's checking the IsLoaded flag! + } + + /// + /// Forwards a message that comes from the hosted window to the WPF window. This method can be used for example to forward keyboard events. + /// + /// The hwnd of the hosted window. + /// The message identifier. + /// The word parameter of the message. + /// The long parameter of the message. + public void ForwardMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam) + { + DispatcherOperation task; + // FIXME xplat-editor + //switch (msg) + //{ + // case NativeHelper.WM_RBUTTONDOWN: + // mouseMoveCount = 0; + // task = Dispatcher.InvokeAsync(() => + // { + // RaiseMouseButtonEvent(Mouse.PreviewMouseDownEvent, MouseButton.Right); + // RaiseMouseButtonEvent(Mouse.MouseDownEvent, MouseButton.Right); + // }); + // task.Wait(TimeSpan.FromSeconds(1.0f)); + // break; + // case NativeHelper.WM_RBUTTONUP: + // task = Dispatcher.InvokeAsync(() => + // { + // RaiseMouseButtonEvent(Mouse.PreviewMouseUpEvent, MouseButton.Right); + // RaiseMouseButtonEvent(Mouse.MouseUpEvent, MouseButton.Right); + // }); + // task.Wait(TimeSpan.FromSeconds(1.0f)); + // break; + // case NativeHelper.WM_LBUTTONDOWN: + // task = Dispatcher.InvokeAsync(() => + // { + // RaiseMouseButtonEvent(Mouse.PreviewMouseDownEvent, MouseButton.Left); + // RaiseMouseButtonEvent(Mouse.MouseDownEvent, MouseButton.Left); + // }); + // task.Wait(TimeSpan.FromSeconds(1.0f)); + // break; + // case NativeHelper.WM_LBUTTONUP: + // task = Dispatcher.InvokeAsync(() => + // { + // RaiseMouseButtonEvent(Mouse.PreviewMouseUpEvent, MouseButton.Left); + // RaiseMouseButtonEvent(Mouse.MouseUpEvent, MouseButton.Left); + // }); + // task.Wait(TimeSpan.FromSeconds(1.0f)); + // break; + // case NativeHelper.WM_MOUSEMOVE: + // ++mouseMoveCount; + // break; + // case NativeHelper.WM_CONTEXTMENU: + // // TODO: Tracking drag offset would be better, but might be difficult since we replace the mouse to its initial position each time it is moved. + // if (mouseMoveCount < 3) + // { + // Dispatcher.InvokeAsync(() => + // { + // Visual? dependencyObject = this; + // while (dependencyObject != null) + // { + // var element = dependencyObject as Control; + // if (element?.ContextMenu != null) + // { + // element.Focus(); + // // Data context will not be properly set if the popup is open this way, so let's set it ourselves + // element.ContextMenu.SetCurrentValue(DataContextProperty, element.DataContext); + // element.ContextMenu.IsOpen = true; + // var source = (HwndSource)PresentationSource.FromVisual(element.ContextMenu); + // if (source != null) + // { + // source.AddHook(ContextMenuWndProc); + // contextMenuPosition = Mouse.GetPosition(this); + // lock (contextMenuSources) + // { + // contextMenuSources.Add(source); + // } + // } + // break; + // } + // dependencyObject = dependencyObject.GetVisualParent(); + // } + // }); + // } + // break; + // default: + // var parent = NativeHelper.GetParent(hwnd); + // NativeHelper.PostMessage(parent, msg, wParam, lParam); + // break; + //} + } + + //private void RaiseMouseButtonEvent(RoutedEvent routedEvent, MouseButton button) + //{ + // RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, Environment.TickCount, button) + // { + // RoutedEvent = routedEvent, + // Source = this, + // }); + //} + + //private IntPtr ContextMenuWndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + //{ + // switch (msg) + // { + // case NativeHelper.WM_LBUTTONDOWN: + // case NativeHelper.WM_RBUTTONDOWN: + // // We need to change from the context menu coordinates to the HwndHost coordinates and re-encode lParam + // var position = new Point(-(short)(lParam.ToInt64() & 0xFFFF), -((lParam.ToInt64() & 0xFFFF0000) >> 16)); + // var offset = contextMenuPosition - position; + // lParam = new IntPtr((short)offset.X + ((short)offset.Y << 16)); + // var threadId = NativeHelper.GetWindowThreadProcessId(Handle, IntPtr.Zero); + // NativeHelper.PostThreadMessage(threadId, msg, wParam, lParam); + // break; + // case NativeHelper.WM_DESTROY: + // lock (contextMenuSources) + // { + // var source = contextMenuSources.First(x => x.Handle == hwnd); + // source.RemoveHook(ContextMenuWndProc); + // } + // break; + // } + // return IntPtr.Zero; + //} + + //[CanBeNull] + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + //private HwndSource GetHwndSource() + //{ + // return (HwndSource)PresentationSource.FromVisual(this); + //} + + // FIXME xplat-editor + //IKeyboardInputSite IKeyboardInputSink.RegisterKeyboardInputSink(IKeyboardInputSink sink) + //{ + // throw new NotSupportedException(); + //} + + //bool IKeyboardInputSink.TranslateAccelerator(ref MSG msg, ModifierKeys modifiers) + //{ + // return false; + //} + + //bool IKeyboardInputSink.TabInto(TraversalRequest request) + //{ + // return false; + //} + + //bool IKeyboardInputSink.OnMnemonic(ref MSG msg, ModifierKeys modifiers) + //{ + // return false; + //} + + //bool IKeyboardInputSink.TranslateChar(ref MSG msg, ModifierKeys modifiers) + //{ + // return false; + //} + + //bool IKeyboardInputSink.HasFocusWithin() + //{ + // var focus = NativeHelper.GetFocus(); + // return Handle != IntPtr.Zero && (focus == Handle || NativeHelper.IsChild(Handle, focus)); + //} +} diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Engine/EmbeddedGameForm.cs b/sources/xplat-editor/Stride.Editor.Avalonia/Engine/EmbeddedGameForm.cs new file mode 100644 index 0000000000..370396057f --- /dev/null +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Engine/EmbeddedGameForm.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Presentation.Avalonia.Controls; +using Stride.Games; + +namespace Stride.Editor.Avalonia.Engine; + +/// +/// A specialization of that is able to forward keyboard and mousewheel events to an associated . +/// +[System.ComponentModel.DesignerCategory("")] +public class EmbeddedGameForm : GameFormSDL +{ + public EmbeddedGameForm() + { + IsFullScreen = false; + } + + /// + /// Gets or sets the associated to this form. + /// + public GameEngineHost Host { get; set; } + + ///// + //protected override void WndProc(ref System.Windows.Forms.Message m) + //{ + // if (Host != null) + // { + // switch (m.Msg) + // { + // case NativeHelper.WM_KEYDOWN: + // case NativeHelper.WM_KEYUP: + // case NativeHelper.WM_MOUSEWHEEL: + // case NativeHelper.WM_RBUTTONDOWN: + // case NativeHelper.WM_RBUTTONUP: + // case NativeHelper.WM_LBUTTONDOWN: + // case NativeHelper.WM_LBUTTONUP: + // case NativeHelper.WM_MOUSEMOVE: + // case NativeHelper.WM_CONTEXTMENU: + // Host.ForwardMessage(m.HWnd, m.Msg, m.WParam, m.LParam); + // break; + // } + // } + // base.WndProc(ref m); + //} +} diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs new file mode 100644 index 0000000000..64b495d5b9 --- /dev/null +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs @@ -0,0 +1,330 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Assets; +using Stride.Core; +using Stride.Core.Assets; +using Stride.Core.Assets.Compiler; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Diagnostics; +using Stride.Core.Extensions; +using Stride.Core.Presentation.Avalonia.Controls; +using Stride.Core.Presentation.Services; +using Stride.Editor.Avalonia.Engine; +using Stride.Editor.Build; +using Stride.Editor.Preview; +using Stride.Games; +using Stride.Graphics; + +namespace Stride.Editor.Avalonia.Preview; + +public class GameStudioPreviewService : IAssetPreviewService, IPreviewBuilder +{ + public static bool DisablePreview = false; + + private readonly ISessionViewModel session; + + private readonly AutoResetEvent initializationSignal = new(false); + private readonly GameEngineHost host; + // FIXME xplat-editor + //private readonly IDebugPage loggerDebugPage; + private IAssetPreview currentPreview; + private IntPtr windowHandle; + private EmbeddedGameForm gameForm; + private object previewView; + + private AssetViewModel previewBuildQueue; + private readonly SemaphoreSlim previewChangeLock = new(1, 1); + /// + /// A lock used to access the and fields safely. + /// + private readonly object previewLock = new(); + private readonly Thread previewGameThread; + private readonly Dictionary assetPreviewFactories = new(); + + private readonly AssetCompilerContext previewCompileContext = new() { Platform = PlatformType.Windows }; + private readonly AssetDependenciesCompiler previewCompiler = new(typeof(PreviewCompilationContext)); + + private readonly GameSettingsAsset previewGameSettings; + private readonly GameSettingsProviderService gameSettingsProvider; + + public GameStudioPreviewService(ISessionViewModel session) + { + this.session = session; + Dispatcher = session.Dispatcher; + AssetBuilderService = session.ServiceProvider.Get(); + gameSettingsProvider = session.ServiceProvider.Get(); + + Logger = GlobalLogger.GetLogger("Preview"); + // FIXME xplat-editor + //loggerDebugPage = EditorDebugTools.CreateLogDebugPage(Logger, "Preview"); + + previewGameSettings = GameSettingsFactory.Create(); + previewGameSettings.GetOrCreate().DefaultGraphicsProfile = GraphicsProfile.Level_11_0; + UpdateGameSettings(gameSettingsProvider.CurrentGameSettings); + previewCompileContext.SetGameSettingsAsset(previewGameSettings); + previewCompileContext.CompilationContext = typeof(PreviewCompilationContext); + + previewGameThread = new Thread(SafeAction.Wrap(StrideUIThread)) { IsBackground = true, Name = "PreviewGame Thread" }; + previewGameThread.SetApartmentState(ApartmentState.STA); + previewGameThread.Start(); + + // Wait for the window handle to be generated on the proper thread + initializationSignal.WaitOne(); + host = new GameEngineHost(windowHandle); + + session.AssetPropertiesChanged += OnAssetPropertyChanged; + gameSettingsProvider.GameSettingsChanged += OnGameSettingsChanged; + } + + /// + /// Gets whether this instance of has been disposed. + /// + public bool IsDisposed { get; private set; } + + public GameStudioBuilderService AssetBuilderService { get; } + + public IDispatcherService Dispatcher { get; } + + public Logger Logger { get; } + + public PreviewGame PreviewGame { get; private set; } + + public event EventHandler PreviewAssetUpdated; + + public void Dispose() + { + if (!IsDisposed) + { + // Terminate preview control thread + previewBuildQueue = null; + + session.AssetPropertiesChanged -= OnAssetPropertyChanged; + gameSettingsProvider.GameSettingsChanged -= OnGameSettingsChanged; + + if (PreviewGame.IsRunning) + { + PreviewGame.Exit(); + } + + // Wait for the game thread to terminate + previewGameThread.Join(); + + + //Game = null; + host.Dispose(); + //host = null; + //gameForm = null; + //windowHandle = IntPtr.Zero; + previewCompileContext?.Dispose(); + + // FIXME xplat-editor + //EditorDebugTools.UnregisterDebugPage(loggerDebugPage); + + IsDisposed = true; + } + } + + private void StrideUIThread() + { + // FIXME xplat-editor + //gameForm = new EmbeddedGameForm { TopLevel = false, Visible = false }; + gameForm = new EmbeddedGameForm { Visible = false }; + windowHandle = gameForm.Handle; + + initializationSignal.Set(); + + PreviewGame = new PreviewGame(AssetBuilderService.EffectCompiler); + var context = new GameContextSDL(gameForm) { InitializeDatabase = false }; + + // Wait for shaders to be loaded + AssetBuilderService.WaitForShaders(); + + // TODO: For now we stop if there is an exception + // Ideally, we should try to recreate the game. + if (!DisablePreview) + { + PreviewGame.GraphicsDeviceManager.DeviceCreated += GraphicsDeviceManagerDeviceCreated; + PreviewGame.Run(context); + PreviewGame.Dispose(); + } + } + + private void OnGameSettingsChanged(object? sender, GameSettingsChangedEventArgs e) + { + UpdateGameSettings(e.GameSettings); + } + + private void UpdateGameSettings(GameSettingsAsset currentGameSettings) + { + previewGameSettings.GetOrCreate().RenderingMode = currentGameSettings.GetOrCreate().RenderingMode; + previewGameSettings.GetOrCreate().ColorSpace = currentGameSettings.GetOrCreate().ColorSpace; + } + + private void OnAssetPropertyChanged(object? sender, AssetChangedEventArgs e) + { + lock (previewLock) + { + var allAssets = AssetViewModel.ComputeRecursiveReferencerAssets(e.Assets); + allAssets.AddRange(e.Assets); + if (currentPreview != null && allAssets.Contains(currentPreview.AssetViewModel)) + { + PreviewGame.Script.AddTask(UpdatePreviewAsset); + } + } + } + + private void GraphicsDeviceManagerDeviceCreated(object? sender, EventArgs e) + { + // Transmit actual GraphicsProfile to preview and thumbnail builder context + var graphicsProfile = PreviewGame.GraphicsDeviceManager.GraphicsDevice.Features.CurrentProfile; + //ThumbnailService.ThumbnailBuilderContext.GetGameSettingsAsset().Get().DefaultGraphicsProfile = graphicsProfile; + previewCompileContext.GetGameSettingsAsset().GetOrCreate().DefaultGraphicsProfile = graphicsProfile; + } + + public void SetAssetToPreview(AssetViewModel asset) + { + lock (previewLock) + { + previewBuildQueue = asset; + } + PreviewGame.Script.AddTask(ChangePreviewAsset); + } + + public AssetCompilerResult Compile(AssetItem asset) + { + return previewCompiler.Prepare(previewCompileContext, asset); + } + + public object GetStrideView() + { + return !IsDisposed ? host : null; + } + + private async Task UpdatePreviewAsset() + { + if (!previewChangeLock.Wait(0)) + return; + + try + { + // copy currentPreview to a local variable because it might be modified in a different thread. + var localPreview = currentPreview; + if (localPreview != null) + { + await localPreview.Update(); + } + Logger.Info("Preview updated following to a a property change."); + } + catch (Exception e) + { + Logger.Error("Unable to update the preview after a property change.", e); + } + finally + { + previewChangeLock.Release(); + } + } + + private async Task ChangePreviewAsset() + { + if (!previewChangeLock.Wait(0)) + return; + + AssetViewModel asset = null; + IAssetPreview nextPreview = null; + try + { + if (currentPreview != null) + { + IAssetPreview previousPreview; + // Ensure that the current preview won't be disposed twice with a lock + lock (previewLock) + { + previousPreview = currentPreview; + currentPreview = null; + } + await previousPreview.IsInitialized(); + await previousPreview.Dispose(); + Logger.Info($"Unloaded previous preview of {previousPreview.AssetViewModel.Url}."); + } + + lock (previewLock) + { + if (previewBuildQueue != null) + { + asset = previewBuildQueue; + nextPreview = GetPreviewForAsset(previewBuildQueue); + previewBuildQueue = null; + } + currentPreview = nextPreview; + } + + if (asset != null && nextPreview != null) + { + previewView = await nextPreview.Initialize(asset, this); + if (previewView != null) + Logger.Info($"Initialized preview of {nextPreview.AssetViewModel.Url}."); + } + else + { + previewView = null; + } + } + catch (Exception e) + { + lock (previewLock) + { + currentPreview = null; + } + previewView = null; + Logger.Error("An exception occurred while changing the previewed asset", e); + } + finally + { + previewChangeLock.Release(); + } + + // Notify that the previewed asset has changed, so the editor view can update its visual tree. + PreviewAssetUpdated?.Invoke(this, EventArgs.Empty); + } + + private IAssetPreview GetPreviewForAsset(AssetViewModel asset) + { + if (asset == null) throw new ArgumentNullException(nameof(asset)); + + var assetType = asset.Asset.GetType(); + while (assetType != null) + { + AssetPreviewFactory factory; + if (assetPreviewFactories.TryGetValue(assetType, out factory)) + { + var assetPreview = factory(this, PreviewGame, asset.AssetItem); + return assetPreview; + } + assetType = assetType.BaseType; + } + return null; + } + + public object GetCurrentPreviewView() + { + return previewView; + } + + public void RegisterAssetPreviewFactories(IReadOnlyDictionary factories) + { + factories.ForEach(x => assetPreviewFactories.Add(x.Key, x.Value)); + } + + public void OnShowPreview() + { + PreviewGame.IsEditorHidden = false; + } + + public void OnHidePreview() + { + PreviewGame.IsEditorHidden = true; + } +} diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Preview/Views/StridePreviewView.cs b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/Views/StridePreviewView.cs new file mode 100644 index 0000000000..f042dc165f --- /dev/null +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/Views/StridePreviewView.cs @@ -0,0 +1,68 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Interactivity; +using Stride.Editor.Preview; +using Stride.Editor.Preview.Views; + +namespace Stride.Editor.Avalonia.Preview.Views; + +[TemplatePart(Name = "PART_StrideView", Type = typeof(ContentPresenter))] +public class StridePreviewView : TemplatedControl, IPreviewView +{ + private IPreviewBuilder? builder; + private IAssetPreview? previewer; + private ContentPresenter? presenter; + + public void InitializeView(IPreviewBuilder previewBuilder, IAssetPreview assetPreview) + { + previewer = assetPreview; + builder = previewBuilder; + var viewModel = previewer.PreviewViewModel; + if (viewModel != null) + { + viewModel.AttachPreview(previewer); + DataContext = viewModel; + } + UpdateStrideView(); + + Loaded += OnLoaded; + } + + public void UpdateView(IAssetPreview assetPreview) + { + var viewModel = previewer?.PreviewViewModel; + if (viewModel != null) + { + viewModel.AttachPreview(previewer!); + DataContext = viewModel; + } + UpdateStrideView(); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + presenter = e.NameScope.Find("PART_StrideView"); + UpdateStrideView(); + } + + private void OnLoaded(object? sender, RoutedEventArgs routedEventArgs) + { + previewer?.OnViewAttached(); + } + + private void UpdateStrideView() + { + if (presenter != null && builder != null) + { + var strideView = builder.GetStrideView(); + presenter.Content = strideView; + } + } +} diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj index 95b3ef3a51..d50b198fb1 100644 --- a/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj @@ -2,8 +2,8 @@ - + From 8310a8775810c68aed3df7111c461e4a91093b8e Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Mon, 30 Oct 2023 12:54:27 +0100 Subject: [PATCH 217/247] [Editor] Debug service and window --- .../StrideEditorPlugin.cs | 9 +- .../Services/EditorDebugService.cs | 108 +++++++++++ .../Services/IDebugPage.cs | 4 +- .../Services/IEditorDebugService.cs | 10 +- .../ViewModels/DebugAssetBaseNodeViewModel.cs | 19 ++ .../DebugAssetChildNodeViewModel.cs | 84 ++++++++ .../DebugAssetNodeCollectionViewModel.cs | 99 ++++++++++ .../ViewModels/DebugAssetNodeViewModel.cs | 47 +++++ .../ViewModels/DebugAssetRootNodeViewModel.cs | 20 ++ .../ViewModels/DebugWindowViewModel.cs | 18 ++ .../ViewModels/LoggerViewModel.cs | 183 ++++++++++++++++++ .../ViewModels/OperationViewModel.cs | 33 ++++ .../ViewModels/SessionViewModel.cs | 48 +++-- .../ViewModels/UndoRedoViewModel.cs | 69 +++++++ .../Build/GameStudioBuilderService.cs | 34 ++-- .../Build/StrideShaderImporter.cs | 3 +- .../Controls/TextLogViewer.cs | 4 +- .../Preview/GameStudioPreviewService.cs | 9 +- .../Stride.GameStudio.Avalonia/App.axaml.cs | 2 + .../Controls/DebugAssetNodesUserControl.axaml | 89 +++++++++ .../DebugAssetNodesUserControl.axaml.cs | 14 ++ .../Controls/DebugLogUserControl.axaml | 44 +++++ .../Controls/DebugLogUserControl.axaml.cs | 14 ++ .../Controls/DebugUndoRedoUserControl.axaml | 27 +++ .../DebugUndoRedoUserControl.axaml.cs | 14 ++ .../Services/EditorDialogService.cs | 24 +++ .../ViewModels/MainViewModel.cs | 20 +- .../Views/DebugWindow.axaml | 33 ++++ .../Views/DebugWindow.axaml.cs | 40 ++++ .../Views/MainView.axaml | 12 +- 30 files changed, 1072 insertions(+), 62 deletions(-) create mode 100644 sources/editor/Stride.Core.Assets.Editor/Services/EditorDebugService.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetBaseNodeViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetChildNodeViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetRootNodeViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugWindowViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/LoggerViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/OperationViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Editor/ViewModels/UndoRedoViewModel.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml.cs diff --git a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs index f2c5d0fd93..3c1c96cbff 100644 --- a/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs +++ b/sources/editor/Stride.Assets.Editor/StrideEditorPlugin.cs @@ -48,12 +48,11 @@ public override void InitializeSession(ISessionViewModel session) var settingsProvider = new GameSettingsProviderService(session); session.ServiceProvider.RegisterService(settingsProvider); - // FIXME xplat-editor broken for now - //var builderService = new GameStudioBuilderService(session, settingsProvider, buildDirectory); - //session.ServiceProvider.RegisterService(builderService); + var builderService = new GameStudioBuilderService(session, settingsProvider, buildDirectory); + session.ServiceProvider.RegisterService(builderService); - //var thumbnailService = new GameStudioThumbnailService(session, settingsProvider, builderService); - //session.ServiceProvider.RegisterService(thumbnailService); + var thumbnailService = new GameStudioThumbnailService(session, settingsProvider, builderService); + session.ServiceProvider.RegisterService(thumbnailService); } public override void RegisterAssetPreviewViewModelTypes(IDictionary assetPreviewViewModelTypes) diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/EditorDebugService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/EditorDebugService.cs new file mode 100644 index 0000000000..f4b4407eb0 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/Services/EditorDebugService.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Editor.ViewModels; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.Services; + +public sealed class EditorDebugService : IEditorDebugService +{ + private static readonly List debugPages = []; + private static readonly HashSet debugWindows = []; + + private readonly IViewModelServiceProvider serviceProvider; + + public EditorDebugService(IViewModelServiceProvider serviceProvider) + { + this.serviceProvider = serviceProvider; + } + + private IDispatcherService Dispatcher => serviceProvider.Get(); + + public IDebugPage CreateAssetNodesDebugPage(ISessionViewModel session, string title, bool register = true) + { + return Dispatcher.Invoke(() => + { + var page = new DebugAssetNodeCollectionViewModel(session) { Title = title }; + if (register) + { + RegisterDebugPage(page); + } + return page; + }); + } + + public IDebugPage CreateLogDebugPage(Logger logger, string title, bool register = true) + { + // Activate all log levels + logger.ActivateLog(LogMessageType.Debug); + + return Dispatcher.Invoke(() => + { + var page = new LoggerViewModel(serviceProvider, logger) { Title = title }; + if (register) + { + RegisterDebugPage(page); + } + return page; + }); + } + + public IDebugPage CreateUndoRedoDebugPage(IUndoRedoService actionService, string title, bool register = true) + { + return Dispatcher.Invoke(() => + { + var page = new UndoRedoViewModel(serviceProvider, actionService) { Title = title }; + if (register) + { + RegisterDebugPage(page); + } + return page; + }); + } + + public void RegisterDebugPage(IDebugPage? page) + { + if (page is null) return; + Dispatcher.CheckAccess(); + + debugPages.Add(page); + foreach (var debugWindow in debugWindows) + { + debugWindow.Pages.Add(page); + } + } + + public void UnregisterDebugPage(IDebugPage? page) + { + if (page is null) return; + Dispatcher.CheckAccess(); + + debugPages.Remove(page); + foreach (var debugWindow in debugWindows) + { + debugWindow.Pages.Remove(page); + } + (page as IDestroyable)?.Destroy(); + } + + public static void RegisterDebugWindow(DebugWindowViewModel debugWindow) + { + if (debugWindows.Add(debugWindow)) + { + debugWindow.Pages.AddRange(debugPages); + } + } + + public static void UnregisterDebugWindow(DebugWindowViewModel debugWindow) + { + if (debugWindows.Remove(debugWindow)) + { + debugWindow.Pages.Clear(); + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs index 5def190c8c..3b46fb6065 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IDebugPage.cs @@ -1,9 +1,9 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. namespace Stride.Core.Assets.Editor.Services; public interface IDebugPage { - string Title { get; set; } + string Title { get; } } diff --git a/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs b/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs index 3dfc1f44c1..d8aeab2410 100644 --- a/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs +++ b/sources/editor/Stride.Core.Assets.Editor/Services/IEditorDebugService.cs @@ -9,13 +9,13 @@ namespace Stride.Core.Assets.Editor.Services; public interface IEditorDebugService { - IDebugPage CreateLogDebugPage(Logger logger, string title, bool register = true); + IDebugPage? CreateLogDebugPage(Logger logger, string title, bool register = true); - IDebugPage CreateUndoRedoDebugPage(IUndoRedoService service, string title, bool register = true); + IDebugPage? CreateUndoRedoDebugPage(IUndoRedoService actionService, string title, bool register = true); - IDebugPage CreateAssetNodesDebugPage(ISessionViewModel session, string title, bool register = true); + IDebugPage? CreateAssetNodesDebugPage(ISessionViewModel session, string title, bool register = true); - void RegisterDebugPage(IDebugPage page); + void RegisterDebugPage(IDebugPage? page); - void UnregisterDebugPage(IDebugPage page); + void UnregisterDebugPage(IDebugPage? page); } diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetBaseNodeViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetBaseNodeViewModel.cs new file mode 100644 index 0000000000..cb9290ee40 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetBaseNodeViewModel.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Quantum; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class DebugAssetBaseNodeViewModel : DebugAssetNodeViewModel +{ + public DebugAssetBaseNodeViewModel(IViewModelServiceProvider serviceProvider, IGraphNode node) + : base(serviceProvider, node) + { + Asset = DebugAssetNodeCollectionViewModel.FindAssetForNode(node.Guid); + } + + public AssetViewModel? Asset { get; } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetChildNodeViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetChildNodeViewModel.cs new file mode 100644 index 0000000000..f2de22dd27 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetChildNodeViewModel.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Assets.Quantum; +using Stride.Core.Reflection; +using Stride.Core.Quantum; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class DebugAssetChildNodeViewModel : DebugAssetNodeViewModel +{ + public const string LinkRoot = "Root"; + public const string LinkChild = "Child"; + public const string LinkRef = "Ref"; + + private readonly HashSet registeredNodes; + + public DebugAssetChildNodeViewModel(IViewModelServiceProvider serviceProvider, IGraphNode? node, HashSet registeredNodes) + : this(serviceProvider, node, NodeIndex.Empty, null, LinkRoot, registeredNodes) + { + } + + private DebugAssetChildNodeViewModel(IViewModelServiceProvider serviceProvider, IGraphNode? node, NodeIndex index, ItemId? itemId, string linkFromParent, HashSet registeredNodes) + : base(serviceProvider, node) + { + this.registeredNodes = registeredNodes; + LinkFromParent = linkFromParent; + Index = index; + ItemId = itemId; + Registered = node == null || registeredNodes.Contains(node); + var assetNode = (IAssetNode?)node; + var baseNode = assetNode?.BaseNode; + if (baseNode != null) + Base = new DebugAssetBaseNodeViewModel(serviceProvider, baseNode); + } + + public NodeIndex Index { get; } + + public ItemId? ItemId { get; } + + public string LinkFromParent { get; } + + public bool Registered { get; } + + public DebugAssetBaseNodeViewModel? Base { get; } + + public List Children => UpdateChildren(); + + protected List UpdateChildren() + { + var list = new List(); + if (Node != null && Registered) + { + if (Node is IObjectNode objNode) + { + foreach (var child in objNode.Members) + { + list.Add(new DebugAssetChildNodeViewModel(ServiceProvider, child, NodeIndex.Empty, null, LinkChild, registeredNodes)); + } + } + if (Node.IsReference) + { + var objReference = (Node as IMemberNode)?.TargetReference; + if (objReference != null) + { + list.Add(new DebugAssetChildNodeViewModel(ServiceProvider, objReference.TargetNode, objReference.Index, null, LinkRef, registeredNodes)); + } + else + { + CollectionItemIdHelper.TryGetCollectionItemIds(Node.Retrieve(), out var itemIds); + foreach (var reference in ((IObjectNode)Node).ItemReferences) + { + ItemId? itemId = null; + if (itemIds != null && itemIds.TryGet(reference.Index.Value, out var retrievedItemId)) + itemId = retrievedItemId; + list.Add(new DebugAssetChildNodeViewModel(ServiceProvider, reference.TargetNode, reference.Index, itemId, LinkRef, registeredNodes)); + + } + } + } + } + return list; + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs new file mode 100644 index 0000000000..ffa98dce73 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs @@ -0,0 +1,99 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Reflection; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Presentation.ViewModels; +using Stride.Core.Assets.Quantum; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Quantum; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class DebugAssetNodeCollectionViewModel : DispatcherViewModel, IDebugPage +{ + private static readonly FieldInfo FieldInfoListener; + private static readonly FieldInfo FieldInfoRegisteredNodes; + private readonly ISessionViewModel session; + + private object? selectedNode; + private static readonly Dictionary NodeToAssetMap = new(); + + static DebugAssetNodeCollectionViewModel() + { + // We use reflection to access a non-public field. + var fieldInfoListener = typeof(AssetPropertyGraph).GetField("nodeListener", BindingFlags.Instance | BindingFlags.NonPublic); + var fieldInfoRegisteredNodes = typeof(GraphNodeChangeListener).GetField("RegisteredNodes", BindingFlags.Instance | BindingFlags.NonPublic); + FieldInfoListener = fieldInfoListener ?? throw new MissingFieldException("AssetPropertyGraph is missing the nodeListener private member."); + FieldInfoRegisteredNodes = fieldInfoRegisteredNodes ?? throw new MissingFieldException("GraphNodeChangeListener is missing the RegisteredNodes private member."); + } + + public DebugAssetNodeCollectionViewModel(ISessionViewModel session) + : base(session.ServiceProvider) + { + this.session = session; + + RefreshQuantumNodesCommand = new AnonymousCommand(ServiceProvider, RefreshQuantumViewModel); + } + + public ObservableList AssetNodes { get; } = []; + + public object? SelectedNode + { + get => selectedNode; + set => SetValue(ref selectedNode, value); + } + + public string Title { get; init; } = string.Empty; + + public ICommandBase RefreshQuantumNodesCommand { get; } + + public static AssetViewModel? FindAssetForNode(Guid nodeId) + { + NodeToAssetMap.TryGetValue(nodeId, out var asset); + return asset; + } + + private void RefreshQuantumViewModel() + { + RefreshNodeToAssetMap(); + AssetNodes.Clear(); + // FIXME xplat-editor + //foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) + //{ + // var nodes = GetRegisterNodes(asset.PropertyGraph); + // if (nodes == null) + // continue; + + // var rootNode = new DebugAssetRootNodeViewModel(ServiceProvider, asset.Url, asset.AssetRootNode, nodes); + // AssetNodes.Add(rootNode); + //} + } + + private void RefreshNodeToAssetMap() + { + // FIXME xplat-editor + //foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) + //{ + // var nodes = GetRegisterNodes(asset.PropertyGraph); + // if (nodes == null) + // continue; + + // foreach (var node in nodes) + // { + // NodeToAssetMap[node.Guid] = asset; + // } + //} + } + + private static HashSet? GetRegisterNodes(AssetPropertyGraph? propertyGraph) + { + if (propertyGraph == null) + return null; + + var listener = (GraphNodeChangeListener?)FieldInfoListener.GetValue(propertyGraph); + return (HashSet?)FieldInfoRegisteredNodes.GetValue(listener); + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeViewModel.cs new file mode 100644 index 0000000000..5eb4186536 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeViewModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Quantum; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class DebugAssetNodeViewModel : DispatcherViewModel +{ + public const string Null = "(NULL)"; + + protected readonly IGraphNode? Node; + + public DebugAssetNodeViewModel(IViewModelServiceProvider serviceProvider, IGraphNode? node) + : base(serviceProvider) + { + Node = node; + BreakCommand = new AnonymousCommand(ServiceProvider, Break); + } + + public string Name => (Node as IMemberNode)?.Name ?? Node?.Type.Name ?? Null; + + public string Value => Node?.Retrieve()?.ToString() ?? Null; + + public string ContentType => GetContentType(); + + public Type? Type => Node?.Type; + + public ICommandBase BreakCommand { get; } + + private string GetContentType() + { + if (Node is IMemberNode) return "Member"; + if (Node is BoxedNode) return "Object (boxed)"; + if (Node is IObjectNode) return "Object"; + return "Unknown"; + } + + private void Break() + { + if (Debugger.IsAttached) + Debugger.Break(); + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetRootNodeViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetRootNodeViewModel.cs new file mode 100644 index 0000000000..ca6fa6987d --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetRootNodeViewModel.cs @@ -0,0 +1,20 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Quantum; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class DebugAssetRootNodeViewModel : DebugAssetChildNodeViewModel +{ + public DebugAssetRootNodeViewModel(IViewModelServiceProvider serviceProvider, string assetName, IGraphNode? node, HashSet registeredNodes) + : base(serviceProvider, node, registeredNodes) + { + AssetName = assetName; + } + + public string AssetName { get; } + + public Type? AssetType => Node?.Type; +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugWindowViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugWindowViewModel.cs new file mode 100644 index 0000000000..6c470e6310 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugWindowViewModel.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public sealed class DebugWindowViewModel : ViewModelBase +{ + public DebugWindowViewModel(IViewModelServiceProvider serviceProvider) + : base(serviceProvider) + { + } + + public ObservableList Pages { get; } = []; +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/LoggerViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/LoggerViewModel.cs new file mode 100644 index 0000000000..a611bed186 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/LoggerViewModel.cs @@ -0,0 +1,183 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections.Specialized; +using System.ComponentModel; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Diagnostics; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public class LoggerViewModel : DispatcherViewModel, IDebugPage +{ + /// + /// The default delay to wait before updating the collection, after a message has been received. + /// + public const int DefaultUpdateInterval = 300; + + private readonly ObservableList messages = []; + private readonly List<(Logger, ILogMessage)> pendingMessages = []; + + private int updateInterval = DefaultUpdateInterval; + private bool updatePending; + private bool hasNewMessages; + + public LoggerViewModel(IViewModelServiceProvider serviceProvider) + : base(serviceProvider) + { + messages.CollectionChanged += MessagesCollectionChanged; + } + + public LoggerViewModel(IViewModelServiceProvider serviceProvider, Logger logger) + : this(serviceProvider) + { + Loggers.Add(logger, []); + logger.MessageLogged += MessageLogged; + } + + public LoggerViewModel(IViewModelServiceProvider serviceProvider, IEnumerable loggers) + : this(serviceProvider) + { + foreach (var logger in loggers) + { + Loggers.Add(logger, []); + logger.MessageLogged += MessageLogged; + } + } + + /// + /// Gets whether the monitored logs have errors. + /// + /// This property does not raise the and events. + public bool HasErrors { get; private set; } + + /// + /// Gets whether the monitored logs have new messages. + /// + public bool HasNewMessages { get { return hasNewMessages; } private set { SetValue(ref hasNewMessages, value); } } + + /// + /// Gets whether the monitored logs have warnings. + /// + /// This property does not raise the and events. + public bool HasWarnings { get; private set; } + + /// + /// Gets the minimum level of message that will be recorded by this view model. + /// + public LogMessageType MinLevel { get; set; } = LogMessageType.Debug; + + /// + /// Gets the collection of messages currently contained in this view model. + /// + public IReadOnlyObservableCollection Messages => messages; + + public string Title { get; init; } = string.Empty; + + /// + /// Gets or sets the interval in milliseconds between updates of the collection. When a message is logged into one of the loggers, + /// the view model will wait this interval before actually updating the message collection to catch other potential messages in a single shot. + /// + /// The default value is equal to . + public int UpdateInterval { get { return updateInterval; } set { SetValue(ref updateInterval, value); } } + + protected Dictionary> Loggers { get; } = new(); + + /// + /// Flushes the pending log messages to add them immediately in the view model. + /// + public void Flush() + { + // Temporary cut the update interval. We use the backing field directly to + // prevent triggering a PropertyChanged event. + var interval = updateInterval; + updateInterval = 0; + Dispatcher.Invoke(UpdateMessagesAsync); + updateInterval = interval; + } + + /// + /// Raised when the messages collection is changed. Updates and properties. + /// + private void MessagesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Add) + { + if (e.NewItems != null) + { + foreach (ILogMessage newMessage in e.NewItems) + { + switch (newMessage.Type) + { + case LogMessageType.Warning: + HasWarnings = true; + break; + case LogMessageType.Error: + case LogMessageType.Fatal: + HasErrors = true; + break; + } + } + } + HasNewMessages = true; + } + else + { + HasWarnings = messages.Any(x => x.Type == LogMessageType.Warning); + HasErrors = messages.Any(x => x.Type == LogMessageType.Error || x.Type == LogMessageType.Fatal); + } + } + + /// + /// The callback of the event, used to monitor incoming messages. + /// + /// The event sender. + /// The event argument. + private void MessageLogged(object? sender, MessageLoggedEventArgs args) + { + lock (pendingMessages) + { + if (sender is Logger logger && args.Message.IsAtLeast(MinLevel)) + { + pendingMessages.Add((logger, args.Message)); + if (!updatePending) + { + updatePending = true; + Dispatcher.Invoke(UpdateMessagesAsync); + } + } + } + } + + /// + /// This methods waits the delay and then updates the collection by adding all pending messages. + /// + private async Task UpdateMessagesAsync() + { + if (UpdateInterval >= 0) await Task.Delay(UpdateInterval); + + List<(Logger, ILogMessage)>? messagesToAdd = null; + lock (pendingMessages) + { + if (pendingMessages.Count > 0) + { + messagesToAdd = pendingMessages.ToList(); + pendingMessages.Clear(); + } + updatePending = false; + } + if (messagesToAdd != null) + { + foreach (var messageToAdd in messagesToAdd) + { + messages.Add(messageToAdd.Item2); + if (Loggers.TryGetValue(messageToAdd.Item1, out var logger)) + { + logger.Add(messageToAdd.Item2); + } + } + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/OperationViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/OperationViewModel.cs new file mode 100644 index 0000000000..d736e06b1a --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/OperationViewModel.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Transactions; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public sealed class OperationViewModel : DispatcherViewModel +{ + private readonly IUndoRedoService actionService; + + public OperationViewModel(IViewModelServiceProvider serviceProvider, IUndoRedoService actionService, Operation operation) + : base(serviceProvider) + { + this.actionService = actionService; + Operation = operation; + if (operation is IReadOnlyTransaction transaction) + { + Children.AddRange(transaction.Operations.Select(x => new OperationViewModel(ServiceProvider, this.actionService, x))); + } + } + + public string? Name => actionService.GetName(Operation); + + public string Type => Operation.GetType().Name; + + public ObservableList Children { get; } = []; + + internal Operation Operation { get; } +} diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs index 6b41c6de68..51e143f197 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs @@ -10,8 +10,8 @@ using Stride.Core.Diagnostics; using Stride.Core.Extensions; using Stride.Core.IO; -using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Quantum.ViewModels; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; @@ -25,6 +25,10 @@ public sealed class SessionViewModel : DispatcherViewModel, ISessionViewModel private readonly Dictionary packageMap = []; private readonly PackageSession session; + private readonly IDebugPage? assetNodesDebugPage; + private readonly IDebugPage? quantumDebugPage; + private readonly IDebugPage? undoRedoStackPage; + private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSession session, ILogger logger) : base(serviceProvider) { @@ -42,7 +46,16 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi // Initialize the asset collection view model EditorCollection = new EditorCollectionViewModel(this); - activeProperties = AssetCollection.AssetViewProperties; + // Initialize debug pages + var debugService = serviceProvider.Get(); + assetNodesDebugPage = debugService.CreateAssetNodesDebugPage(this, "Asset nodes visualizer"); + quantumDebugPage = debugService.CreateLogDebugPage(GlobalLogger.GetLogger(GraphViewModel.DefaultLoggerName), "Quantum log"); + if (ActionService is { } actionService) + { + undoRedoStackPage = debugService.CreateUndoRedoDebugPage(actionService, "Undo/redo stack"); + } + + ActiveProperties = AssetCollection.AssetViewProperties; // Initialize commands EditSelectedContentCommand = new AnonymousCommand(serviceProvider, OnEditSelectedContent); @@ -86,7 +99,7 @@ public SessionObjectPropertiesViewModel ActiveProperties public AssetCollectionViewModel AssetCollection { get; } public AssetNodeContainer AssetNodeContainer { get; } - + /// /// Gets the current active project for build/startup operations. /// @@ -96,12 +109,12 @@ public ProjectViewModel? CurrentProject get => currentProject; private set { - var oldValue = currentProject; + var oldValue = currentProject; //SetValueUncancellable(ref currentProject, value, () => UpdateCurrentProject(oldValue, value)); SetValue(ref currentProject, value, () => UpdateCurrentProject(oldValue, value)); } } - + /// /// Gets the dependency manager associated to this session. /// @@ -117,7 +130,7 @@ private set internal IAssetsPluginService PluginService => ServiceProvider.Get(); - internal IUndoRedoService? UndoRedoService => ServiceProvider.TryGet(); + internal IUndoRedoService? ActionService => ServiceProvider.TryGet(); /// /// Raised when some assets are modified. @@ -141,8 +154,8 @@ private set // TODO register a bunch of services //serviceProvider.RegisterService(new CopyPasteService()); // Create the undo/redo service for this session. We use an initial size of 0 to prevent asset upgrade to be cancellable. - var undoRedoService = new UndoRedoService(0); - serviceProvider.RegisterService(undoRedoService); + var actionService = new UndoRedoService(0); + serviceProvider.RegisterService(actionService); var sessionViewModel = await Task.Run(() => { @@ -159,9 +172,9 @@ private set if (!token.IsCancellationRequested) { result = new SessionViewModel(serviceProvider, sessionResult.Session, sessionResult); - + // Build asset view models - result.LoadAssetsFromPackages(token); + result.LoadAssetsFromPackages(token); } } catch (Exception ex) @@ -169,14 +182,14 @@ private set sessionResult.Error("There was a problem opening the solution.", ex); result = null; } - + return result; }, token); return sessionViewModel; } - + /// public override void Destroy() { @@ -184,6 +197,11 @@ public override void Destroy() Thumbnails.Destroy(); + var debugService = ServiceProvider.Get(); + debugService.UnregisterDebugPage(undoRedoStackPage); + debugService.UnregisterDebugPage(assetNodesDebugPage); + debugService.UnregisterDebugPage(quantumDebugPage); + base.Destroy(); } @@ -193,7 +211,7 @@ public Type GetAssetViewModelType(AssetItem assetItem) var assetType = assetItem.Asset.GetType(); return PluginService.GetAssetViewModelType(assetType) ?? typeof(AssetViewModel<>); } - + /// public void RegisterAsset(AssetViewModel asset) { @@ -243,10 +261,10 @@ private void LoadAssetsFromPackages(CancellationToken token = default) } // This transaction is done to prevent action responding to undoRedoService.TransactionCompletion to occur during loading - using var transaction = UndoRedoService?.CreateTransaction(); + using var transaction = ActionService?.CreateTransaction(); ProcessAddedPackages(AllPackages).Forget(); } - + private async Task ProcessAddedPackages(IEnumerable packages) { var packageList = packages.ToList(); diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/UndoRedoViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/UndoRedoViewModel.cs new file mode 100644 index 0000000000..87a188a6e1 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/UndoRedoViewModel.cs @@ -0,0 +1,69 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Extensions; +using Stride.Core.Transactions; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; +using Stride.Core.Assets.Editor.Services; + +namespace Stride.Core.Assets.Editor.ViewModels; + +public sealed class UndoRedoViewModel : DispatcherViewModel, IDebugPage +{ + private readonly IUndoRedoService actionService; + + public UndoRedoViewModel(IViewModelServiceProvider serviceProvider, IUndoRedoService actionService) + : base(serviceProvider) + { + this.actionService = actionService; + ClearDiscardedItemsCommand = new AnonymousCommand(ServiceProvider, () => DiscardedTransactions.Clear()); + actionService.Done += TransactionAdded; + actionService.TransactionDiscarded -= TransactionDiscarded; + actionService.Cleared += UndoStackCleared; + Transactions.AddRange(actionService.RetrieveAllTransactions().Select(x => new OperationViewModel(ServiceProvider, actionService, (Operation)x))); + } + + public string Title { get; init; } = string.Empty; + + public ObservableList Transactions { get; } = []; + + public ObservableList DiscardedTransactions { get; } = []; + + public ICommandBase ClearDiscardedItemsCommand { get; private set; } + + /// + public override void Destroy() + { + actionService.Done -= TransactionAdded; + actionService.TransactionDiscarded -= TransactionDiscarded; + actionService.Cleared -= UndoStackCleared; + base.Destroy(); + } + + private void TransactionAdded(object? sender, TransactionEventArgs e) + { + if (e.Transaction.Operations.Count == 0) + return; + + Dispatcher.InvokeAsync(() => Transactions.Add(new OperationViewModel(ServiceProvider, actionService, (Operation)e.Transaction))).Forget(); + } + + private void TransactionDiscarded(object? sender, TransactionsDiscardedEventArgs e) + { + Dispatcher.InvokeAsync(() => + { + foreach (var transaction in e.Transactions) + { + Transactions.RemoveWhere(x => x.Operation == transaction); + } + }).Forget(); + } + + private void UndoStackCleared(object? sender, EventArgs e) + { + Dispatcher.InvokeAsync(() => Transactions.Clear()).Forget(); + } +} diff --git a/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs b/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs index 1f23dbc526..7e0e5786a4 100644 --- a/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs +++ b/sources/editor/Stride.Editor/Build/GameStudioBuilderService.cs @@ -4,40 +4,45 @@ using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Presentation.ViewModels; using Stride.Core.BuildEngine; +using Stride.Core.Diagnostics; using Stride.Core.IO; using Stride.Shaders.Compiler; namespace Stride.Editor.Build; -public class GameStudioBuilderService : AssetBuilderService +public sealed class GameStudioBuilderService : AssetBuilderService { public static string GlobalEffectLogPath; private readonly ManualResetEvent shaderLoadedEvent = new(false); private readonly EffectPriorityScheduler taskScheduler; private readonly EffectCompilerBase effectCompiler; - // FIXME xplat-editor - //private readonly IDebugPage assetBuilderServiceDebugPage; - //private readonly IDebugPage effectCompilerServiceDebugPage; + private readonly IEditorDebugService? debugService; + private readonly IDebugPage? assetBuilderServiceDebugPage; + private readonly IDebugPage? effectCompilerServiceDebugPage; private readonly bool createDebugTools; private int currentJobToken = -1; - public GameStudioBuilderService(ISessionViewModel sessionViewModel, GameSettingsProviderService settingsProvider, string buildDirectory, bool createDebugTools = true) + public GameStudioBuilderService(ISessionViewModel session, GameSettingsProviderService settingsProvider, string buildDirectory, bool createDebugTools = true) : base(buildDirectory) { this.createDebugTools = createDebugTools; if (createDebugTools) { - // FIXME xplat-editor - //assetBuilderServiceDebugPage = EditorDebugTools.CreateLogDebugPage(GlobalLogger.GetLogger("AssetBuilderService"), "AssetBuilderService"); - //effectCompilerServiceDebugPage = EditorDebugTools.CreateLogDebugPage(GlobalLogger.GetLogger("EffectCompilerCache"), "EffectCompilerCache"); + debugService = session.ServiceProvider.TryGet(); + assetBuilderServiceDebugPage = debugService?.CreateLogDebugPage(GlobalLogger.GetLogger("AssetBuilderService"), "AssetBuilderService"); + effectCompilerServiceDebugPage = debugService?.CreateLogDebugPage(GlobalLogger.GetLogger("EffectCompilerCache"), "EffectCompilerCache"); } - Session = sessionViewModel ?? throw new ArgumentNullException(nameof(sessionViewModel)); + Session = session; + // FIXME xplat-editor crashes var shaderImporter = new StrideShaderImporter(); - var shaderBuildSteps = shaderImporter.CreateSystemShaderBuildSteps(sessionViewModel); - shaderBuildSteps.StepProcessed += ShaderBuildStepsStepProcessed; - PushBuildUnit(new PrecompiledAssetBuildUnit(AssetBuildUnitIdentifier.Default, shaderBuildSteps, true)); + var shaderBuildSteps = shaderImporter.CreateSystemShaderBuildSteps(session); + if (shaderBuildSteps != null) + { + shaderBuildSteps.StepProcessed += ShaderBuildStepsStepProcessed; + PushBuildUnit(new PrecompiledAssetBuildUnit(AssetBuildUnitIdentifier.Default, shaderBuildSteps, true)); + } Database = new GameStudioDatabase(this, settingsProvider); @@ -84,9 +89,8 @@ public override void Dispose() base.Dispose(); if (createDebugTools) { - // FIXME xplat-editor - //EditorDebugTools.UnregisterDebugPage(assetBuilderServiceDebugPage); - //EditorDebugTools.UnregisterDebugPage(effectCompilerServiceDebugPage); + debugService?.UnregisterDebugPage(assetBuilderServiceDebugPage!); + debugService?.UnregisterDebugPage(effectCompilerServiceDebugPage!); } if (!IsDisposed) { diff --git a/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs b/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs index 2798421245..669a70b80f 100644 --- a/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs +++ b/sources/editor/Stride.Editor/Build/StrideShaderImporter.cs @@ -57,9 +57,8 @@ public override string ToString() /// /// The session used to retrieve currently used system packages. /// A containing the steps to build all shaders from system packages. - public ListBuildStep CreateSystemShaderBuildSteps(ISessionViewModel session) + public ListBuildStep? CreateSystemShaderBuildSteps(ISessionViewModel session) { - if (session == null) throw new ArgumentNullException(nameof(session)); // Check if there are any new system projects to preload // TODO: PDX-1251: For now, allow non-system project as well (which means they will be loaded only once at startup) // Later, they should be imported depending on what project the currently previewed/built asset is diff --git a/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/TextLogViewer.cs b/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/TextLogViewer.cs index b66b30e655..c3bddb72fd 100644 --- a/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/TextLogViewer.cs +++ b/sources/xplat-editor/Stride.Core.Presentation.Avalonia/Controls/TextLogViewer.cs @@ -13,7 +13,7 @@ using Stride.Core.Diagnostics; using Stride.Core.Presentation.Collections; -namespace Stride.Core.Presentation.Avalonia.Controls; +namespace Stride.Core.Assets.Editor.Avalonia.Controls; [TemplatePart(Name = "PART_LogText", Type = typeof(TextBlock))] [TemplatePart(Name = "PART_ClearLog", Type = typeof(Button))] @@ -398,7 +398,7 @@ private void ResetText() AppendText(logMessages); } } - + private void SelectPreviousOccurrence() { // FIXME xplat-editor search diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs index 64b495d5b9..2d25e38c28 100644 --- a/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Preview/GameStudioPreviewService.cs @@ -27,8 +27,7 @@ public class GameStudioPreviewService : IAssetPreviewService, IPreviewBuilder private readonly AutoResetEvent initializationSignal = new(false); private readonly GameEngineHost host; - // FIXME xplat-editor - //private readonly IDebugPage loggerDebugPage; + private readonly IDebugPage? loggerDebugPage; private IAssetPreview currentPreview; private IntPtr windowHandle; private EmbeddedGameForm gameForm; @@ -57,8 +56,7 @@ public GameStudioPreviewService(ISessionViewModel session) gameSettingsProvider = session.ServiceProvider.Get(); Logger = GlobalLogger.GetLogger("Preview"); - // FIXME xplat-editor - //loggerDebugPage = EditorDebugTools.CreateLogDebugPage(Logger, "Preview"); + loggerDebugPage = session.ServiceProvider.TryGet()?.CreateLogDebugPage(Logger, "Preview"); previewGameSettings = GameSettingsFactory.Create(); previewGameSettings.GetOrCreate().DefaultGraphicsProfile = GraphicsProfile.Level_11_0; @@ -119,8 +117,7 @@ public void Dispose() //windowHandle = IntPtr.Zero; previewCompileContext?.Dispose(); - // FIXME xplat-editor - //EditorDebugTools.UnregisterDebugPage(loggerDebugPage); + session.ServiceProvider.TryGet()?.UnregisterDebugPage(loggerDebugPage); IsDisposed = true; } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs index b30aac8f6b..8b811ce083 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/App.axaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Data.Core.Plugins; using Avalonia.Markup.Xaml; +using Stride.Core.Assets.Editor.Services; using Stride.Core.Presentation.Avalonia.Services; using Stride.Core.Presentation.ViewModels; using Stride.GameStudio.Avalonia.Services; @@ -66,6 +67,7 @@ private static IViewModelServiceProvider InitializeServiceProvider() new PluginService() }; var serviceProvider = new ViewModelServiceProvider(services); + serviceProvider.RegisterService(new EditorDebugService(serviceProvider)); serviceProvider.RegisterService(new EditorDialogService(serviceProvider)); return serviceProvider; } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml new file mode 100644 index 0000000000..2a7bca68eb --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + --- + + + + + + + + + + + + + + + + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml.cs new file mode 100644 index 0000000000..35d0017507 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugAssetNodesUserControl.axaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; + +namespace Stride.GameStudio.Avalonia.Controls; + +public partial class DebugAssetNodesUserControl : UserControl +{ + public DebugAssetNodesUserControl() + { + InitializeComponent(); + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml new file mode 100644 index 0000000000..45bf9e2a49 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml @@ -0,0 +1,44 @@ + + + + + + + + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml.cs new file mode 100644 index 0000000000..25c3a67199 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugLogUserControl.axaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; + +namespace Stride.GameStudio.Avalonia.Controls; + +public partial class DebugLogUserControl : UserControl +{ + public DebugLogUserControl() + { + InitializeComponent(); + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml new file mode 100644 index 0000000000..fc02457c71 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml @@ -0,0 +1,27 @@ + + + + + + + + () + + + + + + + + + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml.cs new file mode 100644 index 0000000000..76cd2b1746 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Controls/DebugUndoRedoUserControl.axaml.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; + +namespace Stride.GameStudio.Avalonia.Controls; + +public partial class DebugUndoRedoUserControl : UserControl +{ + public DebugUndoRedoUserControl() + { + InitializeComponent(); + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/EditorDialogService.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/EditorDialogService.cs index 39676e7a47..675987bfe5 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/EditorDialogService.cs +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Services/EditorDialogService.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Avalonia.Controls; +using Stride.Core.Assets.Editor.ViewModels; using Stride.Core.Presentation.Avalonia.Services; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; @@ -10,6 +12,7 @@ namespace Stride.GameStudio.Avalonia.Services; internal class EditorDialogService : DialogService { + private DebugWindow? debugWindow; private readonly IViewModelServiceProvider serviceProvider; public EditorDialogService(IViewModelServiceProvider serviceProvider) @@ -27,4 +30,25 @@ await Dispatcher.InvokeTask(async () => await new AboutWindow().ShowDialog(MainWindow); }); } + + public async Task ShowDebugWindowAsync() + { + await Dispatcher.InvokeAsync(() => + { + if (debugWindow == null) + { + debugWindow = new DebugWindow(new DebugWindowViewModel(serviceProvider)); + debugWindow.Show(); + debugWindow.Closed += (_, _) => debugWindow = null; + } + else + { + if (debugWindow.WindowState == WindowState.Minimized) + { + debugWindow.WindowState = WindowState.Normal; + } + debugWindow.Activate(); + } + }); + } } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/ViewModels/MainViewModel.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/ViewModels/MainViewModel.cs index 052bfcb5af..57dd7f575a 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/ViewModels/MainViewModel.cs +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/ViewModels/MainViewModel.cs @@ -1,8 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Stride.Core.Assets; using Stride.Core.Assets.Editor.ViewModels; using Stride.Core.IO; @@ -21,9 +19,10 @@ internal sealed class MainViewModel : ViewModelBase public MainViewModel(IViewModelServiceProvider serviceProvider) : base(serviceProvider) { - AboutCommand = new AnonymousTaskCommand(ServiceProvider, OnAbout, () => Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime); - ExitCommand = new AnonymousCommand(ServiceProvider, OnExit, () => Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime); - OpenCommand = new AnonymousTaskCommand(ServiceProvider, OnOpen); + AboutCommand = new AnonymousTaskCommand(serviceProvider, OnAbout, () => DialogService.HasMainWindow); + ExitCommand = new AnonymousCommand(serviceProvider, OnExit, () => DialogService.HasMainWindow); + OpenCommand = new AnonymousTaskCommand(serviceProvider, OnOpen); + OpenDebugWindowCommand = new AnonymousTaskCommand(serviceProvider, OnOpenDebugWindow, () => DialogService.HasMainWindow); } public string? Message @@ -46,6 +45,8 @@ public SessionViewModel? Session private EditorDialogService DialogService => ServiceProvider.Get(); + public ICommandBase OpenDebugWindowCommand { get; } + public async Task OpenSession(UFile? filePath, CancellationToken token = default) { if (session != null) @@ -53,14 +54,14 @@ public SessionViewModel? Session if (filePath == null || !File.Exists(filePath)) { - filePath = await ServiceProvider.Get().OpenFilePickerAsync(); + filePath = await DialogService.OpenFilePickerAsync(); } if (filePath == null) return false; var sessionResult = new PackageSessionResult(); var loadedSession = await SessionViewModel.OpenSessionAsync(filePath, sessionResult, ServiceProvider, token); - + // Loading has failed if (loadedSession == null) { @@ -86,4 +87,9 @@ private Task OnOpen() { return OpenSession(null); } + + private async Task OnOpenDebugWindow() + { + await DialogService.ShowDebugWindowAsync(); + } } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml new file mode 100644 index 0000000000..0306bcb17a --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml.cs new file mode 100644 index 0000000000..6c12b9a682 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/DebugWindow.axaml.cs @@ -0,0 +1,40 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia.Controls; +using Avalonia.Input; +using Stride.Core.Assets.Editor.Services; +using Stride.Core.Assets.Editor.ViewModels; + +namespace Stride.GameStudio.Avalonia.Views; + +internal sealed partial class DebugWindow : Window +{ + private readonly DebugWindowViewModel viewModel; + + public DebugWindow(DebugWindowViewModel viewModel) + { + DataContext = this.viewModel = viewModel; + InitializeComponent(); + } + + protected override void OnInitialized() + { + base.OnInitialized(); + EditorDebugService.RegisterDebugWindow(viewModel); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + EditorDebugService.UnregisterDebugWindow(viewModel); + } + + protected override void OnKeyDown(KeyEventArgs e) + { + if (e.Key == Key.Escape) + { + Close(); + } + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml index b7617e15e8..023443a8e1 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/MainView.axaml @@ -18,12 +18,18 @@ - + - + - + + + Date: Wed, 1 Nov 2023 01:09:38 +0100 Subject: [PATCH 218/247] [Presentation] Add package categories --- .../ViewModels/EntityViewModel.cs | 1 + .../UIEditor/ViewModels/UIElementViewModel.cs | 1 + .../ViewModel/CategoryViewModel.cs | 1 + .../ViewModel/DirectoryBaseViewModel.cs | 1 + .../DebugAssetNodeCollectionViewModel.cs | 42 ++++++------ .../ViewModels/SessionViewModel.cs | 62 +++++++++++++++-- .../ViewModels/AssetDependenciesViewModel.cs | 9 +-- .../ViewModels/AssetMountPointViewModel.cs | 4 ++ .../ViewModels/AssetViewModel.cs | 56 +++++++++++++--- .../ViewModels/CategoryViewModel.cs | 53 +++++++++++++++ .../ViewModels/DependencyCategoryViewModel.cs | 26 +++++++ .../DirectDependencyReferenceViewModel.cs | 35 ++++++++++ .../ViewModels/DirectoryViewModel.cs | 13 ++++ .../ViewModels/IChildViewModel.cs | 11 +++ .../ViewModels/ISessionViewModel.cs | 7 ++ .../ViewModels/MountPointViewModel.cs | 19 ++++++ .../ViewModels/PackageCategoryViewModel.cs | 22 ++++++ .../ViewModels/PackageReferenceViewModel.cs | 67 +++++++++++++++++++ .../ViewModels/PackageViewModel.cs | 48 +++++++++++-- .../ViewModels/ProjectCodeViewModel.cs | 2 + .../ViewModels/ProjectViewModel.cs | 4 +- .../ViewModels/SessionObjectViewModel.cs | 50 +++++++++++++- .../ViewModels}/IIsEditableViewModel.cs | 12 ++-- .../Views/SolutionExplorerView.axaml | 15 ++++- 24 files changed, 504 insertions(+), 57 deletions(-) create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/CategoryViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/DependencyCategoryViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectDependencyReferenceViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/IChildViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageCategoryViewModel.cs create mode 100644 sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageReferenceViewModel.cs rename sources/{editor/Stride.Core.Assets.Editor.Wpf/ViewModel => presentation/Stride.Core.Presentation/ViewModels}/IIsEditableViewModel.cs (61%) diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs b/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs index e94102d271..3bd4a4b874 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/EntityHierarchyEditor/ViewModels/EntityViewModel.cs @@ -19,6 +19,7 @@ using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Quantum; using Stride.Core.Presentation.Quantum.Presenters; +using Stride.Core.Presentation.ViewModels; using Stride.Core.Quantum; using Stride.Assets.Entities; using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; diff --git a/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/UIEditor/ViewModels/UIElementViewModel.cs b/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/UIEditor/ViewModels/UIElementViewModel.cs index 2568312e5a..758ca1cba3 100644 --- a/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/UIEditor/ViewModels/UIElementViewModel.cs +++ b/sources/editor/Stride.Assets.Presentation.Wpf/AssetEditors/UIEditor/ViewModels/UIElementViewModel.cs @@ -15,6 +15,7 @@ using Stride.Core.Extensions; using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Quantum; +using Stride.Core.Presentation.ViewModels; using Stride.Core.Quantum; using Stride.Assets.Presentation.AssetEditors.AssetCompositeGameEditor.ViewModels; using Stride.Assets.Presentation.AssetEditors.GameEditor.ViewModels; diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/CategoryViewModel.cs b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/CategoryViewModel.cs index 6154e7c09e..f8716c6a2f 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/CategoryViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/CategoryViewModel.cs @@ -6,6 +6,7 @@ using System.Linq; using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Dirtiables; +using Stride.Core.Presentation.ViewModels; namespace Stride.Core.Assets.Editor.ViewModel { diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/DirectoryBaseViewModel.cs b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/DirectoryBaseViewModel.cs index d8d86967ca..0767afbd77 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/DirectoryBaseViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/DirectoryBaseViewModel.cs @@ -9,6 +9,7 @@ using Stride.Core.Extensions; using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Dirtiables; +using Stride.Core.Presentation.ViewModels; namespace Stride.Core.Assets.Editor.ViewModel { diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs index ffa98dce73..db0f67c779 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/DebugAssetNodeCollectionViewModel.cs @@ -60,32 +60,30 @@ private void RefreshQuantumViewModel() { RefreshNodeToAssetMap(); AssetNodes.Clear(); - // FIXME xplat-editor - //foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) - //{ - // var nodes = GetRegisterNodes(asset.PropertyGraph); - // if (nodes == null) - // continue; - - // var rootNode = new DebugAssetRootNodeViewModel(ServiceProvider, asset.Url, asset.AssetRootNode, nodes); - // AssetNodes.Add(rootNode); - //} + foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) + { + var nodes = GetRegisterNodes(asset.PropertyGraph); + if (nodes == null) + continue; + + var rootNode = new DebugAssetRootNodeViewModel(ServiceProvider, asset.Url, asset.AssetRootNode, nodes); + AssetNodes.Add(rootNode); + } } private void RefreshNodeToAssetMap() { - // FIXME xplat-editor - //foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) - //{ - // var nodes = GetRegisterNodes(asset.PropertyGraph); - // if (nodes == null) - // continue; - - // foreach (var node in nodes) - // { - // NodeToAssetMap[node.Guid] = asset; - // } - //} + foreach (var asset in session.LocalPackages.SelectMany(x => x.Assets)) + { + var nodes = GetRegisterNodes(asset.PropertyGraph); + if (nodes == null) + continue; + + foreach (var node in nodes) + { + NodeToAssetMap[node.Guid] = asset; + } + } } private static HashSet? GetRegisterNodes(AssetPropertyGraph? propertyGraph) diff --git a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs index 51e143f197..002c4fca61 100644 --- a/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Editor/ViewModels/SessionViewModel.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System.Collections.Concurrent; +using System.Collections.Specialized; using Stride.Core.Assets.Analysis; using Stride.Core.Assets.Editor.Services; using Stride.Core.Assets.Presentation.Components.Properties; @@ -10,18 +11,24 @@ using Stride.Core.Diagnostics; using Stride.Core.Extensions; using Stride.Core.IO; +using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Commands; using Stride.Core.Presentation.Quantum.ViewModels; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; +using Stride.Core.Translation; namespace Stride.Core.Assets.Editor.ViewModels; public sealed class SessionViewModel : DispatcherViewModel, ISessionViewModel { + public static readonly string StorePackageCategoryName = "External packages"; // FIXME xplat-editor Tr._("External packages"); + public static readonly string LocalPackageCategoryName = "Local packages"; // FIXME xplat-editor Tr._("Local packages"); + private SessionObjectPropertiesViewModel activeProperties; private readonly ConcurrentDictionary assetIdMap = []; private ProjectViewModel? currentProject; + private readonly Dictionary packageCategories = []; private readonly Dictionary packageMap = []; private readonly PackageSession session; @@ -56,9 +63,18 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi } ActiveProperties = AssetCollection.AssetViewProperties; + + // Construct package categories + var localPackageName = session.SolutionPath != null ? string.Format(Tr._(@"Solution '{0}'"), session.SolutionPath.GetFileNameWithoutExtension()) : LocalPackageCategoryName; + packageCategories.Add(LocalPackageCategoryName, new PackageCategoryViewModel(localPackageName, this)); + packageCategories.Add(StorePackageCategoryName, new PackageCategoryViewModel(StorePackageCategoryName, this)); + LocalPackages.CollectionChanged += LocalPackagesCollectionChanged; // Initialize commands EditSelectedContentCommand = new AnonymousCommand(serviceProvider, OnEditSelectedContent); + + // This event must be subscribed before we create the package view models + PackageCategories.ForEach(x => x.Value.Content.CollectionChanged += PackageCollectionChanged); // Create package view models this.session.Projects.ForEach(x => CreateProjectViewModel(x, true)); @@ -78,7 +94,7 @@ private SessionViewModel(IViewModelServiceProvider serviceProvider, PackageSessi /// /// Gets the currently active . /// - // FIXME: do we need both ActiveProperties and AssetCollection.AssetViewProperties? + // FIXME xplat-editor do we need both ActiveProperties and AssetCollection.AssetViewProperties? public SessionObjectPropertiesViewModel ActiveProperties { get { return activeProperties; } @@ -94,7 +110,7 @@ public SessionObjectPropertiesViewModel ActiveProperties public IEnumerable AllAssets => AllPackages.SelectMany(x => x.Assets); - public IEnumerable AllPackages => packageMap.Keys; + public IEnumerable AllPackages => PackageCategories.Values.SelectMany(x => x.Content); public AssetCollectionViewModel AssetCollection { get; } @@ -124,6 +140,12 @@ private set public AssetPropertyGraphContainer GraphContainer { get; } + public IObservableCollection LocalPackages => PackageCategories[LocalPackageCategoryName].Content; + + public IReadOnlyDictionary PackageCategories => packageCategories; + + public IObservableCollection StorePackages => PackageCategories[StorePackageCategoryName].Content; + public ThumbnailsViewModel Thumbnails { get; } public ICommandBase EditSelectedContentCommand { get; } @@ -187,6 +209,7 @@ private set }, token); + sessionViewModel?.AutoSelectCurrentProject(); return sessionViewModel; } @@ -224,13 +247,22 @@ public void UnregisterAsset(AssetViewModel asset) ((IDictionary)assetIdMap).Remove(asset.Id); } + private void AutoSelectCurrentProject() + { + var currentProject = LocalPackages.OfType().FirstOrDefault(/* FIXME sxplat-editor x => x.Type == ProjectType.Executable && x.Platform == PlatformType.Windows*/) ?? LocalPackages.FirstOrDefault(); + if (currentProject != null) + { + SetCurrentProject(currentProject); + } + } + private PackageViewModel CreateProjectViewModel(PackageContainer packageContainer, bool packageAlreadyInSession) { switch (packageContainer) { case SolutionProject project: { - var packageContainerViewModel = new ProjectViewModel(this, project); + var packageContainerViewModel = new ProjectViewModel(this, project, packageAlreadyInSession); packageMap.Add(packageContainerViewModel, project); if (!packageAlreadyInSession) session.Projects.Add(project); @@ -238,7 +270,7 @@ private PackageViewModel CreateProjectViewModel(PackageContainer packageContaine } case StandalonePackage standalonePackage: { - var packageContainerViewModel = new PackageViewModel(this, standalonePackage); + var packageContainerViewModel = new PackageViewModel(this, standalonePackage, packageAlreadyInSession); packageMap.Add(packageContainerViewModel, standalonePackage); if (!packageAlreadyInSession) session.Projects.Add(standalonePackage); @@ -264,6 +296,28 @@ private void LoadAssetsFromPackages(CancellationToken token = default) using var transaction = ActionService?.CreateTransaction(); ProcessAddedPackages(AllPackages).Forget(); } + + private void LocalPackagesCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + if (e.Action == NotifyCollectionChangedAction.Reset) + { + session.Projects.RemoveWhere(x => !x.Package.IsSystem); + } + if (e.NewItems != null) + { + // When a PackageViewModel is built, we will add it before the Package instance is added to the package map. + // So we can't assume that the view model will always exists in the packageMap. + packageMap.Where(x => e.NewItems.Cast().Contains(x.Key)).ForEach(x => session.Projects.Add(x.Value)); + } + e.OldItems?.Cast().Select(x => packageMap[x]).ForEach(x => session.Projects.Remove(x)); + } + + private void PackageCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // FIXME xplat-editor + //e.NewItems?.Cast().ForEach(x => x.DeletedAssets.CollectionChanged += DeletedAssetChanged); + //e.OldItems?.Cast().ForEach(x => x.DeletedAssets.CollectionChanged -= DeletedAssetChanged); + } private async Task ProcessAddedPackages(IEnumerable packages) { diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs index 21e692a098..4a9a61ac1f 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetDependenciesViewModel.cs @@ -66,8 +66,7 @@ public AssetDependenciesViewModel(AssetViewModel asset, bool forcedRoot) /// public bool IsRoot { - // FIXME xplat-editor - get { return /*!Asset.IsDeleted &&*/ (Session.CurrentProject?.IsInScope(Asset) ?? false) && (ForcedRoot || (Session.CurrentProject?.RootAssets.Contains(Asset) ?? false)); } + get { return !Asset.IsDeleted && (Session.CurrentProject?.IsInScope(Asset) ?? false) && (ForcedRoot || (Session.CurrentProject?.RootAssets.Contains(Asset) ?? false)); } set { if ((Session.CurrentProject?.IsInScope(Asset) ?? false) && !ForcedRoot) @@ -84,7 +83,7 @@ public bool IsRoot /// Gets whether this asset will be compiled as a dependency of an asset that has set to true. /// // FIXME xplat-editor - public bool IsIndirectlyIncluded => !IsRoot /*&& !Asset.IsDeleted*/ && RecursiveReferencerAssets.Any(x => x.Dependencies.IsRoot); + public bool IsIndirectlyIncluded => !IsRoot && !Asset.IsDeleted && RecursiveReferencerAssets.Any(x => x.Dependencies.IsRoot); /// /// Gets whether this asset will be excluded from compilation. @@ -162,9 +161,7 @@ private static void UpdateReferences(ISessionViewModel session) referencerAssets.Clear(); referencedAssets.Clear(); - // FIXME xplat-editor - //if (!asset.IsDeleted) - if (true) + if (!asset.IsDeleted) { var dependencyManager = session.DependencyManager; var dependencies = dependencyManager.ComputeDependencies(asset.AssetItem.Id, AssetDependencySearchOptions.In | AssetDependencySearchOptions.Out, ContentLinkType.Reference); // TODO: Change ContentLinkType.Reference to handle other types diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs index 5959d1f769..1aa9e8d79c 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetMountPointViewModel.cs @@ -10,6 +10,10 @@ public AssetMountPointViewModel(PackageViewModel package) { } + /// + public override bool IsEditable => false; + + /// public override string Name { get => "Assets"; diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs index ef6257c681..f3a82a3dbc 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/AssetViewModel.cs @@ -33,7 +33,7 @@ public abstract class AssetViewModel : SessionObjectViewModel, IAssetPropertyPro private AssetItem assetItem; private DirectoryBaseViewModel directory; private string name; - private ThumbnailData thumbnailData; + private ThumbnailData? thumbnailData; protected AssetViewModel(AssetItem assetItem, DirectoryBaseViewModel directory) : base(directory.Session) @@ -55,21 +55,29 @@ public AssetItem AssetItem set => SetValue(ref assetItem, value); } + public IAssetObjectNode? AssetRootNode => PropertyGraph?.RootNode; + public Type AssetType => AssetItem.Asset.GetType(); public AssetId Id => AssetItem.Id; + /// + /// Gets whether the properties of this asset can be edited. + /// + public override bool IsEditable => Directory?.Package?.IsEditable ?? false; + public DirectoryBaseViewModel Directory { get => directory; private set => SetValue(ref directory, value); } - + /// /// Gets the dependencies of this asset. /// public AssetDependenciesViewModel Dependencies { get; } - + + /// public override string Name { get => name; @@ -77,11 +85,11 @@ public override string Name } public AssetPropertyGraph? PropertyGraph { get; } - + /// /// The associated to this . /// - public ThumbnailData ThumbnailData + public ThumbnailData? ThumbnailData { get => thumbnailData; set => SetValue(ref thumbnailData, value); @@ -90,8 +98,8 @@ public ThumbnailData ThumbnailData /// /// Gets the display name of the type of this asset. /// - public string TypeDisplayName { get { var desc = DisplayAttribute.GetDisplay(AssetType); return desc != null ? desc.Name : AssetType.Name; } } - + public override string TypeDisplayName { get { var desc = DisplayAttribute.GetDisplay(AssetType); return desc != null ? desc.Name : AssetType.Name; } } + /// /// Gets the url of this asset. /// @@ -99,10 +107,8 @@ public ThumbnailData ThumbnailData protected Package Package => Directory.Package.Package; - protected internal IAssetObjectNode? AssetRootNode => PropertyGraph?.RootNode; - protected internal IUndoRedoService? UndoRedoService => ServiceProvider.TryGet(); - + /// /// Initializes this asset. This method is guaranteed to be called once every other assets are loaded in the session. /// @@ -129,6 +135,36 @@ protected virtual GraphNodePath GetPathToPropertiesRootNode() protected virtual bool ShouldConstructPropertyItem(IObjectNode collection, NodeIndex index) => true; protected virtual bool ShouldConstructPropertyMember(IMemberNode member) => true; + + /// + protected override void UpdateIsDeletedStatus() + { + if (IsDeleted) + { + Package.Assets.Remove(AssetItem); + Session.UnregisterAsset(this); + // FIXME xplat-editor + //Directory.Package.DeletedAssetsList.Add(this); + if (PropertyGraph != null) + { + Session.GraphContainer.UnregisterGraph(Id); + } + } + else + { + Package.Assets.Add(AssetItem); + Session.RegisterAsset(this); + // FIXME xplat-editor + //Directory.Package.DeletedAssetsList.Remove(this); + if (/*!Initializing &&*/ PropertyGraph != null) + { + Session.GraphContainer.RegisterGraph(PropertyGraph); + } + } + AssetItem.IsDeleted = IsDeleted; + // FIXME xplat-editor + //Session.SourceTracker?.UpdateAssetStatus(this); + } public static HashSet ComputeRecursiveReferencerAssets(IEnumerable assets) { diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/CategoryViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/CategoryViewModel.cs new file mode 100644 index 0000000000..411ed4fc33 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/CategoryViewModel.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Collections; +using Stride.Core.Presentation.Collections; +using Stride.Core.Presentation.Dirtiables; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public interface ICategoryViewModel : IDirtiable, IIsEditableViewModel +{ + string Name { get; } + + IEnumerable Content { get; } +} + +public abstract class CategoryViewModel : SessionObjectViewModel, ICategoryViewModel +{ + protected CategoryViewModel(string name, ISessionViewModel session, IComparer? childComparer = null) + : base(session) + { + Name = name; + Content = new SortedObservableCollection(childComparer); + } + + public sealed override string Name { get; set; } + + public SortedObservableCollection Content { get; } + + public override bool IsEditable => false; + + IEnumerable ICategoryViewModel.Content => Content; + + public override string TypeDisplayName => "Category"; + + protected override void UpdateIsDeletedStatus() + { + if (IsDeleted) + throw new InvalidOperationException("A category cannot be deleted"); + } +} + +public abstract class CategoryViewModel : CategoryViewModel +{ + protected CategoryViewModel(string name, TParent parent, ISessionViewModel session, IComparer? childComparer = null) + : base(name, session, childComparer) + { + Parent = parent; + } + + public TParent Parent { get; } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DependencyCategoryViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DependencyCategoryViewModel.cs new file mode 100644 index 0000000000..d28c6f1ee0 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DependencyCategoryViewModel.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Presentation.Dirtiables; + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public class DependencyCategoryViewModel : CategoryViewModel, IChildViewModel +{ + public DependencyCategoryViewModel(string name, PackageViewModel parent, ISessionViewModel session, RootAssetCollection packageRootAssets, IComparer? childComparer = null) + : base(name, parent, session, childComparer) + { + } + + public override IEnumerable Dirtiables => base.Dirtiables.Concat(Parent.Dirtiables); + + IChildViewModel IChildViewModel.GetParent() + { + return Parent; + } + + string IChildViewModel.GetName() + { + return Name; + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectDependencyReferenceViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectDependencyReferenceViewModel.cs new file mode 100644 index 0000000000..9284cf9967 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectDependencyReferenceViewModel.cs @@ -0,0 +1,35 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public class DirectDependencyReferenceViewModel : PackageReferenceViewModel +{ + private readonly DependencyRange dependency; + + public DirectDependencyReferenceViewModel(DependencyRange dependency, PackageViewModel referencer, DependencyCategoryViewModel dependencies, bool canUndoRedoCreation) + : base(referencer, dependencies) + { + this.dependency = dependency; + InitialUndelete(canUndoRedoCreation); + } + + public override string Name + { + get => dependency.Name; + set => throw new InvalidOperationException("The name of a package reference cannot be set"); + } + + public override void AddReference() + { + if (!Referencer.Package.Container.DirectDependencies.Contains(dependency)) + { + Referencer.Package.Container.DirectDependencies.Add(dependency); + } + } + + public override void RemoveReference() + { + Referencer.Package.Container.DirectDependencies.Remove(dependency); + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryViewModel.cs index 4a7577d787..6b68291457 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/DirectoryViewModel.cs @@ -15,6 +15,11 @@ public DirectoryViewModel(string name, DirectoryBaseViewModel parent) this.parent = parent; } + /// + /// Gets whether this directory is editable. + /// + public override bool IsEditable => Package.IsEditable; + /// /// Gets the package containing this directory. /// @@ -45,4 +50,12 @@ public override string Name /// public override MountPointViewModel Root => Parent.Root; + + /// + public override string TypeDisplayName => "Folder"; + + protected override void UpdateIsDeletedStatus() + { + throw new NotImplementedException(); + } } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IChildViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IChildViewModel.cs new file mode 100644 index 0000000000..b9cef766c7 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/IChildViewModel.cs @@ -0,0 +1,11 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public interface IChildViewModel +{ + string GetName(); + + IChildViewModel? GetParent(); +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs index b8fad8fa3a..8f03e647c0 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ISessionViewModel.cs @@ -4,6 +4,7 @@ using Stride.Core.Assets.Analysis; using Stride.Core.Assets.Presentation.Components.Properties; using Stride.Core.Assets.Quantum; +using Stride.Core.Presentation.Collections; using Stride.Core.Presentation.Services; using Stride.Core.Presentation.ViewModels; @@ -27,8 +28,14 @@ public interface ISessionViewModel AssetPropertyGraphContainer GraphContainer { get; } + IObservableCollection LocalPackages { get; } + + IReadOnlyDictionary PackageCategories { get; } + IViewModelServiceProvider ServiceProvider { get; } + IObservableCollection StorePackages { get; } + event EventHandler? AssetPropertiesChanged; event EventHandler? SessionStateChanged; diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs index c20a4fbb65..ae25819d8e 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/MountPointViewModel.cs @@ -1,6 +1,8 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. +using Stride.Core.Presentation.Dirtiables; + namespace Stride.Core.Assets.Presentation.ViewModels; public abstract class MountPointViewModel : DirectoryBaseViewModel @@ -11,21 +13,38 @@ protected MountPointViewModel(PackageViewModel package) Package = package; } + /// + public override IEnumerable Dirtiables => base.Dirtiables.Concat(Package.Dirtiables); + + /// public override PackageViewModel Package { get; } + /// public override DirectoryBaseViewModel Parent { get => null!; set => throw new InvalidOperationException($"Cannot change the parent of a {nameof(MountPointViewModel)}"); } + /// public override string Name { get => string.Empty; set => throw new InvalidOperationException($"Cannot change the name of a {nameof(MountPointViewModel)}"); } + /// public override string Path => string.Empty; + /// public override MountPointViewModel Root => this; + + /// + public override string TypeDisplayName => "Mount Point"; + + /// + protected override void UpdateIsDeletedStatus() + { + throw new NotImplementedException(); + } } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageCategoryViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageCategoryViewModel.cs new file mode 100644 index 0000000000..3a7d258836 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageCategoryViewModel.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Core.Assets.Presentation.ViewModels; + +public class PackageCategoryViewModel : CategoryViewModel, IChildViewModel +{ + public PackageCategoryViewModel(string name, ISessionViewModel session, IComparer childComparer = null) + : base(name, session, childComparer) + { + } + + IChildViewModel? IChildViewModel.GetParent() + { + return null; + } + + string IChildViewModel.GetName() + { + return Name; + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageReferenceViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageReferenceViewModel.cs new file mode 100644 index 0000000000..8b955b7b63 --- /dev/null +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageReferenceViewModel.cs @@ -0,0 +1,67 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Stride.Core.Extensions; +using Stride.Core.Presentation.Dirtiables; + +namespace Stride.Core.Assets.Presentation.ViewModels; + +/// +/// Abstract base class that represents a package being referenced by another one. +/// +public abstract class PackageReferenceViewModel : SessionObjectViewModel, IComparable +{ + private readonly DependencyCategoryViewModel dependencies; + + protected PackageReferenceViewModel(PackageViewModel referencer, DependencyCategoryViewModel dependencies) + : base(referencer.SafeArgument(nameof(referencer)).Session) + { + this.dependencies = dependencies; + Referencer = referencer; + } + + /// + /// Gets the referencer package of this package reference. + /// + public PackageViewModel Referencer { get; } + + /// + /// Gets the target package of this package reference. + /// + public PackageViewModel? Target { get; protected set; } + + public override string TypeDisplayName => "Package Reference"; + + public override IEnumerable Dirtiables => dependencies.Dirtiables; + + public override bool IsEditable => Referencer.IsEditable; + + /// + public int CompareTo(PackageReferenceViewModel? other) + { + return other != null ? string.Compare(Name, other.Name, StringComparison.InvariantCultureIgnoreCase) : -1; + } + + public abstract void AddReference(); + + public abstract void RemoveReference(); + + public void Delete() + { + IsDeleted = true; + } + + protected override void UpdateIsDeletedStatus() + { + if (IsDeleted) + { + dependencies.Content.Remove(this); + RemoveReference(); + } + else + { + dependencies.Content.Add(this); + AddReference(); + } + } +} diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs index 5d08f2ebe7..a610ba33cb 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/PackageViewModel.cs @@ -9,18 +9,22 @@ namespace Stride.Core.Assets.Presentation.ViewModels; -public class PackageViewModel : SessionObjectViewModel, IComparable +public class PackageViewModel : SessionObjectViewModel, IComparable, IChildViewModel { // FIXME should only contain editable viewmodels protected readonly SortedObservableCollection content = new(ComparePackageContent); - public PackageViewModel(ISessionViewModel session, PackageContainer packageContainer) + public PackageViewModel(ISessionViewModel session, PackageContainer packageContainer, bool packageAlreadyInSession) : base(session) { AssetMountPoint = new AssetMountPointViewModel(this); PackageContainer = packageContainer; content.Add(AssetMountPoint); + IsLoaded = Package.State >= PackageState.AssetsReady; + + // IsDeleted will make the package added to Session.LocalPackages, so let's do it last + InitialUndelete(!packageAlreadyInSession); } /// @@ -39,6 +43,11 @@ public PackageViewModel(ISessionViewModel session, PackageContainer packageConta /// This collection usually contains categories and root folders. public IReadOnlyObservableCollection Content => content; + /// + /// Gets whether this package is editable. + /// + public override bool IsEditable => !Package.IsSystem && IsLoaded; + public IEnumerable MountPoints => Content.OfType(); /// @@ -51,6 +60,8 @@ public override string Name set { } // TODO rename } + public bool IsLoaded { get; } + /// /// Gets the underlying used as a model for this view. /// @@ -67,7 +78,7 @@ public UFile PackagePath get => Package.FullPath; set => SetValue(() => Package.FullPath = value); } - + /// /// Gets the collection of root assets for this package. /// @@ -75,6 +86,9 @@ public UFile PackagePath public UDirectory RootDirectory => Package.RootDirectory; + /// + public override string TypeDisplayName => "Package"; + /// public int CompareTo(PackageViewModel? other) { @@ -122,7 +136,7 @@ public void LoadPackageInformation(CancellationToken token = default) GetOrCreateAssetDirectory(explicitDirectory); } } - + /// /// Indicates whether the given asset in within the scope of this package, either by being part of this package or part of /// one of its dependencies. @@ -136,6 +150,20 @@ public bool IsInScope(AssetViewModel asset) return assetPackage == this || Package.Container.FlattenedDependencies.Any(x => x.Package == assetPackage.Package); } + protected override void UpdateIsDeletedStatus() + { + var collection = Package.IsSystem ? Session.StorePackages : Session.LocalPackages; + + if (IsDeleted) + { + collection.Remove(this); + } + else + { + collection.Add(this); + } + } + private static int ComparePackageContent(ViewModelBase x, ViewModelBase y) { if (x is AssetMountPointViewModel xAssets) @@ -166,7 +194,7 @@ private AssetViewModel CreateAsset(AssetItem assetItem, DirectoryBaseViewModel d } return (AssetViewModel)Activator.CreateInstance(assetViewModelType, assetItem, directory)!; } - + private void FillRootAssetCollection() { RootAssets.Clear(); @@ -177,4 +205,14 @@ private void FillRootAssetCollection() RootAssets.AddRange(dependency.Package.RootAssets.Select(x => Session.GetAssetById(x.Id)).NotNull()!); } } + + IChildViewModel IChildViewModel.GetParent() + { + return Session.PackageCategories.Values.First(x => x.Content.Contains(this)); + } + + string IChildViewModel.GetName() + { + return Name; + } } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs index b989adca25..4d308e0ee6 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectCodeViewModel.cs @@ -10,6 +10,8 @@ public ProjectCodeViewModel(ProjectViewModel package) { } + public override bool IsEditable => false; + public override string Name { get => "Code"; diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectViewModel.cs index 938474c892..66b14a9762 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/ProjectViewModel.cs @@ -7,8 +7,8 @@ namespace Stride.Core.Assets.Presentation.ViewModels; public sealed class ProjectViewModel : PackageViewModel { - public ProjectViewModel(ISessionViewModel session, SolutionProject project) - : base(session, project) + public ProjectViewModel(ISessionViewModel session, SolutionProject project, bool packageAlreadyInSession) + : base(session, project, packageAlreadyInSession) { content.Add(Code = new ProjectCodeViewModel(this)); } diff --git a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionObjectViewModel.cs b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionObjectViewModel.cs index e9a92b03d3..2f83162651 100644 --- a/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionObjectViewModel.cs +++ b/sources/editor/Stride.Core.Assets.Presentation/ViewModels/SessionObjectViewModel.cs @@ -5,18 +5,66 @@ namespace Stride.Core.Assets.Presentation.ViewModels; -public abstract class SessionObjectViewModel : DispatcherViewModel +public abstract class SessionObjectViewModel : DirtiableEditableViewModel, IIsEditableViewModel { + private bool isEditing; + private bool isDeleted = true; + protected SessionObjectViewModel(ISessionViewModel session) : base(session.ServiceProvider) { Session = session; } + /// + /// Gets whether this object is currently deleted. + /// + public bool IsDeleted { get { return isDeleted; } set { SetValue(ref isDeleted, value, UpdateIsDeletedStatus); } } + + /// + /// Gets whether this object is editable. + /// + public abstract bool IsEditable { get; } + + /// + /// Gets or sets whether this package is being edited in the view. + /// + public virtual bool IsEditing { get { return isEditing; } set { if (IsEditable) SetValueUncancellable(ref isEditing, value); } } + public abstract string Name { get; set; } /// /// Gets the session in which this object is currently in. /// public ISessionViewModel Session { get; } + + /// + /// Gets the display name of the type of this . + /// + public abstract string TypeDisplayName { get; } + + /// + /// Marks this view model as undeleted. + /// + /// Indicates whether a transaction should be created when doing this operation. + /// + /// This method is intended to be called from constructors, to allow the creation of this view model + /// to be undoable or not. + /// + protected void InitialUndelete(bool canUndoRedoCreation) + { + if (canUndoRedoCreation) + { + SetValue(ref isDeleted, false, UpdateIsDeletedStatus, nameof(IsDeleted)); + } + else + { + SetValueUncancellable(ref isDeleted, false, UpdateIsDeletedStatus, nameof(IsDeleted)); + } + } + + /// + /// Updates related session objects when the property changes. + /// + protected abstract void UpdateIsDeletedStatus(); } diff --git a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/IIsEditableViewModel.cs b/sources/presentation/Stride.Core.Presentation/ViewModels/IIsEditableViewModel.cs similarity index 61% rename from sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/IIsEditableViewModel.cs rename to sources/presentation/Stride.Core.Presentation/ViewModels/IIsEditableViewModel.cs index 303a6cc1af..8fbf5e5d5c 100644 --- a/sources/editor/Stride.Core.Assets.Editor.Wpf/ViewModel/IIsEditableViewModel.cs +++ b/sources/presentation/Stride.Core.Presentation/ViewModels/IIsEditableViewModel.cs @@ -1,11 +1,11 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -namespace Stride.Core.Assets.Editor.ViewModel + +namespace Stride.Core.Presentation.ViewModels; + +public interface IIsEditableViewModel { - public interface IIsEditableViewModel - { - bool IsEditable { get; } + bool IsEditable { get; } - bool IsEditing { get; set; } - } + bool IsEditing { get; set; } } diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/SolutionExplorerView.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/SolutionExplorerView.axaml index 1a436a8bb0..e47861a772 100644 --- a/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/SolutionExplorerView.axaml +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia/Views/SolutionExplorerView.axaml @@ -11,7 +11,7 @@ - @@ -22,6 +22,13 @@ + + + + + + @@ -43,6 +50,12 @@ + + + + + + From 9c231a6ecab57e36ecfa392de522f56a5356346b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Wed, 1 Nov 2023 15:36:58 +0100 Subject: [PATCH 219/247] [GameStudio] Crash report --- .../Stride.Editor.Avalonia.csproj | 4 +- .../Assets/CrashReportImage.png | 3 + .../Crash/CrashReportArgs.cs | 18 ++ .../Crash/CrashReportViewModel.cs | 176 ++++++++++++++++++ .../Crash/CrashReportWindow.axaml | 70 +++++++ .../Crash/CrashReportWindow.axaml.cs | 20 ++ .../Program.cs | 110 ++++++++++- .../Stride.GameStudio.Avalonia.Desktop.csproj | 7 +- .../Assets/GameStudio.ico | Bin 0 -> 56408 bytes .../Assets/avalonia-logo.ico | Bin 176111 -> 0 bytes .../ViewModels/MainViewModel.cs | 7 + .../Views/MainView.axaml | 4 + .../Views/MainWindow.axaml | 4 +- .../Views/MainWindow.axaml.cs | 3 + 14 files changed, 417 insertions(+), 9 deletions(-) create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Assets/CrashReportImage.png create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportArgs.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportViewModel.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportWindow.axaml create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportWindow.axaml.cs create mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Assets/GameStudio.ico delete mode 100644 sources/xplat-editor/Stride.GameStudio.Avalonia/Assets/avalonia-logo.ico diff --git a/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj index d50b198fb1..f545fe1298 100644 --- a/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj +++ b/sources/xplat-editor/Stride.Editor.Avalonia/Stride.Editor.Avalonia.csproj @@ -1,10 +1,10 @@ - + - + \ No newline at end of file diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Assets/CrashReportImage.png b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Assets/CrashReportImage.png new file mode 100644 index 0000000000..b1bf3de6f6 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Assets/CrashReportImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36917dc9595f57e66f7d9692cd77fc19e8cc0f7eb31d76fe117d877ad72b2d31 +size 3019 diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportArgs.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportArgs.cs new file mode 100644 index 0000000000..ceca069046 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportArgs.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.GameStudio.Avalonia.Desktop.Crash; + +internal enum CrashLocation +{ + Main, + UnhandledException +} + +internal class CrashReportArgs +{ + public Exception Exception; + public CrashLocation Location; + public string[] Logs; + public string? ThreadName; +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportViewModel.cs b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportViewModel.cs new file mode 100644 index 0000000000..deb50d2134 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportViewModel.cs @@ -0,0 +1,176 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using System.Text; +using Stride.Core.Extensions; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; +using Stride.CrashReport; + +namespace Stride.GameStudio.Avalonia.Desktop.Crash; + +internal sealed class CrashReportViewModel : ViewModelBase +{ + public const string PrivacyPolicyUrl = "https://stride3d.net/legal/privacy-policy"; + + private readonly NullDispatcherService dispatcherService = new(); + + private readonly Func setClipboard; + private readonly CancellationTokenSource exitToken; + private readonly CrashReportData report; + + private bool isReportVisible; + private bool rememberEmail; + + public CrashReportViewModel(CrashReportArgs args, Func setClipboard, CancellationTokenSource exitToken) + : base(new ViewModelServiceProvider()) + { + this.exitToken = exitToken; + this.setClipboard = setClipboard; + ServiceProvider.RegisterService(dispatcherService); + + report = ComputeReport(args); + + CopyReportCommand = new AnonymousTaskCommand(ServiceProvider, OnCopyReport); + DontSendCommand = new AnonymousCommand(ServiceProvider, OnDontSend); + OpenPrivacyPolicyCommand = new AnonymousCommand(ServiceProvider, OnOpenPrivacyPolicy); +#if DEBUG + SendCommand = new AnonymousTaskCommand(ServiceProvider, OnSend); +#else + SendCommand = DisabledCommand.Instance; +#endif + ViewReportCommand = new AnonymousCommand(ServiceProvider, OnViewReport); + } + + public string? Description + { + get => report["UserMessage"]; + set => SetValue(() => report["UserMessage"] = value, nameof(Description), nameof(Report)); + } + + public string? EmailAddress + { + get => report["UserEmail"]; + set => SetValue(() => report["UserEmail"] = value, nameof(EmailAddress), nameof(Report)); + } + + public bool IsReportVisible + { + get => isReportVisible; + set => SetValue(ref isReportVisible, value); + } + + public CrashReportData Report + { + get => report; + } + + public bool RememberEmail + { + get => rememberEmail; + set => SetValue(ref rememberEmail, value); + } + + public ICommandBase CopyReportCommand { get; } + public ICommandBase DontSendCommand { get; } + public ICommandBase OpenPrivacyPolicyCommand { get; } + public ICommandBase SendCommand { get; } + public ICommandBase ViewReportCommand { get; } + + private void Close() + { + exitToken.Cancel(); + } + + private Task OnCopyReport() + { + return setClipboard.Invoke(Report.ToJson()); + } + + private void OnDontSend() + { + Close(); + } + + private void OnOpenPrivacyPolicy() + { + try + { + var psi = new ProcessStartInfo + { + FileName = PrivacyPolicyUrl, + UseShellExecute = true + }; + Process.Start(psi); + } + // FIXME: catch only specific exceptions? + catch (Exception) + { + var error = "An error occurred while opening the browser. You can access the privacy policy at the following url:" + + Environment.NewLine + Environment.NewLine + PrivacyPolicyUrl; + // TODO: display error + } + } + + private async Task OnSend() + { + if (!await SendReport(Report)) + { + // TODO: display error + } + + Close(); + } + + private void OnViewReport() + { + IsReportVisible = true; + } + + private static CrashReportData ComputeReport(CrashReportArgs args) + { + return new CrashReportData + { + ["Application"] = "GameStudio", + ["UserEmail"] = "", + ["UserMessage"] = "", + ["StrideVersion"] = StrideVersion.NuGetVersion, + ["ThreadName"] = args.ThreadName, +#if DEBUG + ["CrashLocation"] = args.Location.ToString(), + ["ProcessID"] = Process.GetCurrentProcess().Id.ToString(), +#endif + ["CurrentDirectory"] = Environment.CurrentDirectory, + ["OsArch"] = Environment.Is64BitOperatingSystem ? "x64" : "x86", + ["ProcessorCount"] = Environment.ProcessorCount.ToString(), + ["Exception"] = args.Exception.FormatFull(), + ["LastLogs"] = FormatLogs(args.Logs), + }; + + static string FormatLogs(string[] logs) + { + var builder = new StringBuilder(); + for (var i = 0; i < logs.Length; i++) + { + var log = logs[i]; + builder.AppendLine($"{i + 1}: {log}"); + } + return builder.ToString(); + } + } + + private static async Task SendReport(CrashReportData report) + { + try + { + await CrashReporter.Report(report); + return true; + } + catch (Exception) + { + return false; + } + } +} diff --git a/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportWindow.axaml b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportWindow.axaml new file mode 100644 index 0000000000..32dc332ec3 --- /dev/null +++ b/sources/xplat-editor/Stride.GameStudio.Avalonia.Desktop/Crash/CrashReportWindow.axaml @@ -0,0 +1,70 @@ + + + + + + + + + + Unfortunately, Game Studio has crashed. + Please help us improve Stride by sending information about this crash. + + If you'd like to allow us to ask some questions about the crash, + please enter your email (optional, we will never send you an email for any other reason). + + + + If you have time, please describe what you were doing during the crash: + + + + Email: (optional) + + + Remember my email + + Privacy: you can see exactly what will be sent to us by pressing the 'View Report' button. + We do not collect anything else. + + By sending this report you accept our Privacy Policy. + + +