feat(command-queue): apply global asset changes to all loaded files (#12954) cfd611f559 Co-authored-by: David Skuza <david@rive.app>
diff --git a/.rive_head b/.rive_head index 493ef2f..625d3cb 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -11ef1543e1e70032221a9ff631cf06df2292a2d8 +cfd611f559c885a535c5c322eafc8a4ae0801d13
diff --git a/include/rive/command_queue.hpp b/include/rive/command_queue.hpp index dd9fc74..f0615e7 100644 --- a/include/rive/command_queue.hpp +++ b/include/rive/command_queue.hpp
@@ -475,18 +475,93 @@ void deleteFile(FileHandle, uint64_t requestId = 0); + /** + * Registers a global image asset that provides the contents for any file's + * out-of-band image asset with the same unique name. + * + * The change reaches files that are already open as well as files loaded + * afterward. Registering a name that is already registered replaces its + * resource everywhere. + * + * Assets with embedded contents and assets claimed by a host-supplied file + * asset loader are never overridden. Reports an error on the matching + * asset listener if the handle is invalid. + * + * @param name The unique asset name to register. + * @param handle The decoded image resource to provide. + * @param requestId The request identifier used for an error callback. + */ void addGlobalImageAsset(std::string name, - RenderImageHandle, + RenderImageHandle handle, uint64_t requestId = 0); + /** + * Registers a global font asset that provides the contents for any file's + * out-of-band font asset with the same unique name. + * + * The change reaches files that are already open as well as files loaded + * afterward. Registering a name that is already registered replaces its + * resource everywhere. + * + * Assets with embedded contents and assets claimed by a host-supplied file + * asset loader are never overridden. Reports an error on the matching + * asset listener if the handle is invalid. + * + * @param name The unique asset name to register. + * @param handle The decoded font resource to provide. + * @param requestId The request identifier used for an error callback. + */ void addGlobalFontAsset(std::string name, - FontHandle, + FontHandle handle, uint64_t requestId = 0); + /** + * Registers a global audio asset that provides the contents for any file's + * out-of-band audio asset with the same unique name. + * + * The change reaches files that are already open as well as files loaded + * afterward. Registering a name that is already registered replaces its + * resource everywhere. + * + * Assets with embedded contents and assets claimed by a host-supplied file + * asset loader are never overridden. Reports an error on the matching + * asset listener if the handle is invalid. + * + * @param name The unique asset name to register. + * @param handle The decoded audio resource to provide. + * @param requestId The request identifier used for an error callback. + */ void addGlobalAudioAsset(std::string name, - AudioSourceHandle, + AudioSourceHandle handle, uint64_t requestId = 0); + /** + * Unregisters the global image asset with the given unique name, clearing + * it from every open file it was applied to. Deleting the backing image + * resource has the same effect for every name that resource was registered + * under. + * + * @param name The unique asset name to unregister. + * @param requestId The request identifier. + */ void removeGlobalImageAsset(std::string name, uint64_t requestId = 0); + /** + * Unregisters the global font asset with the given unique name, clearing it + * from every open file it was applied to. Deleting the backing font + * resource has the same effect for every name that resource was registered + * under. + * + * @param name The unique asset name to unregister. + * @param requestId The request identifier. + */ void removeGlobalFontAsset(std::string name, uint64_t requestId = 0); + /** + * Unregisters the global audio asset with the given unique name, clearing + * it from every open file it was applied to. Deleting the backing audio + * resource has the same effect for every name that resource was registered + * under. + * + * @param name The unique asset name to unregister. + * @param requestId The request identifier. + */ void removeGlobalAudioAsset(std::string name, uint64_t requestId = 0); ArtboardHandle instantiateArtboardNamed(
diff --git a/include/rive/command_server.hpp b/include/rive/command_server.hpp index 23d2591..6c1fe17 100644 --- a/include/rive/command_server.hpp +++ b/include/rive/command_server.hpp
@@ -16,13 +16,28 @@ namespace rive { class ScriptingContext; +class GlobalAssetRegistry; + // Server-side worker that executes commands from a CommandQueue. class CommandServer { public: - CommandServer(rcp<CommandQueue>, - Factory*, - rcp<rive::FileAssetLoader> = nullptr); + /** + * Creates the server that executes commands from commandQueue. + * + * @param commandQueue The queue from which commands are read. + * @param factory The factory used to create runtime resources. + * @param internalFileAssetLoader An optional FileAssetLoader for loading + * out-of-band asset contents without using command-queue global assets. + * Provide a loader that recognizes and loads the assets it owns, returning + * true from FileAssetLoader::loadContents for those assets. This is + * typically used by an embedder that already has its own asset-loading + * path. Leave it null when out-of-band images, audio, and fonts should be + * handled through the command queue. + */ + CommandServer(rcp<CommandQueue> commandQueue, + Factory* factory, + rcp<rive::FileAssetLoader> internalFileAssetLoader = nullptr); virtual ~CommandServer(); Factory* factory() const { return m_factory; } @@ -249,6 +264,7 @@ std::unordered_map<DrawKey, CommandServerDrawCallback> m_uniqueDraws; class CommandFileAssetLoader; - rcp<CommandFileAssetLoader> m_fileAssetLoader; + rcp<GlobalAssetRegistry> m_globalAssetRegistry; + rcp<FileAssetLoader> m_internalFileAssetLoader; }; }; // namespace rive
diff --git a/src/command_server.cpp b/src/command_server.cpp index 05c52a6..178b561 100644 --- a/src/command_server.cpp +++ b/src/command_server.cpp
@@ -22,176 +22,422 @@ namespace rive { +/** + * Stores registered resource handles and matching assets in loaded files for + * one file asset type. + * + * Entries are keyed by unique asset name. Each entry contains the one resource + * handle registered for that name and a map of the loaded files containing a + * matching out-of-band file asset. Registering a resource and applying it to + * those file assets are intentionally separate operations. + */ +template <typename TResourceHandle, typename TFileAsset> +class TypedGlobalAssetRegistry +{ +public: + /** Adds a matching file asset after its file imports successfully. */ + void addFileAsset(FileHandle fileHandle, TFileAsset* asset) + { + m_entries[asset->uniqueName()].assetsByFileHandle[fileHandle] = + ref_rcp(asset); + } + + /** Removes a file asset when its containing file is deleted. */ + void removeFileAsset(FileHandle fileHandle, const std::string& name) + { + auto itr = m_entries.find(name); + if (itr == m_entries.end()) + { + return; + } + itr->second.assetsByFileHandle.erase(fileHandle); + eraseIfUnused(itr); + } + + /** Returns the resource registered for a name, or the null handle. */ + TResourceHandle getResourceHandle(const std::string& name) const + { + auto itr = m_entries.find(name); + return itr == m_entries.end() ? RIVE_NULL_HANDLE + : itr->second.resourceHandle; + } + + /** Sets the resource for a name without applying it to file assets. */ + void setResource(const std::string& name, TResourceHandle handle) + { + m_entries[name].resourceHandle = handle; + } + + /** Unregisters the resource for a name without clearing file assets. */ + void unregisterResourceByName(const std::string& name) + { + auto itr = m_entries.find(name); + if (itr == m_entries.end()) + { + return; + } + itr->second.resourceHandle = RIVE_NULL_HANDLE; + eraseIfUnused(itr); + } + + /** + * Unregisters every name backed by a resource handle. + * + * Returns the names that still have loaded file assets and therefore need + * to have the null resource applied separately. + */ + std::vector<std::string> unregisterResourceByHandle(TResourceHandle handle) + { + std::vector<std::string> names; + for (auto itr = m_entries.begin(); itr != m_entries.end();) + { + if (itr->second.resourceHandle != handle) + { + ++itr; + continue; + } + + itr->second.resourceHandle = RIVE_NULL_HANDLE; + if (itr->second.empty()) + { + itr = m_entries.erase(itr); + } + else + { + names.push_back(itr->first); + ++itr; + } + } + return names; + } + + /** Applies an operation to every loaded file asset matching a name. */ + template <typename Apply> + void applyToFileAssets(const std::string& name, Apply&& apply) const + { + auto itr = m_entries.find(name); + if (itr == m_entries.end()) + { + return; + } + + for (const auto& fileAsset : itr->second.assetsByFileHandle) + { + apply(*fileAsset.second); + } + } + +private: + struct Entry + { + TResourceHandle resourceHandle = RIVE_NULL_HANDLE; + std::unordered_map<FileHandle, rcp<TFileAsset>> assetsByFileHandle; + + bool empty() const + { + return resourceHandle == RIVE_NULL_HANDLE && + assetsByFileHandle.empty(); + } + }; + + using Entries = std::unordered_map<std::string, Entry>; + + void eraseIfUnused(typename Entries::iterator itr) + { + if (itr->second.empty()) + { + m_entries.erase(itr); + } + } + + Entries m_entries; +}; + +/** + * Maintains global resource registrations and the loaded out-of-band file + * assets that consume them. + * + * One typed registry is maintained for images, audio sources, and fonts. This + * class adds and removes file assets, registers and unregisters resource + * handles, resolves them from read-only references to CommandServer's resource + * maps, and applies the resources to matching loaded file assets. + * + * It does not own decoded resources. CommandServer owns those resources. + * + * Resource registration and application remain separate so callers control + * when each operation occurs. + */ +class GlobalAssetRegistry : public RefCnt<GlobalAssetRegistry> +{ +public: + GlobalAssetRegistry( + const std::unordered_map<RenderImageHandle, rcp<RenderImage>>& images, + const std::unordered_map<AudioSourceHandle, rcp<AudioSource>>& + audioSources, + const std::unordered_map<FontHandle, rcp<Font>>& fonts) : + m_images(images), m_audioSources(audioSources), m_fonts(fonts) + {} + + /** Adds successfully imported out-of-band file assets to the registry. */ + void addFileAssets(FileHandle fileHandle, Span<const rcp<FileAsset>> assets) + { + for (const auto& asset : assets) + { + if (asset->is<ImageAsset>()) + { + m_imageRegistry.addFileAsset(fileHandle, + asset->as<ImageAsset>()); + } + else if (asset->is<AudioAsset>()) + { + m_audioRegistry.addFileAsset(fileHandle, + asset->as<AudioAsset>()); + } + else if (asset->is<FontAsset>()) + { + m_fontRegistry.addFileAsset(fileHandle, asset->as<FontAsset>()); + } + } + } + + /** Applies registered resources to newly imported file assets. */ + void applyResourcesToFileAssets(Span<const rcp<FileAsset>> fileAssets) const + { + for (const auto& asset : fileAssets) + { + if (asset->is<ImageAsset>()) + { + auto handle = renderImageHandle(asset->uniqueName()); + asset->as<ImageAsset>()->renderImage( + getResource(m_images, handle)); + } + else if (asset->is<AudioAsset>()) + { + auto handle = audioSourceHandle(asset->uniqueName()); + asset->as<AudioAsset>()->audioSource( + getResource(m_audioSources, handle)); + } + else if (asset->is<FontAsset>()) + { + auto handle = fontHandle(asset->uniqueName()); + asset->as<FontAsset>()->font(getResource(m_fonts, handle)); + } + } + } + + RenderImageHandle renderImageHandle(const std::string& name) const + { + return m_imageRegistry.getResourceHandle(name); + } + + AudioSourceHandle audioSourceHandle(const std::string& name) const + { + return m_audioRegistry.getResourceHandle(name); + } + + FontHandle fontHandle(const std::string& name) const + { + return m_fontRegistry.getResourceHandle(name); + } + + /** Sets the resource for a name. The caller applies it separately. */ + void setRenderImage(const std::string& name, RenderImageHandle handle) + { + m_imageRegistry.setResource(name, handle); + } + + void setAudioSource(const std::string& name, AudioSourceHandle handle) + { + m_audioRegistry.setResource(name, handle); + } + + void setFont(const std::string& name, FontHandle handle) + { + m_fontRegistry.setResource(name, handle); + } + + /** + * Unregisters the resource for a name. The caller applies the null resource + * separately. + */ + void removeRenderImage(const std::string& name) + { + m_imageRegistry.unregisterResourceByName(name); + } + + void removeAudioSource(const std::string& name) + { + m_audioRegistry.unregisterResourceByName(name); + } + + void removeFont(const std::string& name) + { + m_fontRegistry.unregisterResourceByName(name); + } + + void applyRenderImage(const std::string& name, + const rcp<RenderImage>& image) const + { + m_imageRegistry.applyToFileAssets(name, [&image](ImageAsset& asset) { + asset.renderImage(image); + }); + } + + void applyAudioSource(const std::string& name, + const rcp<AudioSource>& audioSource) const + { + m_audioRegistry.applyToFileAssets(name, + [&audioSource](AudioAsset& asset) { + asset.audioSource(audioSource); + }); + } + + void applyFont(const std::string& name, const rcp<Font>& font) const + { + m_fontRegistry.applyToFileAssets(name, [&font](FontAsset& asset) { + asset.font(font); + }); + } + + /** + * Unregisters every name backed by a deleted resource handle. The caller + * applies each returned name separately. + */ + std::vector<std::string> removeRenderImage(RenderImageHandle handle) + { + return m_imageRegistry.unregisterResourceByHandle(handle); + } + + std::vector<std::string> removeAudioSource(AudioSourceHandle handle) + { + return m_audioRegistry.unregisterResourceByHandle(handle); + } + + std::vector<std::string> removeFont(FontHandle handle) + { + return m_fontRegistry.unregisterResourceByHandle(handle); + } + + /** + * Removes a deleted file from each registry entry named by an asset in that + * loaded Rive file. No file-to-assets reverse registry is needed. + */ + void removeFileAssets(FileHandle fileHandle, + Span<const rcp<FileAsset>> assets) + { + for (const auto& asset : assets) + { + if (asset->is<ImageAsset>()) + { + m_imageRegistry.removeFileAsset(fileHandle, + asset->uniqueName()); + } + else if (asset->is<AudioAsset>()) + { + m_audioRegistry.removeFileAsset(fileHandle, + asset->uniqueName()); + } + else if (asset->is<FontAsset>()) + { + m_fontRegistry.removeFileAsset(fileHandle, asset->uniqueName()); + } + } + } + +#ifdef TESTING + RenderImageHandle testing_imageNamed(const std::string& name) + { + return renderImageHandle(name); + } + + AudioSourceHandle testing_audioNamed(const std::string& name) + { + return audioSourceHandle(name); + } + + FontHandle testing_fontNamed(const std::string& name) + { + return fontHandle(name); + } +#endif + +private: + template <typename TResourceHandle, typename TResource> + static rcp<TResource> getResource( + const std::unordered_map<TResourceHandle, rcp<TResource>>& resources, + TResourceHandle handle) + { + auto itr = resources.find(handle); + return itr == resources.end() ? nullptr : itr->second; + } + + const std::unordered_map<RenderImageHandle, rcp<RenderImage>>& m_images; + const std::unordered_map<AudioSourceHandle, rcp<AudioSource>>& + m_audioSources; + const std::unordered_map<FontHandle, rcp<Font>>& m_fonts; + TypedGlobalAssetRegistry<RenderImageHandle, ImageAsset> m_imageRegistry; + TypedGlobalAssetRegistry<AudioSourceHandle, AudioAsset> m_audioRegistry; + TypedGlobalAssetRegistry<FontHandle, FontAsset> m_fontRegistry; +}; + +/** + * The file asset loader for one command-queue import. + * + * It first delegates to the optional FileAssetLoader supplied to CommandServer. + * When that loader returns true, it is loading or has loaded the asset outside + * the command queue, so the asset is not registered for a global resource. + * Otherwise, eligible out-of-band assets are retained locally so they can be + * registered after a successful import. + */ class CommandServer::CommandFileAssetLoader : public FileAssetLoader { public: - CommandFileAssetLoader(CommandServer* server, - rcp<rive::FileAssetLoader> internalLoader) : - m_server(server), m_internalLoader(internalLoader) + CommandFileAssetLoader(rcp<FileAssetLoader> internalLoader) : + m_internalLoader(std::move(internalLoader)) {} - virtual bool loadContents(FileAsset& asset, - Span<const uint8_t> inBandBytes, - Factory* factory) override + bool loadContents(FileAsset& asset, + Span<const uint8_t> inBandBytes, + Factory* factory) override { - if (m_internalLoader) + if (m_internalLoader && + m_internalLoader->loadContents(asset, inBandBytes, factory)) { - if (m_internalLoader->loadContents(asset, inBandBytes, factory)) - return true; + return true; } - if (asset.is<ImageAsset>()) + if (!inBandBytes.empty()) { - // No need for another if because as just asserts the above - // condition anyway - auto imageAsset = asset.as<ImageAsset>(); - auto itr = m_imageAssets.find(asset.uniqueName()); - if (itr != m_imageAssets.end()) - { - auto image = m_server->getImage(itr->second); - if (image) - { - imageAsset->renderImage(ref_rcp(image)); - return true; - } - return false; - } + return false; } - - else if (asset.is<AudioAsset>()) + if (asset.is<ImageAsset>() || asset.is<AudioAsset>() || + asset.is<FontAsset>()) { - auto audioAsset = asset.as<AudioAsset>(); - auto itr = m_audioAssets.find(asset.uniqueName()); - if (itr != m_audioAssets.end()) - { - auto audioSource = m_server->getAudioSource(itr->second); - if (audioSource) - { - audioAsset->audioSource(ref_rcp(audioSource)); - return true; - } - return false; - } + m_fileAssets.push_back(ref_rcp(&asset)); + return false; } - else if (asset.is<FontAsset>()) - { - auto fontAsset = asset.as<FontAsset>(); - auto itr = m_fontAssets.find(asset.uniqueName()); - if (itr != m_fontAssets.end()) - { - auto font = m_server->getFont(itr->second); - if (font) - { - fontAsset->font(ref_rcp(font)); - return true; - } - return false; - } - } - else if (asset.is<TextAsset>() || asset.is<ScriptModuleAsset>() || - asset.is<BlobAsset>() || asset.is<ManifestAsset>()) + if (asset.is<TextAsset>() || asset.is<ScriptModuleAsset>() || + asset.is<BlobAsset>() || asset.is<ManifestAsset>()) { // These assets cannot be registered externally with the command // server. Returning false lets the importer decode their in-band // contents. TextAsset includes ScriptAsset and ShaderAsset. return false; } - else - { - fprintf(stderr, - "ERROR: CommandFileAssetLoader::loadContents - Unsupported" - " asset type for asset: '%s'\n", - asset.uniqueFilename().c_str()); - return false; - } - + fprintf(stderr, + "ERROR: CommandFileAssetLoader::loadContents - Unsupported" + " asset type for asset: '%s'\n", + asset.uniqueFilename().c_str()); return false; } - void addRenderImage(std::string name, RenderImageHandle handle) + Span<const rcp<FileAsset>> fileAssets() const { - m_imageAssets[name] = handle; + return {m_fileAssets.data(), m_fileAssets.size()}; } - void addAudioSource(std::string name, AudioSourceHandle handle) - { - m_audioAssets[name] = handle; - } - - void addFont(std::string name, FontHandle handle) - { - m_fontAssets[name] = handle; - } - - void removeRenderImage(RenderImageHandle handle) - { - using ItrType = std::pair<std::string, RenderImageHandle>; - auto itr = std::find_if( - m_imageAssets.begin(), - m_imageAssets.end(), - [handle](const ItrType& p) { return p.second == handle; }); - if (itr != m_imageAssets.end()) - m_imageAssets.erase(itr); - } - - void removeAudioSource(AudioSourceHandle handle) - { - using ItrType = std::pair<std::string, AudioSourceHandle>; - auto itr = std::find_if( - m_audioAssets.begin(), - m_audioAssets.end(), - [handle](const ItrType& p) { return p.second == handle; }); - if (itr != m_audioAssets.end()) - m_audioAssets.erase(itr); - } - - void removeFont(FontHandle handle) - { - using ItrType = std::pair<std::string, FontHandle>; - auto itr = std::find_if( - m_fontAssets.begin(), - m_fontAssets.end(), - [handle](const ItrType& p) { return p.second == handle; }); - if (itr != m_fontAssets.end()) - m_fontAssets.erase(itr); - } - - void removeRenderImage(std::string name) { m_imageAssets.erase(name); } - void removeAudioSource(std::string name) { m_audioAssets.erase(name); } - void removeFont(std::string name) { m_fontAssets.erase(name); } - -#ifdef TESTING - RenderImageHandle testing_imageNamed(std::string name) - { - auto itr = m_imageAssets.find(name); - if (itr != m_imageAssets.end()) - return itr->second; - return nullptr; - } - - AudioSourceHandle testing_audioNamed(std::string name) - { - auto itr = m_audioAssets.find(name); - if (itr != m_audioAssets.end()) - return itr->second; - return nullptr; - } - - FontHandle testing_fontNamed(std::string name) - { - auto itr = m_fontAssets.find(name); - if (itr != m_fontAssets.end()) - return itr->second; - return nullptr; - } -#endif - private: - const CommandServer* m_server; - - std::unordered_map<std::string, RenderImageHandle> m_imageAssets; - std::unordered_map<std::string, AudioSourceHandle> m_audioAssets; - std::unordered_map<std::string, FontHandle> m_fontAssets; rcp<FileAssetLoader> m_internalLoader; + std::vector<rcp<FileAsset>> m_fileAssets; }; std::ostream& operator<<(std::ostream& os, DataType t) @@ -248,46 +494,48 @@ RenderImageHandle CommandServer::testing_globalImageNamed(std::string name) { - return m_fileAssetLoader->testing_imageNamed(name); + return m_globalAssetRegistry->testing_imageNamed(name); } AudioSourceHandle CommandServer::testing_globalAudioNamed(std::string name) { - return m_fileAssetLoader->testing_audioNamed(name); + return m_globalAssetRegistry->testing_audioNamed(name); } FontHandle CommandServer::testing_globalFontNamed(std::string name) { - return m_fileAssetLoader->testing_fontNamed(name); + return m_globalAssetRegistry->testing_fontNamed(name); } bool CommandServer::testing_globalImageContains(std::string name) { - return m_fileAssetLoader->testing_imageNamed(name) != nullptr; + return m_globalAssetRegistry->testing_imageNamed(name) != nullptr; } bool CommandServer::testing_globalAudioContains(std::string name) { - return m_fileAssetLoader->testing_audioNamed(name) != nullptr; + return m_globalAssetRegistry->testing_audioNamed(name) != nullptr; } bool CommandServer::testing_globalFontContains(std::string name) { - return m_fileAssetLoader->testing_fontNamed(name) != nullptr; + return m_globalAssetRegistry->testing_fontNamed(name) != nullptr; } #endif -CommandServer::CommandServer(rcp<CommandQueue> commandBuffer, - Factory* factory, - rcp<rive::FileAssetLoader> internalLoader) : +CommandServer::CommandServer( + rcp<CommandQueue> commandBuffer, + Factory* factory, + rcp<rive::FileAssetLoader> internalFileAssetLoader) : m_commandQueue(std::move(commandBuffer)), m_factory(factory), #ifndef NDEBUG m_threadID(std::this_thread::get_id()), #endif - m_fileAssetLoader( - make_rcp<CommandFileAssetLoader>(this, std::move(internalLoader))) + m_globalAssetRegistry( + make_rcp<GlobalAssetRegistry>(m_images, m_audioSources, m_fonts)), + m_internalFileAssetLoader(std::move(internalFileAssetLoader)) {} CommandServer::~CommandServer() {} @@ -701,6 +949,9 @@ scriptingContextFactory; #endif lock.unlock(); + auto fileAssetLoader = + make_rcp<CommandFileAssetLoader>(m_internalFileAssetLoader); + #if defined(WITH_RIVE_SCRIPTING) && defined(WITH_RIVE_SCRIPTING_LUAU) std::cout << "Rive: Command Server Scripting Enabled.\n"; // Use the host-provided scripting context when supplied (e.g. @@ -718,7 +969,7 @@ rcp<rive::File> file = rive::File::import(rivBytes, m_factory, nullptr, - m_fileAssetLoader, + fileAssetLoader, vm.get()); #else @@ -730,11 +981,15 @@ rcp<rive::File> file = rive::File::import(rivBytes, m_factory, nullptr, - m_fileAssetLoader); + fileAssetLoader); #endif if (file != nullptr) { + auto fileAssets = fileAssetLoader->fileAssets(); + m_globalAssetRegistry->addFileAssets(handle, fileAssets); + m_globalAssetRegistry->applyResourcesToFileAssets( + fileAssets); m_fileDependencies[handle] = {}; m_files[handle] = file; @@ -762,7 +1017,14 @@ commandStream >> handle; commandStream >> requestId; lock.unlock(); - m_files.erase(handle); + auto fileItr = m_files.find(handle); + if (fileItr != m_files.end()) + { + m_globalAssetRegistry->removeFileAssets( + handle, + fileItr->second->assets()); + m_files.erase(fileItr); + } auto itr = m_fileDependencies.find(handle); if (itr != m_fileDependencies.end()) { @@ -855,7 +1117,11 @@ commandStream >> requestId; lock.unlock(); m_images.erase(handle); - m_fileAssetLoader->removeRenderImage(handle); + for (const auto& name : + m_globalAssetRegistry->removeRenderImage(handle)) + { + m_globalAssetRegistry->applyRenderImage(name, nullptr); + } std::unique_lock<std::mutex> messageLock( m_commandQueue->m_messageMutex); messageStream << CommandQueue::Message::imageDeleted; @@ -1019,7 +1285,11 @@ commandStream >> requestId; lock.unlock(); m_audioSources.erase(handle); - m_fileAssetLoader->removeAudioSource(handle); + for (const auto& name : + m_globalAssetRegistry->removeAudioSource(handle)) + { + m_globalAssetRegistry->applyAudioSource(name, nullptr); + } std::unique_lock<std::mutex> messageLock( m_commandQueue->m_messageMutex); messageStream << CommandQueue::Message::audioDeleted; @@ -1099,7 +1369,11 @@ commandStream >> requestId; lock.unlock(); m_fonts.erase(handle); - m_fileAssetLoader->removeFont(handle); + for (const auto& name : + m_globalAssetRegistry->removeFont(handle)) + { + m_globalAssetRegistry->applyFont(name, nullptr); + } std::unique_lock<std::mutex> messageLock( m_commandQueue->m_messageMutex); messageStream << CommandQueue::Message::fontDeleted; @@ -4032,9 +4306,22 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - if (handle && getImage(handle) != nullptr) + auto image = ref_rcp(getImage(handle)); + if (image != nullptr) { - m_fileAssetLoader->addRenderImage(std::move(name), handle); + m_globalAssetRegistry->setRenderImage(name, handle); + m_globalAssetRegistry->applyRenderImage(name, image); + } + else + { + ErrorReporter<RenderImageHandle>( + this, + handle, + requestId, + CommandQueue::Message::imageError) + << "Invalid image handle when adding global image " + "asset \"" + << name << "\""; } break; } @@ -4047,7 +4334,8 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - m_fileAssetLoader->removeRenderImage(std::move(name)); + m_globalAssetRegistry->removeRenderImage(name); + m_globalAssetRegistry->applyRenderImage(name, nullptr); break; } @@ -4061,9 +4349,22 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - if (handle && getAudioSource(handle) != nullptr) + auto audioSource = ref_rcp(getAudioSource(handle)); + if (audioSource != nullptr) { - m_fileAssetLoader->addAudioSource(std::move(name), handle); + m_globalAssetRegistry->setAudioSource(name, handle); + m_globalAssetRegistry->applyAudioSource(name, audioSource); + } + else + { + ErrorReporter<AudioSourceHandle>( + this, + handle, + requestId, + CommandQueue::Message::audioError) + << "Invalid audio handle when adding global audio " + "asset \"" + << name << "\""; } break; } @@ -4076,7 +4377,8 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - m_fileAssetLoader->removeAudioSource(std::move(name)); + m_globalAssetRegistry->removeAudioSource(name); + m_globalAssetRegistry->applyAudioSource(name, nullptr); break; } @@ -4090,9 +4392,21 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - if (handle && getFont(handle) != nullptr) + auto font = ref_rcp(getFont(handle)); + if (font != nullptr) { - m_fileAssetLoader->addFont(std::move(name), handle); + m_globalAssetRegistry->setFont(name, handle); + m_globalAssetRegistry->applyFont(name, font); + } + else + { + ErrorReporter<FontHandle>(this, + handle, + requestId, + CommandQueue::Message::fontError) + << "Invalid font handle when adding global font asset " + "\"" + << name << "\""; } break; } @@ -4105,7 +4419,8 @@ commandStream >> requestId; m_commandQueue->m_names >> name; lock.unlock(); - m_fileAssetLoader->removeFont(std::move(name)); + m_globalAssetRegistry->removeFont(name); + m_globalAssetRegistry->applyFont(name, nullptr); break; }
diff --git a/tests/unit_tests/assets/hosted_audio_file.riv b/tests/unit_tests/assets/hosted_audio_file.riv new file mode 100644 index 0000000..8afc639 --- /dev/null +++ b/tests/unit_tests/assets/hosted_audio_file.riv Binary files differ
diff --git a/tests/unit_tests/runtime/command_queue_test.cpp b/tests/unit_tests/runtime/command_queue_test.cpp index ac60e27..9ec1277 100644 --- a/tests/unit_tests/runtime/command_queue_test.cpp +++ b/tests/unit_tests/runtime/command_queue_test.cpp
@@ -518,7 +518,7 @@ }; static void server_thread_file_loader(rcp<CommandQueue> commandQueue, - rcp<TestAssetsFileLoader> loader) + rcp<rive::FileAssetLoader> loader) { std::unique_ptr<gpu::RenderContext> nullContext = RenderContextNULL::MakeContext(); @@ -7647,3 +7647,1062 @@ commandQueue->setGlobalStateMachineListener(nullptr); commandQueue->disconnect(); } + +TEST_CASE("registered global image asset applies to files loaded afterward", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + FileAssetsListenerCallback listener; + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalImageAsset resolves across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, imageHandle](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalImageAsset replaces across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream imageStream1("assets/batdude.png", std::ios::binary); + auto imageHandle1 = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream1), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle1); + + std::ifstream imageStream2("assets/batdude.png", std::ios::binary); + auto imageHandle2 = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream2), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle2); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, imageHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle2)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle2)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("removeGlobalImageAsset clears across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + commandQueue->removeGlobalImageAsset("one-45008"); + + commandQueue->runOnce([fileHandle1, fileHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE( + "addGlobalImageAsset with non-matching name does not resolve on files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("wrong-name", imageHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalImageAsset with invalid handle reports error", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + TestRenderImageErrorListener errorListener; + errorListener.m_handle = RIVE_NULL_HANDLE; + commandQueue->setGlobalRenderImageListener(&errorListener); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + commandQueue->addGlobalImageAsset("one-45008", RIVE_NULL_HANDLE); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + wait_for_server(commandQueue.get()); + + commandQueue->processMessages(); + CHECK(errorListener.m_hasCallback); + + commandQueue->setGlobalRenderImageListener(nullptr); + commandQueue->disconnect(); + serverThread.join(); +} + +#ifdef WITH_RIVE_TEXT +TEST_CASE("addGlobalFontAsset resolves across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {})); + + commandQueue->addGlobalFontAsset("Inter-43276", fontHandle); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, fontHandle](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<FontAsset>()->font().get() == + server->getFont(fontHandle)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<FontAsset>()->font().get() == + server->getFont(fontHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalFontAsset replaces across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream fontStream1("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle1 = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream1), {})); + + commandQueue->addGlobalFontAsset("Inter-43276", fontHandle1); + + std::ifstream fontStream2("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle2 = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream2), {})); + + commandQueue->addGlobalFontAsset("Inter-43276", fontHandle2); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, fontHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<FontAsset>()->font().get() == + server->getFont(fontHandle2)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<FontAsset>()->font().get() == + server->getFont(fontHandle2)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("removeGlobalFontAsset clears across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {})); + + commandQueue->addGlobalFontAsset("Inter-43276", fontHandle); + commandQueue->removeGlobalFontAsset("Inter-43276"); + + commandQueue->runOnce([fileHandle1, fileHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<FontAsset>()->font() == nullptr); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<FontAsset>()->font() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalFontAsset with non-matching name does not resolve on files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {})); + + commandQueue->addGlobalFontAsset("wrong-name", fontHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<FontAsset>()->font() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalFontAsset with invalid handle reports error", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + TestFontErrorListener errorListener; + errorListener.m_handle = RIVE_NULL_HANDLE; + commandQueue->setGlobalFontListener(&errorListener); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + commandQueue->addGlobalFontAsset("Inter-43276", RIVE_NULL_HANDLE); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<FontAsset>()->font() == nullptr); + }); + + wait_for_server(commandQueue.get()); + + commandQueue->processMessages(); + CHECK(errorListener.m_hasCallback); + + commandQueue->setGlobalFontListener(nullptr); + commandQueue->disconnect(); + serverThread.join(); +} +#endif + +#ifdef WITH_RIVE_AUDIO +TEST_CASE("addGlobalAudioAsset resolves across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream audioStream("assets/audio/what.wav", std::ios::binary); + auto audioHandle = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {})); + + commandQueue->addGlobalAudioAsset("sound-55368", audioHandle); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, audioHandle](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<AudioAsset>()->audioSource().get() == + server->getAudioSource(audioHandle)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<AudioAsset>()->audioSource().get() == + server->getAudioSource(audioHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalAudioAsset replaces across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream audioStream1("assets/audio/what.wav", std::ios::binary); + auto audioHandle1 = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream1), {})); + + commandQueue->addGlobalAudioAsset("sound-55368", audioHandle1); + + std::ifstream audioStream2("assets/audio/what.wav", std::ios::binary); + auto audioHandle2 = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream2), {})); + + commandQueue->addGlobalAudioAsset("sound-55368", audioHandle2); + + commandQueue->runOnce( + [fileHandle1, fileHandle2, audioHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<AudioAsset>()->audioSource().get() == + server->getAudioSource(audioHandle2)); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<AudioAsset>()->audioSource().get() == + server->getAudioSource(audioHandle2)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("removeGlobalAudioAsset clears across all files", "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream audioStream("assets/audio/what.wav", std::ios::binary); + auto audioHandle = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {})); + + commandQueue->addGlobalAudioAsset("sound-55368", audioHandle); + commandQueue->removeGlobalAudioAsset("sound-55368"); + + commandQueue->runOnce([fileHandle1, fileHandle2](CommandServer* server) { + auto file1 = server->getFile(fileHandle1); + REQUIRE(file1 != nullptr); + CHECK(file1->assets()[0]->as<AudioAsset>()->audioSource() == nullptr); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<AudioAsset>()->audioSource() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE( + "addGlobalAudioAsset with non-matching name does not resolve on files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream audioStream("assets/audio/what.wav", std::ios::binary); + auto audioHandle = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {})); + + commandQueue->addGlobalAudioAsset("wrong-name", audioHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<AudioAsset>()->audioSource() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("addGlobalAudioAsset with invalid handle reports error", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + TestAudioSourceErrorListener errorListener; + errorListener.m_handle = RIVE_NULL_HANDLE; + commandQueue->setGlobalAudioSourceListener(&errorListener); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + commandQueue->addGlobalAudioAsset("sound-55368", RIVE_NULL_HANDLE); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<AudioAsset>()->audioSource() == nullptr); + }); + + wait_for_server(commandQueue.get()); + + commandQueue->processMessages(); + CHECK(errorListener.m_hasCallback); + + commandQueue->setGlobalAudioSourceListener(nullptr); + commandQueue->disconnect(); + serverThread.join(); +} +#endif + +TEST_CASE("deleting a file does not disturb another file's global asset", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener1; + FileAssetsListenerCallback listener2; + + std::ifstream stream1("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle1 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream1), {}), + &listener1); + + std::ifstream stream2("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle2 = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream2), {}), + &listener2); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + commandQueue->deleteFile(fileHandle1); + + // The surviving file keeps its resolved asset, and the deleted file is + // gone. + commandQueue->runOnce( + [fileHandle1, fileHandle2, imageHandle](CommandServer* server) { + CHECK(server->getFile(fileHandle1) == nullptr); + + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + // A subsequent global change for the same name must reach the surviving + // file and must not touch the deleted file's assets. + std::ifstream replacementStream("assets/batdude.png", std::ios::binary); + auto replacementHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(replacementStream), + {})); + + commandQueue->addGlobalImageAsset("one-45008", replacementHandle); + + commandQueue->runOnce( + [fileHandle2, replacementHandle](CommandServer* server) { + auto file2 = server->getFile(fileHandle2); + REQUIRE(file2 != nullptr); + CHECK(file2->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(replacementHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("global asset changes after deleting its only file are a safe no-op", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + commandQueue->deleteFile(fileHandle); + + // With no file referencing the name, applying and clearing it must not + // dereference the deleted file's assets. + commandQueue->removeGlobalImageAsset("one-45008"); + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + CHECK(server->getFile(fileHandle) == nullptr); + CHECK(server->getImage(imageHandle) != nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("failed load does not contaminate a later file's global assets", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + // A file that fails to import must not leave its assets tracked. + FileHandle badFile = + commandQueue->loadFile(std::vector<uint8_t>(100 * 1024, 0)); + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle goodFile = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + commandQueue->runOnce( + [badFile, goodFile, imageHandle](CommandServer* server) { + CHECK(server->getFile(badFile) == nullptr); + + auto file = server->getFile(goodFile); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("global image asset does not override embedded asset", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + // Registered before the file loads: the embedded contents must still win. + commandQueue->addGlobalImageAsset("1x1-45022", imageHandle); + + std::ifstream stream("assets/in_band_asset.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + auto image = file->assets()[0]->as<ImageAsset>()->renderImage(); + CHECK(image != nullptr); + CHECK(image != server->getImage(imageHandle)); + }); + + // Removing and re-adding the global after load must not clear or replace + // the embedded contents either. + commandQueue->removeGlobalImageAsset("1x1-45022"); + commandQueue->addGlobalImageAsset("1x1-45022", imageHandle); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + auto image = file->assets()[0]->as<ImageAsset>()->renderImage(); + CHECK(image != nullptr); + CHECK(image != server->getImage(imageHandle)); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("deleting an image clears the applied global asset on loaded files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + // Deleting the image deletes the global asset backed by it, so the + // change is applied to loaded files just like a removal. + commandQueue->deleteImage(imageHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +TEST_CASE("deleting an image registered under multiple names clears all", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + // The same image backs two global asset names. + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + commandQueue->addGlobalImageAsset("two-45009", imageHandle); + + commandQueue->runOnce([fileHandle, imageHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(imageHandle)); + }); + + // Deleting the image deletes every global asset it backed, applying the + // removal to loaded files and leaving no stale registry entries. + commandQueue->deleteImage(imageHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + CHECK(!server->testing_globalImageContains("one-45008")); + CHECK(!server->testing_globalImageContains("two-45009")); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +#ifdef WITH_RIVE_TEXT +TEST_CASE("deleting a font clears the applied global asset on loaded files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_font_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream fontStream("assets/fonts/OpenSans-Italic.ttf", + std::ios::binary); + auto fontHandle = commandQueue->decodeFont( + std::vector<uint8_t>(std::istreambuf_iterator<char>(fontStream), {})); + + commandQueue->addGlobalFontAsset("Inter-43276", fontHandle); + + commandQueue->runOnce([fileHandle, fontHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<FontAsset>()->font().get() == + server->getFont(fontHandle)); + }); + + // Deleting the font deletes the global asset backed by it, so the + // change is applied to loaded files just like a removal. + commandQueue->deleteFont(fontHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<FontAsset>()->font() == nullptr); + CHECK(!server->testing_globalFontContains("Inter-43276")); + }); + + commandQueue->disconnect(); + serverThread.join(); +} +#endif + +#ifdef WITH_RIVE_AUDIO +TEST_CASE( + "deleting an audio source clears the applied global asset on loaded files", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_audio_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream audioStream("assets/audio/what.wav", std::ios::binary); + auto audioHandle = commandQueue->decodeAudio( + std::vector<uint8_t>(std::istreambuf_iterator<char>(audioStream), {})); + + commandQueue->addGlobalAudioAsset("sound-55368", audioHandle); + + commandQueue->runOnce([fileHandle, audioHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<AudioAsset>()->audioSource().get() == + server->getAudioSource(audioHandle)); + }); + + // Deleting the audio source deletes the global asset backed by it, so + // the change is applied to loaded files just like a removal. + commandQueue->deleteAudio(audioHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<AudioAsset>()->audioSource() == nullptr); + CHECK(!server->testing_globalAudioContains("sound-55368")); + }); + + commandQueue->disconnect(); + serverThread.join(); +} +#endif + +TEST_CASE("deleting a replaced image does not clear its former name", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + std::thread serverThread(server_thread, commandQueue); + + FileAssetsListenerCallback listener; + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + std::ifstream imageStream1("assets/batdude.png", std::ios::binary); + auto firstHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream1), {})); + + std::ifstream imageStream2("assets/batdude.png", std::ios::binary); + auto secondHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream2), {})); + + // The name is re-registered from the first image to the second. + commandQueue->addGlobalImageAsset("one-45008", firstHandle); + commandQueue->addGlobalImageAsset("one-45008", secondHandle); + + // Deleting the first image must not clear the name, which now belongs + // to the second image. + commandQueue->deleteImage(firstHandle); + + commandQueue->runOnce([fileHandle, secondHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == + server->getImage(secondHandle)); + CHECK(server->testing_globalImageContains("one-45008")); + }); + + commandQueue->disconnect(); + serverThread.join(); +} + +namespace +{ +// Claims image assets without providing contents, standing in for a custom +// loader that owns asset resolution. +class ClaimingImageAssetLoader : public rive::FileAssetLoader +{ +public: + bool loadContents(FileAsset& asset, + Span<const uint8_t> inBandBytes, + Factory* factory) override + { + return asset.is<ImageAsset>(); + } +}; +} // namespace + +TEST_CASE("internal loader claimed asset is not overridden by global asset", + "[CommandQueue]") +{ + auto commandQueue = make_rcp<CommandQueue>(); + auto loader = make_rcp<ClaimingImageAssetLoader>(); + std::thread serverThread(server_thread_file_loader, commandQueue, loader); + + FileAssetsListenerCallback listener; + + std::ifstream imageStream("assets/batdude.png", std::ios::binary); + auto imageHandle = commandQueue->decodeImage( + std::vector<uint8_t>(std::istreambuf_iterator<char>(imageStream), {})); + + // Registered before the file loads: the claiming loader still wins. + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + std::ifstream stream("assets/hosted_image_file.riv", std::ios::binary); + FileHandle fileHandle = commandQueue->loadFile( + std::vector<uint8_t>(std::istreambuf_iterator<char>(stream), {}), + &listener); + + // The claiming loader set no contents, and the global asset must not + // have been applied over its claim at load. + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + // Re-applying the global after load must not reach the claimed asset + // either. + commandQueue->addGlobalImageAsset("one-45008", imageHandle); + + commandQueue->runOnce([fileHandle](CommandServer* server) { + auto file = server->getFile(fileHandle); + REQUIRE(file != nullptr); + CHECK(file->assets()[0]->as<ImageAsset>()->renderImage() == nullptr); + }); + + commandQueue->disconnect(); + serverThread.join(); +}