Candle-Fire / umbra

1 stars 3 forks source link

Asset management system #31

Open TheCurle opened 7 months ago

TheCurle commented 7 months ago

This PR adds an asset management system, in two parts:

This PR depends on the Profiler (#30).

FileSystem abstraction

Since our engine plans to use object "pack" files to efficiently store assets, we need some concise way to interact with both the on-disk filesystem and our "virtual filesystem" for these packs, as if they were one and the same.

Thus, the first major thing implemented here is a FileSystem abstraction: You can create a DiskFS from a root folder using the static helper:

ShadowEngine::FileSystem::createDiskFS("./");

You can also create a VFS from a root pack file:

ShadowEngine::FileSystem::createVFS("./root.sff");

The FileSystem allows you to perform many disk operations, some asynchronously.

 // Open a file for reading.
        virtual bool open(const std::string& path, FileInput& input) = 0;
        // Open a file for writing.
        virtual bool open(const std::string& path, FileOutput& output) = 0;
        // Check whether a file exists at the given path.
        virtual bool fileExists(const std::string& path) = 0;
        // Get the time a file at the given path was last modified.
        virtual size_t getLastModified(const std::string& path) = 0;
        // Copy a file from one path to another.
        virtual bool copyFile(const std::string& from, const std::string& to) = 0;
        // Move a file from one path to another.
        virtual bool moveFile(const std::string& from, const std::string& to) = 0;
        // Disassociate any files at the given path (not an immediate delete)
        virtual bool deleteFile(const std::string& path) = 0;

        // Get the path that this FileSystem originates at. The default is "/" for VFS, and whatever the Executable Path is for Disk FS.
        virtual std::string const& getBasePath() const = 0;
        // Set a new base path for the FileSystem. Any operations involving file paths will be relative to this new path.
        virtual void setBasePath(const std::string& path) = 0;

        // Process all the callbacks for async file operations.
        virtual void processCallbacks() = 0;
        // Check whether there are any outstanding async operations that need work.
        virtual bool hasWork() = 0;

        // Write new content to a file synchronously. The thread will be blocked when doing this.
        virtual bool saveSync(const Path& file, const uint8_t* content, size_t size) = 0;
        // Read content from a file synchronously. The thread will be blocked when doing this.
        virtual bool readSync(const Path& file, struct OutputMemoryStream& content) = 0;

        // Read a file asynchronously. The given callback will be called with the file content once it is available.
        virtual AsyncHandle readAsync(const Path& file, const ContentCallback& callback) = 0;
        // Cancel an asynchronous operation, if it is not already complete. The associated callback will be called with a special flag.
        virtual void cancelAsync(AsyncHandle& handle) = 0;

The main point here is that no matter what is underlying the file resources, you can interact with them in the same way, through the FileSystem.

The main consumer of FileSystem is the Resource Management.

Resources & Resource Managers

A Resource is a single, self contained object that something in the Engine can consume. It may be a texture, a mesh, a model (which would depend on and thus load both the texture and model, as well as other things like animations..), or something like a library, resource script, etc.

Every type of Resource is identified by a unique ResourceType key, and has its own ResourceTypeManager. All ResourceTypeManagers are managed by the central, unique ResourceManager.

The ResourceManager has three ways to load a Resource - and it can load a resource of any type that has a recognized ResourceTypeManager loaded:


        template <typename R>
        R* load(const Path& path) {
            return static_cast<R*>(load(R::TYPE, path));
        }

        Resource* load(ResourceTypeManager& manager, const Path& path);
        Resource* load(ResourceType type, const Path& path);

The first option is simply resourceManager.load<RESOURCE>(path), which will load the file at the given path, check whether it's of the type given, then return the type. The second option is to give the resource type directly, which is obtainable by every Resource class' static TYPE member. The third option is to get the ResourceTypeManager of a given ResourceType, then pass that back in to get a resource.

Each of these ways provide better compile-time stability, better runtime performance, and better clarity of programming, so usage of these is preferential and circumstantial.

Once a Resource goes out of scope, it can be culled by the ResourceManager - as all Resource references are counted.

You can get another reference to a loaded Resource by calling load() again with the same path - it will return the cached value, and increase the number of references internally.

Closes #18.