1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-15 11:03:00 +01:00

Implement multiple texture atlas system to handle small and large images

This commit is contained in:
Alexander Overvoorde
2016-07-23 18:33:53 +02:00
parent c8a0cedf33
commit bb3fe8b804
9 changed files with 159 additions and 90 deletions

View File

@@ -1,11 +1,12 @@
#version 150
uniform vec4 uPalette[256];
uniform usampler2D uTexture;
uniform usampler2DArray uTexture;
flat in ivec4 fClip;
flat in int fFlags;
in vec4 fColour;
flat in int fTexAtlasIndex;
in vec2 fTexColourCoords;
in vec2 fTexMaskCoords;
flat in int fMask;
@@ -23,8 +24,8 @@ void main()
discard;
}
vec4 mask = uPalette[texture(uTexture, fTexMaskCoords).r];
vec4 texel = uPalette[texture(uTexture, fTexColourCoords).r];
vec4 mask = uPalette[texture(uTexture, vec3(fTexMaskCoords, float(fTexAtlasIndex))).r];
vec4 texel = uPalette[texture(uTexture, vec3(fTexColourCoords, float(fTexAtlasIndex))).r];
if (fMask != 0)
{

View File

@@ -3,6 +3,7 @@
uniform ivec2 uScreenSize;
in ivec4 ivClip;
in int ivTexAtlasIndex;
in vec4 ivTexColourBounds;
in vec4 ivTexMaskBounds;
in int ivFlags;
@@ -17,6 +18,7 @@ out vec2 fPosition;
flat out ivec4 fClip;
flat out int fFlags;
out vec4 fColour;
flat out int fTexAtlasIndex;
out vec2 fTexColourCoords;
out vec2 fTexMaskCoords;
flat out int fMask;
@@ -58,6 +60,7 @@ void main()
fFlags = ivFlags;
fColour = ivColour;
fMask = ivMask;
fTexAtlasIndex = ivTexAtlasIndex;
gl_Position = vec4(pos, 0.0, 1.0);
}

View File

@@ -36,6 +36,7 @@ DrawImageShader::DrawImageShader() : OpenGLShaderProgram("drawimage")
glBindBuffer(GL_ARRAY_BUFFER, _vboInstances);
glVertexAttribIPointer(vClip, 4, GL_INT, sizeof(DrawImageInstance), (void*) offsetof(DrawImageInstance, clip));
glVertexAttribIPointer(vTexAtlasIndex, 1, GL_INT, sizeof(DrawImageInstance), (void*) offsetof(DrawImageInstance, texAtlasIndex));
glVertexAttribPointer(vTexColourBounds, 4, GL_FLOAT, GL_FALSE, sizeof(DrawImageInstance), (void*) offsetof(DrawImageInstance, texColourBounds));
glVertexAttribPointer(vTexMaskBounds, 4, GL_FLOAT, GL_FALSE, sizeof(DrawImageInstance), (void*) offsetof(DrawImageInstance, texMaskBounds));
glVertexAttribIPointer(vFlags, 1, GL_INT, sizeof(DrawImageInstance), (void*) offsetof(DrawImageInstance, flags));
@@ -45,6 +46,7 @@ DrawImageShader::DrawImageShader() : OpenGLShaderProgram("drawimage")
glEnableVertexAttribArray(vIndex);
glEnableVertexAttribArray(vClip);
glEnableVertexAttribArray(vTexAtlasIndex);
glEnableVertexAttribArray(vTexColourBounds);
glEnableVertexAttribArray(vTexMaskBounds);
glEnableVertexAttribArray(vFlags);
@@ -53,6 +55,7 @@ DrawImageShader::DrawImageShader() : OpenGLShaderProgram("drawimage")
glEnableVertexAttribArray(vMask);
glVertexAttribDivisor(vClip, 1);
glVertexAttribDivisor(vTexAtlasIndex, 1);
glVertexAttribDivisor(vTexColourBounds, 1);
glVertexAttribDivisor(vTexMaskBounds, 1);
glVertexAttribDivisor(vFlags, 1);
@@ -81,6 +84,7 @@ void DrawImageShader::GetLocations()
vIndex = GetAttributeLocation("vIndex");
vClip = GetAttributeLocation("ivClip");
vTexAtlasIndex = GetAttributeLocation("ivTexAtlasIndex");
vTexColourBounds = GetAttributeLocation("ivTexColourBounds");
vTexMaskBounds = GetAttributeLocation("ivTexMaskBounds");
vFlags = GetAttributeLocation("ivFlags");

View File

@@ -24,6 +24,7 @@
// Per-instance data for images
struct DrawImageInstance {
vec4i clip;
int texAtlasIndex;
vec4f texColourBounds;
vec4f texMaskBounds;
int flags;
@@ -41,6 +42,7 @@ private:
GLuint vIndex;
GLuint vClip;
GLuint vTexAtlasIndex;
GLuint vTexColourBounds;
GLuint vTexMaskBounds;
GLuint vFlags;

View File

@@ -68,7 +68,8 @@ static const char * TryLoadAllProcAddresses()
SetupOpenGLFunction(glTexImage2D);
SetupOpenGLFunction(glTexParameteri);
SetupOpenGLFunction(glViewport);
SetupOpenGLFunction(glTexSubImage2D);
SetupOpenGLFunction(glTexSubImage3D);
SetupOpenGLFunction(glTexImage3D);
// 2.0+ functions
SetupOpenGLFunction(glAttachShader);

View File

@@ -40,7 +40,8 @@
#define glTexImage2D __static__glTexImage2D
#define glTexParameteri __static__glTexParameteri
#define glViewport __static__glViewport
#define glTexSubImage2D __static__glTexSubImage2D
#define glTexSubImage3D __static__glTexSubImage3D
#define glTexImage3D __static__glTexImage3D
#endif
@@ -68,7 +69,8 @@
#undef glTexImage2D
#undef glTexParameteri
#undef glViewport
#undef glTexSubImage2D
#undef glTexSubImage3D
#undef glTexImage3D
// 1.1 function signatures
typedef void (APIENTRYP PFNGLBEGINPROC )(GLenum mode);
@@ -89,7 +91,8 @@ typedef void (APIENTRYP PFNGLREADPIXELSPROC )(GLint x, GLint y, GLsizei wid
typedef void (APIENTRYP PFNGLTEXIMAGE2DPROC )(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
typedef void (APIENTRYP PFNGLTEXPARAMETERIPROC )(GLenum target, GLenum pname, GLint param);
typedef void (APIENTRYP PFNGLVIEWPORTPROC )(GLint x, GLint y, GLsizei width, GLsizei height);
typedef void (APIENTRYP PFNGLTEXSUBIMAGE2DPROC )(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid* data);
typedef void (APIENTRYP PFNGLTEXSUBIMAGE3DPROC )(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const GLvoid* data);
typedef void (APIENTRYP PFNGLTEXIMAGE3DPROC )(GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid * data);
#ifdef NO_EXTERN_GLAPI
// Defines the function pointers
@@ -121,7 +124,8 @@ GLAPI_DECL PFNGLREADPIXELSPROC glReadPixels GLAP
GLAPI_DECL PFNGLTEXIMAGE2DPROC glTexImage2D GLAPI_SET;
GLAPI_DECL PFNGLTEXPARAMETERIPROC glTexParameteri GLAPI_SET;
GLAPI_DECL PFNGLVIEWPORTPROC glViewport GLAPI_SET;
GLAPI_DECL PFNGLTEXSUBIMAGE2DPROC glTexSubImage2D GLAPI_SET;
GLAPI_DECL PFNGLTEXSUBIMAGE3DPROC glTexSubImage3D GLAPI_SET;
GLAPI_DECL PFNGLTEXIMAGE3DPROC glTexImage3D GLAPI_SET;
// 2.0+ function pointers
GLAPI_DECL PFNGLATTACHSHADERPROC glAttachShader GLAPI_SET;

View File

@@ -943,7 +943,7 @@ void OpenGLDrawingContext::FlushLines() {
void OpenGLDrawingContext::FlushImages() {
if (_commandBuffers.images.size() == 0) return;
OpenGLAPI::SetTexture(0, GL_TEXTURE_2D, _textureCache->GetAtlasTexture());
OpenGLAPI::SetTexture(0, GL_TEXTURE_2D_ARRAY, _textureCache->GetAtlasTextureArray());
std::vector<DrawImageInstance> instances;
instances.reserve(_commandBuffers.images.size());
@@ -952,8 +952,9 @@ void OpenGLDrawingContext::FlushImages() {
DrawImageInstance instance;
instance.clip = {command.clip[0], command.clip[1], command.clip[2], command.clip[3]};
instance.texColourBounds = command.texColour.bounds;
instance.texMaskBounds = command.texMask.bounds;
instance.texAtlasIndex = command.texColour.index;
instance.texColourBounds = command.texColour.normalizedBounds;
instance.texMaskBounds = command.texMask.normalizedBounds;
instance.flags = command.flags;
instance.colour = command.colour;
instance.bounds = {command.bounds[0], command.bounds[1], command.bounds[2], command.bounds[3]};

View File

@@ -17,6 +17,7 @@
#ifndef DISABLE_OPENGL
#include <vector>
#include <stdexcept>
#include "../../../core/Memory.hpp"
#include "TextureCache.h"
@@ -41,19 +42,19 @@ void TextureCache::SetPalette(const SDL_Color * palette)
void TextureCache::InvalidateImage(uint32 image)
{
InitializeAtlasTexture();
InitialiseAtlases();
auto kvp = _imageTextureMap.find(image);
if (kvp != _imageTextureMap.end())
{
_atlases[kvp->second.index].Free(kvp->second);
_imageTextureMap.erase(kvp);
_freeSlots.push_back(kvp->second.slot);
}
}
CachedTextureInfo TextureCache::GetOrLoadImageTexture(uint32 image)
{
InitializeAtlasTexture();
InitialiseAtlases();
auto kvp = _imageTextureMap.find(image & 0x7FFFF);
if (kvp != _imageTextureMap.end())
@@ -64,14 +65,12 @@ CachedTextureInfo TextureCache::GetOrLoadImageTexture(uint32 image)
auto cacheInfo = LoadImageTexture(image);
_imageTextureMap[image & 0x7FFFF] = cacheInfo;
//printf("%d slots left\n", (int) _freeSlots.size()); // TODO REMOVE
return cacheInfo;
}
CachedTextureInfo TextureCache::GetOrLoadGlyphTexture(uint32 image, uint8 * palette)
{
InitializeAtlasTexture();
InitialiseAtlases();
GlyphId glyphId;
glyphId.Image = image;
@@ -86,65 +85,49 @@ CachedTextureInfo TextureCache::GetOrLoadGlyphTexture(uint32 image, uint8 * pale
auto cacheInfo = LoadGlyphTexture(image, palette);
_glyphTextureMap[glyphId] = cacheInfo;
//printf("%d slots left\n", (int) _freeSlots.size()); // TODO REMOVE
return cacheInfo;
}
void TextureCache::InitializeAtlasTexture() {
if (!_atlasTextureInitialized) {
glGenTextures(1, &_atlasTexture);
glBindTexture(GL_TEXTURE_2D, _atlasTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_R8UI, TEXTURE_CACHE_ATLAS_WIDTH, TEXTURE_CACHE_ATLAS_HEIGHT, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
void TextureCache::InitialiseAtlases() {
if (!_atlasInitialised) {
// Create an array texture to hold all of the atlases
glGenTextures(1, &_atlasTextureArray);
glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray);
glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_R8UI, TEXTURE_CACHE_ATLAS_WIDTH, TEXTURE_CACHE_ATLAS_HEIGHT, _atlases.size(), 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
_freeSlots.resize(TEXTURE_CACHE_MAX_IMAGES);
for (size_t i = 0; i < _freeSlots.size(); i++) _freeSlots[i] = i;
_atlasTextureInitialized = true;
_atlasInitialised = true;
}
}
CachedTextureInfo TextureCache::LoadImageTexture(uint32 image)
{
rct_drawpixelinfo * dpi = GetImageAsDPI(image, 0);
GLuint slot = _freeSlots.back();
_freeSlots.pop_back();
if (dpi->width > TEXTURE_CACHE_MAX_IMAGE_WIDTH) dpi->width = TEXTURE_CACHE_MAX_IMAGE_WIDTH;
if (dpi->height > TEXTURE_CACHE_MAX_IMAGE_HEIGHT) dpi->height = TEXTURE_CACHE_MAX_IMAGE_HEIGHT;
vec4i coords = CalculateAtlasCoordinates(slot, dpi->width, dpi->height);
glBindTexture(GL_TEXTURE_2D, _atlasTexture);
glTexSubImage2D(GL_TEXTURE_2D, 0, coords.x, coords.y, dpi->width, dpi->height, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits);
auto cacheInfo = AllocateFromAppropriateAtlas(dpi->width, dpi->height);
glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi->width, dpi->height, 1, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits);
DeleteDPI(dpi);
return {slot, ConvertToNormalizedCoordinates(coords)};
return cacheInfo;
}
CachedTextureInfo TextureCache::LoadGlyphTexture(uint32 image, uint8 * palette)
{
rct_drawpixelinfo * dpi = GetGlyphAsDPI(image, palette);
GLuint slot = _freeSlots.back();
_freeSlots.pop_back();
auto cacheInfo = AllocateFromAppropriateAtlas(dpi->width, dpi->height);
if (dpi->width > TEXTURE_CACHE_MAX_IMAGE_WIDTH) dpi->width = TEXTURE_CACHE_MAX_IMAGE_WIDTH;
if (dpi->height > TEXTURE_CACHE_MAX_IMAGE_HEIGHT) dpi->height = TEXTURE_CACHE_MAX_IMAGE_HEIGHT;
vec4i coords = CalculateAtlasCoordinates(slot, dpi->width, dpi->height);
glBindTexture(GL_TEXTURE_2D, _atlasTexture);
glTexSubImage2D(GL_TEXTURE_2D, 0, coords.x, coords.y, dpi->width, dpi->height, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits);
glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasTextureArray);
glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi->width, dpi->height, 1, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi->bits);
DeleteDPI(dpi);
return{slot, ConvertToNormalizedCoordinates(coords)};
return cacheInfo;
}
void * TextureCache::GetImageAsARGB(uint32 image, uint32 tertiaryColour, uint32 * outWidth, uint32 * outHeight)
@@ -163,6 +146,16 @@ void * TextureCache::GetImageAsARGB(uint32 image, uint32 tertiaryColour, uint32
return pixels32;
}
CachedTextureInfo TextureCache::AllocateFromAppropriateAtlas(int imageWidth, int imageHeight) {
for (Atlas& atlas : _atlases) {
if (atlas.GetFreeSlots() > 0 && atlas.SupportsImage(imageWidth, imageHeight)) {
return atlas.Allocate(imageWidth, imageHeight);
}
}
throw std::runtime_error("no atlas with free slots left that supports image!");
}
rct_drawpixelinfo * TextureCache::GetImageAsDPI(uint32 image, uint32 tertiaryColour)
{
rct_g1_element * g1Element = gfx_get_g1_element(image & 0x7FFFF);
@@ -233,28 +226,7 @@ void * TextureCache::ConvertDPIto32bpp(const rct_drawpixelinfo * dpi)
void TextureCache::FreeTextures()
{
// Free array texture
glDeleteTextures(1, &_atlasTexture);
}
vec4i TextureCache::CalculateAtlasCoordinates(GLuint slot, int width, int height) {
int row = slot / TEXTURE_CACHE_IMAGES_U;
int col = slot % TEXTURE_CACHE_IMAGES_U;
return vec4i{
TEXTURE_CACHE_MAX_IMAGE_WIDTH * col,
TEXTURE_CACHE_MAX_IMAGE_HEIGHT * row,
TEXTURE_CACHE_MAX_IMAGE_WIDTH * col + width,
TEXTURE_CACHE_MAX_IMAGE_HEIGHT * row + height,
};
}
vec4f TextureCache::ConvertToNormalizedCoordinates(vec4i coordinates) {
return vec4f{
coordinates.x / (float) TEXTURE_CACHE_ATLAS_WIDTH,
coordinates.y / (float) TEXTURE_CACHE_ATLAS_HEIGHT,
coordinates.z / (float) TEXTURE_CACHE_ATLAS_WIDTH,
coordinates.w / (float) TEXTURE_CACHE_ATLAS_HEIGHT
};
glDeleteTextures(1, &_atlasTextureArray);
}
rct_drawpixelinfo * TextureCache::CreateDPI(sint32 width, sint32 height)
@@ -280,8 +252,8 @@ void TextureCache::DeleteDPI(rct_drawpixelinfo* dpi)
delete dpi;
}
GLuint TextureCache::GetAtlasTexture() {
return _atlasTexture;
GLuint TextureCache::GetAtlasTextureArray() {
return _atlasTextureArray;
}
#endif /* DISABLE_OPENGL */

View File

@@ -17,6 +17,7 @@
#pragma once
#include <unordered_map>
#include <array>
#include <SDL_pixels.h>
#include "../../../common.h"
#include "OpenGLAPI.h"
@@ -52,27 +53,108 @@ struct GlyphId
};
// TODO: Derive from hardware limits instead
// TODO: Support > 64x64 images
// TODO: Handle no more slots remaining (allocate more atlases?)
// TODO: Handle images larger than 256x256
constexpr int TEXTURE_CACHE_ATLAS_WIDTH = 8192;
constexpr int TEXTURE_CACHE_ATLAS_HEIGHT = 8192;
constexpr int TEXTURE_CACHE_MAX_IMAGE_WIDTH = 64;
constexpr int TEXTURE_CACHE_MAX_IMAGE_HEIGHT = 64;
constexpr int TEXTURE_CACHE_IMAGES_U = TEXTURE_CACHE_ATLAS_WIDTH / TEXTURE_CACHE_MAX_IMAGE_WIDTH;
constexpr int TEXTURE_CACHE_IMAGES_V = TEXTURE_CACHE_ATLAS_HEIGHT / TEXTURE_CACHE_MAX_IMAGE_HEIGHT;
constexpr int TEXTURE_CACHE_MAX_IMAGES = TEXTURE_CACHE_IMAGES_U * TEXTURE_CACHE_IMAGES_V;
// Location of an image (texture atlas index, slot and normalized coordinates)
struct CachedTextureInfo {
GLuint index;
GLuint slot;
vec4f bounds;
vec4i bounds;
vec4f normalizedBounds;
};
// Represents a texture atlas that images of a given maximum size can be allocated from
// Atlases are all stored in the same 2D texture array, occupying the specified index
class Atlas {
private:
GLuint _index;
int _imageWidth, _imageHeight;
std::vector<GLuint> _freeSlots;
int _cols, _rows;
public:
Atlas(GLuint index, int imageWidth, int imageHeight) {
_index = index;
_imageWidth = imageWidth;
_imageHeight = imageHeight;
_cols = TEXTURE_CACHE_ATLAS_WIDTH / imageWidth;
_rows = TEXTURE_CACHE_ATLAS_HEIGHT / imageHeight;
_freeSlots.resize(_cols * _rows);
for (size_t i = 0; i < _freeSlots.size(); i++) {
_freeSlots[i] = i;
}
}
CachedTextureInfo Allocate(int actualWidth, int actualHeight) {
assert(_freeSlots.size() > 0);
GLuint slot = _freeSlots.back();
_freeSlots.pop_back();
auto bounds = GetSlotCoordinates(slot, actualWidth, actualHeight);
#ifdef DEBUG
printf("texture atlas (%d, %d) has %d slots left\n", _imageWidth, _imageHeight, GetFreeSlots());
#endif
return {_index, slot, bounds, NormalizeCoordinates(bounds)};
}
void Free(const CachedTextureInfo& info) {
assert(_index == info.index);
_freeSlots.push_back(info.slot);
}
bool SupportsImage(int actualWidth, int actualHeight) const {
return actualWidth <= _imageWidth && actualHeight <= _imageHeight;
}
int GetFreeSlots() const {
return (int) _freeSlots.size();
}
private:
vec4i GetSlotCoordinates(GLuint slot, int actualWidth, int actualHeight) {
int row = slot / _cols;
int col = slot % _cols;
return vec4i{
_imageWidth * col,
_imageHeight * row,
_imageWidth * col + actualWidth,
_imageHeight * row + actualHeight,
};
}
static vec4f NormalizeCoordinates(const vec4i& coords) {
return vec4f{
coords.x / (float) TEXTURE_CACHE_ATLAS_WIDTH,
coords.y / (float) TEXTURE_CACHE_ATLAS_HEIGHT,
coords.z / (float) TEXTURE_CACHE_ATLAS_WIDTH,
coords.w / (float) TEXTURE_CACHE_ATLAS_HEIGHT
};
}
};
class TextureCache
{
private:
bool _atlasTextureInitialized = false;
GLuint _atlasTexture;
bool _atlasInitialised = false;
std::vector<GLuint> _freeSlots;
GLuint _atlasTextureArray;
// Atlases should be ordered from small to large image support
std::array<Atlas, 2> _atlases = {
Atlas{0, 64, 64},
Atlas{1, 256, 256}
};
std::unordered_map<uint32, CachedTextureInfo> _imageTextureMap;
std::unordered_map<GlyphId, CachedTextureInfo, GlyphId::Hash, GlyphId::Equal> _glyphTextureMap;
@@ -87,20 +169,19 @@ public:
CachedTextureInfo GetOrLoadImageTexture(uint32 image);
CachedTextureInfo GetOrLoadGlyphTexture(uint32 image, uint8 * palette);
GLuint GetAtlasTexture();
GLuint GetAtlasTextureArray();
private:
void InitializeAtlasTexture();
void InitialiseAtlases();
CachedTextureInfo LoadImageTexture(uint32 image);
CachedTextureInfo LoadGlyphTexture(uint32 image, uint8 * palette);
CachedTextureInfo AllocateFromAppropriateAtlas(int imageWidth, int imageHeight);
void * GetImageAsARGB(uint32 image, uint32 tertiaryColour, uint32 * outWidth, uint32 * outHeight);
rct_drawpixelinfo * GetImageAsDPI(uint32 image, uint32 tertiaryColour);
void * GetGlyphAsARGB(uint32 image, uint8 * palette, uint32 * outWidth, uint32 * outHeight);
rct_drawpixelinfo * GetGlyphAsDPI(uint32 image, uint8 * palette);
void * ConvertDPIto32bpp(const rct_drawpixelinfo * dpi);
void FreeTextures();
vec4i CalculateAtlasCoordinates(GLuint slot, int width, int height);
vec4f ConvertToNormalizedCoordinates(vec4i coordinates);
static rct_drawpixelinfo * CreateDPI(sint32 width, sint32 height);
static void DeleteDPI(rct_drawpixelinfo * dpi);