-
Notifications
You must be signed in to change notification settings - Fork 0
The Genode IO module provides resource management for your game, including streamlining resource deserialization, managing resource lifetime, and interacting with the file system. Resource loader factory can also be made to incorporate the Gx::Context class, allowing you to create a complex resource loader.
Note
Currently, Genode does not streamline how you serialize your game resources. However, it provides minimum functionalities for writing data into the local disk. That is hopefully adequate for general use cases.
Genode provides an abstraction layer for the file system to interact with files, which allows you to implement a virtual file system on top of the physical file system implementation.
The Gx::FileSystemController interface represents a controller for a file system to implement a virtual file system. By virtual means that the implementation of the file system controller may (or may not) deal with files that are not managed by the operating system, such as physical or memory-mapped files. For instance, a virtual file system can be implemented to specifically handle files inside an archive, or files that live over a network, or any other files based on your definition.
This abstraction layer isolates any IO file operations that the game can perform. It also helps to indiscriminate the read and write operations: It doesn't matter if the game reads a file from a physical file or a virtual file that is served by your custom file system controller implementation while still being able to enable or disable one or more of these file systems.
To implement custom Gx::FileSystemController, you must provide the implementation of the following pure abstract functions:
-
Gx::ResourcePtr<sf::InputStream> Open(const std::string& fileName)
Open a file by returning a valid file stream assf::InputStream. -
std::vector<std::unique_ptr<Gx::FileInfo>> Scan(const std::string& pattern)
Scan files using the given pattern (e.g.,"*.txt") and return a collection ofGx::FileInfo. -
bool Contains(const std::string& fileName)
Determine whether the specified file name exists within the file system controller. This function will be used to determine whether this particular file system controller instance should process the given file. -
std::unique_ptr<Gx::FileInfo> GetFileInfo(const std::string& fileName)
ReturnGx::FileInfoof given file name. -
std::optional<std::size_t> ReadFile(const std::string& fileName, void* data, std::size_t size)
Read a file of the given file name based on the given size and store it inside the given data pointer. -
std::optional<std::size_t> GetFileSize(const std::string& fileName)
Get the file size of the given file name. -
std::vector<std::unique_ptr<Gx::FileInfo>> GetFileEntries() const
Return all files that the file system controller implementation can handle.
The Gx::FileInfo structure describes the file metadata. The controller returns a pointer of this structure that allows you to return a derived type of this file info struct should you wish to add extra information. The Gx::FileInfo has properties as follows:
-
Parent
AFileSystemControllerreference to which the file belongs. -
Name
The file name of the file. -
Size
The file size of the file.
The Gx::Archive class is an abstract class representing an archive virtual file system.
This class inherits Gx::FileSystemController and has an additional pure abstract function to implement:
-
bool LoadFromFile(const std::string& fileName)
Load the archive from a file. Returntruewhen the file is supported by the archive implementation, otherwisefalse.
By default, the archive class sets the prefix by using the given file name without extension upon loading (e.g., Calling LoadFromFile("bgEffect.zip"); will yield the "bgEffect" prefix.
Note
All pure abstract functions in Gx::FileSystemController are remain pure abstract in Gx::Archive.
Therefore, you must still implement them when using Gx::Archive.
Gx::FileSystem is a class aggregating multiple file system controller instances. It provides static functions that mirror the interface of Gx::FileSytemController. Always use Gx::FileSystem when dealing with files to isolate and indiscriminate the read-and-write IO operations from multiple Gx::FileSystemController.
Use Mount and Dismount to mount and dismount Gx::FileSystemController to/from the Gx::FileSystem respectively.
std::unique_ptr<Archive> bgm = // ... concrete implementation of Archive
if (bgm->LoadFromFile("BGM.zip"))
Gx::FileSystem::Mount(*bgm);
// This will retrieve the file info of "BGM/music.ogg" from the archive when the following conditions are met:
// 1. "BGM/music.ogg" does not exist physically (By default, Gx::LocalFileSystem is mounted first and has the first check priority)
// 2. "BGM/music.ogg" exists inside the "BGM.zip" (checked via Gx::FileSystemController::Contains)
auto fileInfo = Gx::FileSystem::GetFileInfo("BGM/music.ogg");
// You can disable the file system controllers by dismounting them
Gx::FileSystem::Dismount(*bgm);Gx::LocalFileSystem is a built-in Gx::FileSystemController implementation that deals with physical files in the local disks. It is a singleton class that can be accessed via Gx::LocalFileSystem::Instance().
In addition to standard Gx::FileSystemController functions, Gx::FileSystem can have asset paths that can be associated by calling AddAssetPath. This will allow you to specify a relative path to the asset paths you've added in every local file system function.
// Add "/some/root/path" as one of the asset paths
Gx::LocalFileSystem::Instance().AddAssetPath("/some/root/path");
// You can add more asset path here if needed!
// The file system will look up "some_file.dat"
// If it does not exist, it will try to open "/some/root/path/some_file.dat"
auto size = Gx::LocalFileSystem::Instance().GetFileSize("some_file.dat");Note
Gx::LocalFileSystem::Instance() is mounted to the Gx::FileSystem by default, so you don't have to mount it manually.
Use the Gx::ResourceLoader interface to implement resource-loading logic for a particular type. The template parameter determines the type of resource that the resource loader can handle. It is up to you to decide whether a type can be considered a resource and how your loader may or may not affect the resource's lifetime.
By default, the framework provides three built-in resource loaders: Gx::TextureLoader, Gx::FontLoader, and Gx::SoundBufferLoader. They are also automatically registered within Gx::ResourceLoaderFactory, so you don't have to register them manually.
A valid Gx::ResourceLoader implementation must override the LoadFromFile, LoadFromMemory, and LoadFromStream functions, which describe how a resource should be loaded from three different sources, respectively.
Typically, a Gx::ResourceLoader for sf::Texture will look something like this:
class TextureLoader : public Gx::ResourceLoader
{
public:
Gx::ResourcePtr<sf::Texture> LoadFromFile(const std::string& path, Gx::ResourceContext&)
{
auto texture = std::make_unique<sf::Texture>();
if (!texture->loadFromFile(Gx::LocalFileSystem::Instance().GetFullName(path)))
return nullptr;
return texture; // return as std::unique_ptr with a custom deleter (Gx::ResourcePtr)
}
// Do something similar for LoadFromMemory and LoadFromStream ...
};
// Usage:
TextureLoader loader;
auto texture = loader.LoadFromFile("/path/to/texture.png", Gx::ResourceContext::Default);Important
The best practice is always to incorporate Gx::FileSystem whenever you need to do an IO operation when loading a resource. Such as when you need to read a file or open a file as an sf::InputStream.
You may notice that the LoadFromFile function does not provide additional parameters for constructing a resource. For example, there's no way to specify the area of the texture image to load in TextureLoader. This is intended and part of the design. The framework encourages you to define metadata/definitions for your resources that describe the resource properties for the deserialization process.
For example, the path parameter in the LoadFromFile function should accept the filename of the metadata/definition file instead of the path of the actual texture file itself.
class CustomTextureLoader : public Gx::ResourceLoader
{
public:
Gx::ResourcePtr<sf::Texture> LoadFromFile(const std::string& path, Gx::ResourceContext&)
{
auto texture = std::make_unique<sf::Texture>();
auto metadata = //.. load your metadata from the given path
if (!texture->loadFromFile(metadata.GetTexturePath()))
return nullptr;
return texture;
}
// Do something similar for LoadFromMemory and LoadFromStream ...
};
// Usage:
CustomTextureLoader loader;
auto texture = loader.LoadFromFile("/path/to/texture/metadata.json", Gx::ResourceContext::Default);You are responsible for designing the metadata/definition structure and determining how it can be deserialized.
This design will help reduce the number of constants when loading resources and enable you to build various complex resources, which can be useful in many scenarios. However, it is perfectly fine for trivial resources not to have metadata/definitions, particularly when they don't have extra options to construct.
Often, a resource depends on one or multiple resources. For example, sf::Sprite requires sf::Texture to function properly. To load these kinds of resources, you can either resolve the dependencies by loading the resources via Gx::ResourceLoader manually or use the provided Gx::ResourceContext to load or resolve the resources from a Gx::ResourceManager.
Note that an independent resource loader without Gx::ResourceManager will be unable to utilize the resource context. Attempting to use context in this circumstance will result in Gx::ResourceLoadException being thrown. In such a case, you must load the resource dependency manually.
Consider the following example where a SpriteLoader needs to load the corresponding sf::Texture to construct a Gx::Sprite instance.
class SpriteLoader : public Gx::ResourceLoader
{
public:
Gx::ResourcePtr<Gx::Sprite> LoadFromFile(const std::string& path, Gx::ResourceContext& ctx)
{
auto metadata = // ... load metadata from the given "path" which can be a json/xml file
// WARNING: You have to manage the texture lifetime and make sure it is alive beyond this scope
// Otherwise, the sprite will be invalid
auto loader = TextureLoader(); // or Gx::ResourceLoaderFactory::CreateLoader<sf::Texture>();
Gx::ResourcePtr<sf::Texture> texture = loader->LoadFromFile(metadata.GetTexturePath(), ctx);
if (!texture)
return nullptr;
// Construct the sprite with the texture
auto sprite = std::make_unique<Gx::Sprite>(*texture.get());
// Set the rest of the attributes from the metadata
sprite->SetPosition(metadata.GetPosition());
sprite->SetRotation(metadata.GetRotation());
// And so on..
// Again, ensure the texture is valid beyond this scope
// This function currently will return an invalid Sprite
return sprite;
}
// Do the same for LoadFromMemory and LoadFromStream ...
};
// Usage:
SpriteLoader loader;
auto sprite = loader.Load("/path/to/sprite/metadata.json", Gx::ResourceContext::Default);If a Gx::ResourceManager is bound to the Gx::ResourceContext, you can use the context to load or resolve the resource from the Gx::ResourceManager automatically. Use Gx::ResourceContext::Available() to check whether a resource manager is available.
class SpriteLoader : public Gx::ResourceLoader
{
public:
Gx::ResourcePtr<Gx::Sprite> LoadFromFile(const std::string& path, Gx::ResourceContext& ctx)
{
Gx::ResourcePtr<Gx::Sprite> sprite = nullptr;
auto metadata = // ... load metadata from the given "path" which can be a json/xml file
if (ctx.Available())
{
// You don't have to manage the texture since it is already stored inside the resource manager
sf::Texture& texture = ctx.Acquire<sf::Texture>(metadata.GetTexturePath()); // Accept string that represents resource id
// If the resource is not found, it will be evaluated as both a resource path and a resource id
sprite = std::make_unique<Gx::Sprite>(texture);
}
else
{
// You have to load the texture manually and manage its lifetime..
}
// Set the rest of the attributes from the metadata
sprite->SetPosition(metadata.GetPosition());
sprite->SetRotation(metadata.GetRotation());
// And so on..
return sprite;
}
// Do the same for LoadFromMemory and LoadFromStream ...
};
// You need to register the required loaders into the Gx::ResourceLoaderFactory
Gx::ResourceLoaderFactory::Register<sf::Texture, TextureLoader>();
Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>();
// Usage:
Gx::ResourceManager resources;
auto& sprite = resources.AddFromFile<sf::Sprite>("/path/to/sprite/metadata.json"); // Resource loader and its context are automatically resolved by the resource managerThe Gx::ResourceContext::Acquire also has several overloads to load the resource from the three standard sources. Those sources will only be used when the resource cannot be found within the Gx::ResourceManager. To make those Acquire function overloads work properly, you must register the appropriate resource loaders via Gx::ResourceLoaderFactory::Register.
The resources acquired via Acquire are automatically stored inside Gx::ResourceManager to delegate its lifetime to the Gx::ResourceManager object.
Gx::ResourceLoaderFactory provides a central place to register and resolve a resource loader by a given type.
You can simply associate a resource loader with a resource type by calling Register.
// Associate Gx::Sprite with SpriteLoader
Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>();
// Alternatively, you can decide how a resource loader should be instantiated with a builder function
Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>([] () -> std::unique_ptr<SpriteLoader>
{
return std::make_unique<SpriteLoader>(); // return as std::unique_ptr
});To create the registered resource loader from the factory, call CreateLoader with the resource type you wish to load.
// Create a std::unique_ptr of Gx::ResourceLoader for Gx::Sprite
auto loader = Gx::ResourceLoaderFactory::CreateLoader<Gx::Sprite>();Important
When you manage your resource with Gx::ResourceManager, it is highly recommended that you register all your loaders into the Gx::ResourceLoaderFactory so that the resource manager can resolve the appropriate resource loader and load the resource for you when needed.
Note
Gx::TextureLoader, Gx::FontLoader, and Gx::SoundBufferLoader are automatically registered within Gx::ResourceLoaderFactory, so you don't have to register them manually.
Sometimes, you may want to create an entity that shares the same properties as the other entity but with different logic. For example, you may interested in implementing Gx::Sprite to make your own entity like this:
class MyCustomEntity : public Gx::Sprite, public Gx::Updatable
{
protected:
void Update(const double delta) override
{
// your custom entity logic here..
}
// The rest have similar properties to Gx::Sprite
};By default, you cannot use Gx::ResourceLoaderFactory::Register because it only binds one resource loader class with a resource type at a time, and duplicating the SpriteLoader logic would be tedious, repetitive, and prone to error in the long run.
To solve this, use Gx::ResourceLoaderFactory::RegisterDerived to associate a resource loader already associated with a parent resource type. You can bind multiple derived resource types at a time.
// Associate Gx::Sprite with SpriteLoader
Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>();
// Associate the resource loader that was associated with Gx::Sprite with the MyCustomEntity
Gx::ResourceLoaderFactory::RegisterDerived<Gx::Sprite, MyCustomEntity>();
// Alternatively, you can specify how the resource should be constructed when `Instantiate` is called
Gx::ResourceLoaderFactory::RegisterDerived<Gx::Sprite, MyCustomEntity>([] (Gx::ResourceContext& ctx) -> Gx::ResourcePtr<MyCustomEntity>
{
return std::make_unique<MyCustomEntity>();
});
// You can also bind another derived resource type as many times as you like
Gx::ResourceLoaderFactory::RegisterDerived<Gx::Sprite, MyAnotherCustomEntity>();
// Create a resource loader for the derived type
auto loader = Gx::ResourceLoaderFactory::CreateLoader<MyCustomEntity>();You will then have to use Instantiate() in your resource loader to instantiate a resource with the correct type.
class SpriteLoader : public Gx::ResourceLoader
{
public:
Gx::ResourcePtr<Gx::Sprite> LoadFromFile(const std::string& path, Gx::ResourceContext& ctx)
{
// Use `Instantiate()` to create a correct type of resource
Gx::ResourcePtr<Gx::Sprite> sprite = Instantiate(ctx);
// Implement the rest of resource loading resource logic here..
}
// Do the same for LoadFromMemory and LoadFromStream ...
};Important
Register and RegisterDerived calls can be out-of-order. However, the parent resource type must be associated with the appropriate resource loader when a resource loader for the derived type is requested.
Gx::ResourceLoaderFactory automatically binds the root system context attached to the Gx::Application when you create an instance of Gx::Application. This allows the factory to perform dependency injection when creating a loader.
The following code illustrates a resource loader implementation with a dependency that the factory automatically injects.
struct ResourceLoaderConfig
{
// Some other configuration that your resource loader needs..
};
class SpriteLoader : public Gx::ResourceLoader<Gx::Sprite>
{
public:
// This is where the dependencies are injected into your resource loader
SpriteLoader(ResourceLoaderConfig& config) :
m_config(config)
{
}
// Implement LoadFromFile, LoadFromMemory and LoadFromStream here..
private:
ResourceLoaderConfig& m_config;
};
class MyAwesomeGame : public Gx::Application
{
public:
using Gx::Application::Application; // forward constructors
protected:
void Boot() override
{
// Configure the resource loader config
GetContext().Provide<ResourceLoaderConfig>([] (auto& ctx)
{
auto config = std::make_unique<ResourceLoaderConfig>();
// Load your config here..
return config;
});
// Register the loader
Gx::ResourceLoaderFactory::Register<Gx::Sprite, SpriteLoader>();
// Create the loader using the provided resource loader config via the factory
auto spriteLoader = Gx::ResourceLoaderFactory::CreateLoader<Gx::Sprite>();
}
};Use Gx::ResourceLoaderFactory::BindContext(const Gx::Context&) to change the context used by the resource factory.
The Gx::ResourceContainer<T> class provides a central point to store, access and destroy your resources. The template argument determines the type of resource that can be stored inside the container. As such, Gx::ResourceContainer can only hold a collection of one resource type at a time.
When you create or store resources inside the container, their lifetime is bound to the container's lifetime. Therefore, whenever the Gx::ResourceContainer object gets destroyed, all resources inside it will also be destroyed. By design, the container prevents you from regaining ownership of the resources.
Note
Sharing ownership or Dynamic Resource Allocation is micro-optimization that Genode avoids. The idea is to allocate the resources when they are actually needed and de-allocate them when no longer needed; this will reduce the memory usage required by the resources.
However, Dynamic allocation/de-allocation by sharing ownership can be hard to maintain. Improper implementation may cause circular dependencies that could lead to memory leaks as the resource potentially never gets destroyed. It's often unclear when the resource gets allocated or destroyed since everything becomes implicit. Some applications that are particular with their resource management may find it undesirable.
The implementation may not scale well as the application/game grows. Even if you don't care when a resource gets allocated or de-allocated, we still trade precious CPU load for (often small and trivial) memory usage. This is because the deserialization process may utilize the CPU considerably when dealing with many resources, especially when it involves particular processes such as decryption, decompression, etc. Meanwhile, memory usage tends to be small for a mere 2D game.
Alternatively, if you really need to dynamically allocate and de-allocate the resources, you can always roll out your own implementation by manually managing the resources inside the container object. This will be fine-grain controlled and efficient in many complex scenarios that can't be possibly covered by the framework entirely.
There are three cache modes available to choose from when storing a resource in the container:
-
Gx::CacheMode::None: The resource must be newly allocated inside the container.Gx::ResourceStoreExceptionwill be thrown if the container contains a resource associated with the specified resource id. -
Gx::CacheMode::Update: Store and return the specified resource. If the container contains a resource associated with the specified resource id, it will be overwritten. -
Gx::CacheMode::Reuse: Return an existing resource if the specified resource id exists inside the container and ignore the specified resource. This is the default cache mode if you don't specify it when calling theStorefunction.
To store a resource inside the container, simply call Store:
// Create the container and load the sprite
Gx::ResourceContainer<Gx::Sprite> container();
Gx::ResourcePtr<Gx::Sprite> sprite = // Load your sprite here via SpriteLoader..
// Store the sprite in the container
Gx::Sprite& storedSprite = container.Store(
"my-sprite", // Resource identifier
std::move(sprite), // Resource to store
Gx::CacheMode::Reuse // The cache mode
);Alternatively, to make Gx::CacheMode::Reuse more effective, you can use the Store overload function that takes a deserializer function. The deserializer will only be evaluated when necessary. Using Gx::CacheMode::None in this overload, however, will still throw an exception if the specified resource id exists within the container.
Gx::ResourceContainer<Gx::Sprite> container();
Gx::Sprite& sprite = container.Store(
"my-sprite",
// The following block will not be executed if Gx::CacheMode::Reuse is used and a resource associated with "my-sprite" id exists within the container
[&] ()
{
auto loader = Gx::ResourceLoaderFactory::CreateResourceLoaderFor<sf::Texture()>();
return loader->LoadFromFile("some/path/to/my/sprite.json", Gx::ResourceContext::Default); // return as Gx::ResourcePtr
},
Gx::CacheMode::Reuse
);Once the resource is stored within the container, use the Find function to retrieve a non-owning raw pointer of the resource that matches the specified id. Otherwise, nullptr will returned if it doesn't exist
Gx::ResourceContainer<Gx::Sprite> container();
Gx::Sprite* sprite = container.Find("my-sprite");Alternatively, use Get to work with reference instead of pointer. However, Gx::ResourceAccessException will be thrown if the specified resource id doesn't exist. You can use Contains function to check whether the resource exists within the container to use this method safely.
Gx::ResourceContainer<Gx::Sprite> container();
// Check before retrieving the resource reference
if (container.Contains("my-sprite"))
{
Gx::Sprite& sprite = container.Get("my-sprite");
}Resources inside the container can be iterated via the Each function. Additionally, you can retrieve the number of resources inside the container by calling the Count function.
Gx::ResourceContainer<Gx::Sprite> container();
std::size_t count = container.Count(); // Get the number of resources inside the container
container.Each([] (const std::string& resourceID, Gx::Sprite& sprite)
{
// Do something with resourceID / sprite here..
});
Use Destroy to delete the resource from the container. You can specify the resource id or the non-owning resource pointer previously retrieved from the Find function to delete the resource.
The function will return true if the resource is successfully destroyed; otherwise, it will return false when the specified resource does not exist within the container.
Gx::ResourceContainer<Gx::Sprite> container();
// Destroy resource by resource id
container.Destroy("my-sprite");
// .. Or you can destroy the resource by using the resource pointer
auto sprite = container.Find("my-sprite");
container.Destroy(sprite);Finally, you can delete all resources in the container by using the Clear function.
Gx::ResourceContainer<Gx::Sprite> container();
container.Clear();Caution
Make sure you no longer use the non-owning resource pointer once the resource is deleted from the container. Otherwise, it will lead to undefined behavior and may cause your game to crash!
Managing multiple Gx::ResourceContainer can be tedious, especially when dealing with dozens of concrete resource types. This is where Gx::ResourceManager comes into play.
Gx::ResourceManager is a class that orchestrates multiple Gx::ResourceContainer simultaneously. This means you can store multiple types of resources in one single resource manager object. In addition, the resource manager can also load resources for you and instantiate a new instance of a resource via the copy constructor.
The lifetime of resource containers is shared with the Gx::ResourceManager. Therefore, when the Gx::ResourceManager object gets destroyed, all resource containers it handles (along with its resources) will also be destroyed.
Gx::ResourceManager class exposes similar functions as the Gx::ResourceContainer: Store, Find, Each, Count, and Destroy. Each function accepts a template argument representing the resource type you're dealing with. Apart from this, the signature and return type remain the same as the Gx::ResourceContainer implementation.
You can also destroy all resources inside the resource manager by calling the Clear() function.
The resource manager can also load your resources from the three standard sources. Once loaded, the resource will be added to the resource manager. Use AddFromFile, AddFromMemory, and AddFromStream to load and store the resource from file, memory, or stream, respectively.
Gx::ResourceManager resources();
// Load texture from the file and add it to the resource manager
resources.AddFromFile<sf::Texture>("my-texture", "/path/to/the/texture.png", Gx::CacheMode::Reuse);In addition, the class provides the AddFromDeserializer function, which loads the resource using the specified deserializer function.
Gx::ResourceManager resources();
// Load texture from the deserializer function and add it to the resource manager
resources.AddFromDeserializer<sf::Texture>
(
"my-texture",
[] ()
{
return std::make_unique<sf::Texture>("/path/to/the/texture.png"); // return as Gx::ResourcePtr
},
Gx::CacheMode::Reuse
);Important
Gx::ResourceManager relies on Gx::ResourceLoaderFactory to create a resource loader for the requested type. Therefore, you must register the appropriate Gx::ResourceLoader to Gx::ResourceLoaderFactory to make these functions work properly.
Note
Similar to the Gx::ResourceContainer, the sources or deserializer won't be evaluated when you're using Gx::CacheMode::Reuse and the specified resource id is exists within the resource manager.
You can also instantiate a copy instance of a resource by calling the Instantiate function. This function also has overloads to load resources from three standard sources plus a deserializer whenever the specified resource id does not exist that are similar to the AddFromFile, AddFromMemory, AddFromStream, and AddFromDeserializer functions.
Gx::ResourceManager resources();
// Add a sprite to the resource manager
resources.AddFromFile<Gx::Sprite>("my-sprite", "/path/to/the/sprite.json", Gx::CacheMode::Reuse);
// Instantiate a new instance of sprite using `my-sprite`
Gx::ResourcePtr<Gx::Sprite> anotherSprite = resources.Instantiate<Gx::Sprite>("my-sprite");Note that the function returns Gx::ResourcePtr, and the ownership of the instantiated resource belongs to you, not the Gx::ResourceManager. Therefore, it will not be destroyed when Gx::ResourceManager is destroyed. You will also be unable to locate the resource via the Find function.
Caution
The resource is instantiated via the copy constructor. As such, you cannot instantiate resources if the type is not copiable.
Copyright © 2024 - CXO2
This open-source library is licensed under the zlib/libpng license.