Browse Source

Initial support for writing to textures in compute shaders.

Alex Szpakowski 4 years ago
parent
commit
a551fcd492

+ 1 - 1
src/modules/graphics/Graphics.cpp

@@ -768,7 +768,7 @@ void Graphics::setRenderTargets(const RenderTargets &rts)
 		PixelFormat dsformat = PIXELFORMAT_STENCIL8;
 		if (wantsdepth && wantsstencil)
 			dsformat = PIXELFORMAT_DEPTH24_UNORM_STENCIL8;
-		else if (wantsdepth && isPixelFormatSupported(PIXELFORMAT_DEPTH24_UNORM, true, false, false))
+		else if (wantsdepth && isPixelFormatSupported(PIXELFORMAT_DEPTH24_UNORM, PIXELFORMATUSAGEFLAGS_RENDERTARGET, false))
 			dsformat = PIXELFORMAT_DEPTH24_UNORM;
 		else if (wantsdepth)
 			dsformat = PIXELFORMAT_DEPTH16_UNORM;

+ 2 - 2
src/modules/graphics/Graphics.h

@@ -787,9 +787,9 @@ public:
 	virtual PixelFormat getSizedFormat(PixelFormat format, bool rendertarget, bool readable) const = 0;
 
 	/**
-	 * Gets whether the specified pixel format is supported.
+	 * Gets whether the specified pixel format usage is supported.
 	 **/
-	virtual bool isPixelFormatSupported(PixelFormat format, bool rendertarget, bool readable, bool sRGB = false) = 0;
+	virtual bool isPixelFormatSupported(PixelFormat format, PixelFormatUsageFlags usage, bool sRGB = false) = 0;
 
 	/**
 	 * Gets the renderer used by love.graphics.

+ 76 - 5
src/modules/graphics/Shader.cpp

@@ -731,6 +731,55 @@ bool Shader::validate(StrongRef<ShaderStage> stages[], std::string& err)
 	return validateInternal(stages, err, reflection);
 }
 
+static PixelFormat getPixelFormat(glslang::TLayoutFormat format)
+{
+	using namespace glslang;
+
+	switch (format)
+	{
+		case ElfNone: return PIXELFORMAT_UNKNOWN;
+		case ElfRgba32f: return PIXELFORMAT_RGBA32_FLOAT;
+		case ElfRgba16f: return PIXELFORMAT_RGBA16_FLOAT;
+		case ElfR32f: return PIXELFORMAT_R32_FLOAT;
+		case ElfRgba8: return PIXELFORMAT_RGBA8_UNORM;
+		case ElfRgba8Snorm: return PIXELFORMAT_UNKNOWN; // no snorm yet
+		case ElfRg32f: return PIXELFORMAT_RG32_FLOAT;
+		case ElfRg16f: return PIXELFORMAT_RG16_FLOAT;
+		case ElfR11fG11fB10f: return PIXELFORMAT_RG11B10_FLOAT;
+		case ElfR16f: return PIXELFORMAT_R16_FLOAT;
+		case ElfRgba16: return PIXELFORMAT_RGBA16_UNORM;
+		case ElfRgb10A2: return PIXELFORMAT_RGB10A2_UNORM;
+		case ElfRg16: return PIXELFORMAT_RG16_UNORM;
+		case ElfRg8: return PIXELFORMAT_RG8_UNORM;
+		case ElfR8: return PIXELFORMAT_R8_UNORM;
+		case ElfRgba16Snorm: return PIXELFORMAT_UNKNOWN;
+		case ElfRg16Snorm: return PIXELFORMAT_UNKNOWN;
+		case ElfRg8Snorm: return PIXELFORMAT_UNKNOWN;
+		case ElfR16Snorm: return PIXELFORMAT_UNKNOWN;
+		case ElfR8Snorm: return PIXELFORMAT_UNKNOWN;
+		case ElfRgba32i: return PIXELFORMAT_RGBA32_INT;
+		case ElfRgba16i: return PIXELFORMAT_RGBA16_INT;
+		case ElfRgba8i: return PIXELFORMAT_RGBA8_INT;
+		case ElfR32i: return PIXELFORMAT_R32_INT;
+		case ElfRg32i: return PIXELFORMAT_RG32_INT;
+		case ElfRg16i: return PIXELFORMAT_RG16_INT;
+		case ElfRg8i: return PIXELFORMAT_RG8_INT;
+		case ElfR16i: return PIXELFORMAT_R16_INT;
+		case ElfR8i: return PIXELFORMAT_R8_INT;
+		case ElfRgba32ui: return PIXELFORMAT_RGBA32_UINT;
+		case ElfRgba16ui: return PIXELFORMAT_RGBA16_UINT;
+		case ElfRgba8ui: return PIXELFORMAT_RGBA8_UINT;
+		case ElfR32ui: return PIXELFORMAT_R32_UINT;
+		case ElfRg32ui: return PIXELFORMAT_RG32_UINT;
+		case ElfRg16ui: return PIXELFORMAT_RG16_UINT;
+		case ElfRgb10a2ui: return PIXELFORMAT_UNKNOWN;
+		case ElfRg8ui: return PIXELFORMAT_RG8_UINT;
+		case ElfR16ui: return PIXELFORMAT_R16_UINT;
+		case ElfR8ui: return PIXELFORMAT_R8_UINT;
+		default: return PIXELFORMAT_UNKNOWN;
+	}
+}
+
 bool Shader::validateInternal(StrongRef<ShaderStage> stages[], std::string &err, ValidationReflection &reflection)
 {
 	glslang::TProgram program;
@@ -781,12 +830,34 @@ bool Shader::validateInternal(StrongRef<ShaderStage> stages[], std::string &err,
 		if (type == nullptr)
 			continue;
 
+		const glslang::TQualifier &qualifiers = type->getQualifier();
+
 		if (type->isImage())
 		{
-			// TODO: Change this check to only error if a writable image is
-			// declared in non-compute, once support is added for them.
-			err = "Shader validation error:\nImage load/store (image2D uniforms, etc.) is not supported yet.";
-			return false;
+			if ((info.stages & EShLangComputeMask) == 0)
+			{
+				err = "Shader validation error:\nStorage Texture uniform variables (image2D, etc) are only allowed in compute shaders.";
+				return false;
+			}
+
+			if (!qualifiers.hasFormat())
+			{
+				err = "Shader validation error:\nStorage Texture '" + info.name + "' must have an explicit format set in its layout declaration.";
+				return false;
+			}
+
+			StorageTextureReflection texreflection = {};
+
+			texreflection.format = getPixelFormat(qualifiers.getFormat());
+
+			if (qualifiers.isReadOnly())
+				texreflection.access = ACCESS_READ;
+			else if (qualifiers.isWriteOnly())
+				texreflection.access = ACCESS_WRITE;
+			else
+				texreflection.access = (Access)(ACCESS_READ | ACCESS_WRITE);
+
+			reflection.storageTextures[info.name] = texreflection;
 		}
 	}
 
@@ -798,7 +869,7 @@ bool Shader::validateInternal(StrongRef<ShaderStage> stages[], std::string &err,
 		{
 			const glslang::TQualifier &qualifiers = type->getQualifier();
 
-			if ((!qualifiers.isReadOnly() || qualifiers.isWriteOnly()) && (info.stages & (EShLangVertexMask | EShLangFragmentMask)))
+			if ((!qualifiers.isReadOnly() || qualifiers.isWriteOnly()) && (info.stages & EShLangComputeMask) == 0)
 			{
 				err = "Shader validation error:\nStorage Buffer block '" + info.name + "' must be marked as readonly in vertex and pixel shaders.";
 				return false;

+ 9 - 0
src/modules/graphics/Shader.h

@@ -76,6 +76,7 @@ public:
 		UNIFORM_UINT,
 		UNIFORM_BOOL,
 		UNIFORM_SAMPLER,
+		UNIFORM_STORAGETEXTURE,
 		UNIFORM_TEXELBUFFER,
 		UNIFORM_STORAGEBUFFER,
 		UNIFORM_UNKNOWN,
@@ -135,6 +136,7 @@ public:
 		TextureType textureType;
 		Access access;
 		bool isDepthSampler;
+		PixelFormat storageTextureFormat;
 		size_t bufferStride;
 		size_t bufferMemberCount;
 		std::string name;
@@ -251,9 +253,16 @@ protected:
 		Access access;
 	};
 
+	struct StorageTextureReflection
+	{
+		PixelFormat format;
+		Access access;
+	};
+
 	struct ValidationReflection
 	{
 		std::map<std::string, BufferReflection> storageBuffers;
+		std::map<std::string, StorageTextureReflection> storageTextures;
 		int localThreadgroupSize[3];
 		bool usesPointSize;
 	};

+ 19 - 2
src/modules/graphics/Texture.cpp

@@ -163,6 +163,7 @@ Texture::Texture(const Settings &settings, const Slices *slices)
 	: texType(settings.type)
 	, format(settings.format)
 	, renderTarget(settings.renderTarget)
+	, computeWrite(settings.computeWrite)
 	, readable(true)
 	, mipmapsMode(settings.mipmaps)
 	, sRGB(isGammaCorrect() && !settings.linear)
@@ -257,7 +258,15 @@ Texture::Texture(const Settings &settings, const Slices *slices)
 	if (isCompressed() && renderTarget)
 		throw love::Exception("Compressed textures cannot be render targets.");
 
-	if (!gfx->isPixelFormatSupported(format, renderTarget, readable, sRGB))
+	uint32 usage = PIXELFORMATUSAGEFLAGS_NONE;
+	if (renderTarget)
+		usage |= PIXELFORMATUSAGEFLAGS_RENDERTARGET;
+	if (readable)
+		usage |= PIXELFORMATUSAGEFLAGS_SAMPLE;
+	if (computeWrite)
+		usage |= PIXELFORMATUSAGEFLAGS_COMPUTEWRITE;
+
+	if (!gfx->isPixelFormatSupported(format, (PixelFormatUsageFlags) usage, sRGB))
 	{
 		const char *fstr = "unknown";
 		love::getConstant(format, fstr);
@@ -267,7 +276,9 @@ Texture::Texture(const Settings &settings, const Slices *slices)
 			readablestr = readable ? " readable" : " non-readable";
 
 		const char *rtstr = "";
-		if (renderTarget)
+		if (computeWrite)
+			rtstr = " as a compute shader-writable texture";
+		else if (renderTarget)
 			rtstr = " as a render target";
 
 		throw love::Exception("The %s%s pixel format is not supported%s on this system.", fstr, readablestr, rtstr);
@@ -571,6 +582,11 @@ bool Texture::isRenderTarget() const
 	return renderTarget;
 }
 
+bool Texture::isComputeWritable() const
+{
+	return computeWrite;
+}
+
 bool Texture::isReadable() const
 {
 	return readable;
@@ -934,6 +950,7 @@ static StringMap<Texture::SettingType, Texture::SETTING_MAX_ENUM>::Entry setting
 	{ "dpiscale",     Texture::SETTING_DPI_SCALE     },
 	{ "msaa",         Texture::SETTING_MSAA          },
 	{ "canvas",       Texture::SETTING_RENDER_TARGET },
+	{ "computewrite", Texture::SETTING_COMPUTE_WRITE },
 	{ "readable",     Texture::SETTING_READABLE      },
 };
 

+ 6 - 0
src/modules/graphics/Texture.h

@@ -63,6 +63,7 @@ enum PixelFormatUsage
 	PIXELFORMATUSAGE_RENDERTARGET,
 	PIXELFORMATUSAGE_BLEND,
 	PIXELFORMATUSAGE_MSAA,
+	PIXELFORMATUSAGE_COMPUTEWRITE,
 	PIXELFORMATUSAGE_MAX_ENUM
 };
 
@@ -74,6 +75,7 @@ enum PixelFormatUsageFlags
 	PIXELFORMATUSAGEFLAGS_RENDERTARGET = (1 << PIXELFORMATUSAGE_RENDERTARGET),
 	PIXELFORMATUSAGEFLAGS_BLEND = (1 << PIXELFORMATUSAGE_BLEND),
 	PIXELFORMATUSAGEFLAGS_MSAA = (1 << PIXELFORMATUSAGE_MSAA),
+	PIXELFORMATUSAGEFLAGS_COMPUTEWRITE = (1 << PIXELFORMATUSAGE_COMPUTEWRITE),
 };
 
 struct SamplerState
@@ -169,6 +171,7 @@ public:
 		SETTING_DPI_SCALE,
 		SETTING_MSAA,
 		SETTING_RENDER_TARGET,
+		SETTING_COMPUTE_WRITE,
 		SETTING_READABLE,
 		SETTING_MAX_ENUM
 	};
@@ -186,6 +189,7 @@ public:
 		float dpiScale = 1.0f;
 		int msaa = 1;
 		bool renderTarget = false;
+		bool computeWrite = false;
 		OptionalBool readable;
 	};
 
@@ -249,6 +253,7 @@ public:
 	MipmapsMode getMipmapsMode() const;
 
 	bool isRenderTarget() const;
+	bool isComputeWritable() const;
 	bool isReadable() const;
 
 	bool isCompressed() const;
@@ -307,6 +312,7 @@ protected:
 
 	PixelFormat format;
 	bool renderTarget;
+	bool computeWrite;
 	bool readable;
 
 	MipmapsMode mipmapsMode;

+ 10 - 12
src/modules/graphics/opengl/Graphics.cpp

@@ -501,9 +501,9 @@ static bool computeDispatchBarriers(Shader *shader, GLbitfield &preDispatchBarri
 			postDispatchBarriers |= GL_PIXEL_BUFFER_BARRIER_BIT;
 	}
 
-	for (auto texture : shader->getActiveWritableTextures())
+	for (const auto &binding : shader->getStorageTextureBindings())
 	{
-		if (texture == nullptr)
+		if (binding.texture == nullptr)
 			return false;
 
 		preDispatchBarriers |= GL_SHADER_IMAGE_ACCESS_BARRIER_BIT;
@@ -512,7 +512,7 @@ static bool computeDispatchBarriers(Shader *shader, GLbitfield &preDispatchBarri
 			| GL_TEXTURE_UPDATE_BARRIER_BIT
 			| GL_TEXTURE_FETCH_BARRIER_BIT;
 
-		if (texture->isRenderTarget())
+		if (binding.texture->isRenderTarget())
 			postDispatchBarriers |= GL_FRAMEBUFFER_BARRIER_BIT;
 	}
 
@@ -1684,7 +1684,7 @@ PixelFormat Graphics::getSizedFormat(PixelFormat format, bool rendertarget, bool
 	}
 }
 
-bool Graphics::isPixelFormatSupported(PixelFormat format, bool rendertarget, bool readable, bool sRGB)
+bool Graphics::isPixelFormatSupported(PixelFormat format, PixelFormatUsageFlags usage, bool sRGB)
 {
 	if (sRGB && format == PIXELFORMAT_RGBA8_UNORM)
 	{
@@ -1692,22 +1692,20 @@ bool Graphics::isPixelFormatSupported(PixelFormat format, bool rendertarget, boo
 		sRGB = false;
 	}
 
-	uint32 requiredflags = 0;
-	if (rendertarget)
-		requiredflags |= PIXELFORMATUSAGEFLAGS_RENDERTARGET;
-	if (readable)
-		requiredflags |= PIXELFORMATUSAGEFLAGS_SAMPLE;
+	bool rendertarget = (usage & PIXELFORMATUSAGEFLAGS_RENDERTARGET) != 0;
+	bool readable = (usage & PIXELFORMATUSAGEFLAGS_SAMPLE) != 0;
+	bool computewrite = (usage & PIXELFORMATUSAGEFLAGS_COMPUTEWRITE) != 0;
 
 	format = getSizedFormat(format, rendertarget, readable);
 
-	OptionalBool &supported = supportedFormats[format][rendertarget ? 1 : 0][readable ? 1 : 0][sRGB ? 1 : 0];
+	OptionalBool &supported = supportedFormats[format][rendertarget ? 1 : 0][readable ? 1 : 0][computewrite ? 1 : 0][sRGB ? 1 : 0];
 
 	if (supported.hasValue)
 		return supported.value;
 
-	auto supportedflags = OpenGL::getPixelFormatUsageFlags(format);
+	uint32 supportedflags = OpenGL::getPixelFormatUsageFlags(format);
 
-	if ((requiredflags & supportedflags) != requiredflags)
+	if ((usage & supportedflags) != usage)
 	{
 		supported.set(false);
 		return supported.value;

+ 3 - 3
src/modules/graphics/opengl/Graphics.h

@@ -107,7 +107,7 @@ public:
 	void setWireframe(bool enable) override;
 
 	PixelFormat getSizedFormat(PixelFormat format, bool rendertarget, bool readable) const override;
-	bool isPixelFormatSupported(PixelFormat format, bool rendertarget, bool readable, bool sRGB = false) override;
+	bool isPixelFormatSupported(PixelFormat format, PixelFormatUsageFlags usage, bool sRGB = false) override;
 	Renderer getRenderer() const override;
 	RendererInfo getRendererInfo() const override;
 
@@ -170,8 +170,8 @@ private:
 	// Only needed for buffer types that can be bound to shaders.
 	StrongRef<love::graphics::Buffer> defaultBuffers[BUFFERUSAGE_MAX_ENUM];
 
-	// [rendertarget][readable][srgb]
-	OptionalBool supportedFormats[PIXELFORMAT_MAX_ENUM][2][2][2];
+	// [rendertarget][readable][computewrite][srgb]
+	OptionalBool supportedFormats[PIXELFORMAT_MAX_ENUM][2][2][2][2];
 
 }; // Graphics
 

+ 47 - 1
src/modules/graphics/opengl/OpenGL.cpp

@@ -292,7 +292,7 @@ void OpenGL::setupContext()
 	// This can't be done in initContext with the rest of the bug checks because
 	// isPixelFormatSupported relies on state initialized here / after init.
 	auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
-	if (GLAD_ES_VERSION_3_0 && gfx != nullptr && !gfx->isPixelFormatSupported(PIXELFORMAT_R8_UNORM, true, true))
+	if (GLAD_ES_VERSION_3_0 && gfx != nullptr && !gfx->isPixelFormatSupported(PIXELFORMAT_R8_UNORM, PIXELFORMATUSAGEFLAGS_SAMPLE | PIXELFORMATUSAGEFLAGS_RENDERTARGET))
 		bugs.brokenR8PixelFormat = true;
 #endif
 }
@@ -2085,6 +2085,7 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 {
 	const uint32 commonsample = PIXELFORMATUSAGEFLAGS_SAMPLE | PIXELFORMATUSAGEFLAGS_LINEAR;
 	const uint32 commonrender = PIXELFORMATUSAGEFLAGS_RENDERTARGET | PIXELFORMATUSAGEFLAGS_BLEND | PIXELFORMATUSAGEFLAGS_MSAA;
+	const uint32 computewrite = PIXELFORMATUSAGEFLAGS_COMPUTEWRITE;
 
 	uint32 flags = PIXELFORMATUSAGEFLAGS_NONE;
 
@@ -2096,11 +2097,15 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			flags |= commonsample | commonrender;
 		else if (pixelformat == PIXELFORMAT_R8_UNORM && (GLAD_ES_VERSION_2_0 || GLAD_VERSION_1_1))
 			flags |= commonsample; // We'll use OpenGL's luminance format internally.
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_RGBA8_UNORM:
 		flags |= commonsample;
 		if (GLAD_VERSION_1_0 || GLAD_ES_VERSION_3_0 || GLAD_OES_rgb8_rgba8 || GLAD_ARM_rgba8)
 			flags |= commonrender;
+		if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_sRGBA8_UNORM:
 		if (gl.bugs.brokenSRGB)
@@ -2110,6 +2115,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 		if (GLAD_ES_VERSION_3_0 || GLAD_VERSION_3_0
 			|| ((GLAD_ARB_framebuffer_sRGB || GLAD_EXT_framebuffer_sRGB) && (GLAD_VERSION_2_1 || GLAD_EXT_texture_sRGB)))
 			flags |= commonrender;
+		if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_R16_UNORM:
 	case PIXELFORMAT_RG16_UNORM:
@@ -2117,10 +2124,14 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			|| (GLAD_VERSION_1_1 && GLAD_ARB_texture_rg)
 			|| (GLAD_EXT_texture_norm16 && (GLAD_ES_VERSION_3_0 || GLAD_EXT_texture_rg)))
 			flags |= commonsample | commonrender;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_RGBA16_UNORM:
 		if (GLAD_VERSION_1_1 || GLAD_EXT_texture_norm16)
 			flags |= commonsample | commonrender;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_R16_FLOAT:
 	case PIXELFORMAT_RG16_FLOAT:
@@ -2132,6 +2143,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			flags |= commonrender;
 		if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear))
 			flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_RGBA16_FLOAT:
 		if (GLAD_VERSION_3_0 || (GLAD_VERSION_1_0 && GLAD_ARB_texture_float && GLAD_ARB_half_float_pixel))
@@ -2142,8 +2155,13 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			flags |= commonrender;
 		if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear))
 			flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR;
+		if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_R32_FLOAT:
+		if (GLAD_ES_VERSION_3_1)
+			flags |= computewrite;
+		// Fallthrough.
 	case PIXELFORMAT_RG32_FLOAT:
 		if (GLAD_VERSION_3_0 || (GLAD_VERSION_1_0 && GLAD_ARB_texture_float && GLAD_ARB_texture_rg))
 			flags |= commonsample | commonrender;
@@ -2151,6 +2169,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			flags |= commonsample;
 		if (!(GLAD_VERSION_1_1 || GLAD_ES_VERSION_3_0 || GLAD_OES_texture_half_float_linear))
 			flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_RGBA32_FLOAT:
 		if (GLAD_VERSION_3_0 || (GLAD_VERSION_1_0 && GLAD_ARB_texture_float))
@@ -2159,6 +2179,8 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 			flags |= commonsample;
 		if (!(GLAD_VERSION_1_1 || GLAD_OES_texture_float_linear))
 			flags &= ~PIXELFORMATUSAGEFLAGS_LINEAR;
+		if (GLAD_VERSION_4_3 || GLAD_ES_VERSION_3_1)
+			flags |= computewrite;
 		break;
 
 		case PIXELFORMAT_R8_INT:
@@ -2181,6 +2203,26 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 		case PIXELFORMAT_RGBA32_UINT:
 			if (GLAD_VERSION_3_0 || GLAD_ES_VERSION_3_0)
 				flags |= PIXELFORMATUSAGEFLAGS_SAMPLE | PIXELFORMATUSAGEFLAGS_RENDERTARGET;
+			if (GLAD_VERSION_4_3)
+				flags |= computewrite;
+			if (GLAD_ES_VERSION_3_1)
+			{
+				switch (pixelformat)
+				{
+				case PIXELFORMAT_RGBA8_INT:
+				case PIXELFORMAT_RGBA8_UINT:
+				case PIXELFORMAT_RGBA16_INT:
+				case PIXELFORMAT_RGBA16_UINT:
+				case PIXELFORMAT_R32_INT:
+				case PIXELFORMAT_R32_UINT:
+				case PIXELFORMAT_RGBA32_INT:
+				case PIXELFORMAT_RGBA32_UINT:
+					flags |= computewrite;
+					break;
+				default:
+					break;
+				}
+			}
 			break;
 
 	case PIXELFORMAT_LA8_UNORM:
@@ -2198,12 +2240,16 @@ uint32 OpenGL::getPixelFormatUsageFlags(PixelFormat pixelformat)
 	case PIXELFORMAT_RGB10A2_UNORM:
 		if (GLAD_ES_VERSION_3_0 || GLAD_VERSION_1_0)
 			flags |= commonsample | commonrender;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 	case PIXELFORMAT_RG11B10_FLOAT:
 		if (GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_texture_packed_float)
 			flags |= commonsample;
 		if (GLAD_VERSION_3_0 || GLAD_EXT_packed_float || GLAD_APPLE_color_buffer_packed_float)
 			flags |= commonrender;
+		if (GLAD_VERSION_4_3)
+			flags |= computewrite;
 		break;
 
 	case PIXELFORMAT_STENCIL8:

+ 159 - 15
src/modules/graphics/opengl/Shader.cpp

@@ -63,7 +63,7 @@ Shader::~Shader()
 		if (p.second.data != nullptr)
 			free(p.second.data);
 
-		if (p.second.baseType == UNIFORM_SAMPLER)
+		if (p.second.baseType == UNIFORM_SAMPLER || p.second.baseType == UNIFORM_STORAGETEXTURE)
 		{
 			for (int i = 0; i < p.second.count; i++)
 			{
@@ -158,6 +158,26 @@ void Shader::mapActiveUniforms()
 			for (int i = 0; i < u.count; i++)
 				textureUnits.push_back(unit);
 		}
+		else if (u.baseType == UNIFORM_STORAGETEXTURE)
+		{
+			StorageTextureBinding binding = {};
+			binding.gltexture = gl.getDefaultTexture(u.textureType, u.dataBaseType);
+			binding.type = u.textureType;
+
+			if ((u.access & (ACCESS_READ | ACCESS_WRITE)) != 0)
+				binding.access = GL_READ_WRITE;
+			else if ((u.access & ACCESS_WRITE) != 0)
+				binding.access = GL_WRITE_ONLY;
+			else if ((u.access & ACCESS_READ) != 0)
+				binding.access = GL_READ_ONLY;
+
+			bool sRGB = false;
+			auto fmt = OpenGL::convertPixelFormat(u.storageTextureFormat, false, sRGB);
+			binding.internalFormat = fmt.internalformat;
+
+			for (int i = 0; i < u.count; i++)
+				storageTextureBindings.push_back(binding);
+		}
 
 		// Make sure previously set uniform data is preserved, and shader-
 		// initialized values are retrieved.
@@ -183,6 +203,7 @@ void Shader::mapActiveUniforms()
 			case UNIFORM_INT:
 			case UNIFORM_BOOL:
 			case UNIFORM_SAMPLER:
+			case UNIFORM_STORAGETEXTURE:
 			case UNIFORM_TEXELBUFFER:
 				u.dataSize = sizeof(int) * u.components * u.count;
 				u.data = malloc(u.dataSize);
@@ -226,6 +247,17 @@ void Shader::mapActiveUniforms()
 						memset(u.textures, 0, sizeof(Texture *) * u.count);
 					}
 				}
+				else if (u.baseType == UNIFORM_STORAGETEXTURE)
+				{
+					int startbinding = (int) storageTextureBindings.size() - u.count;
+					for (int i = 0; i < u.count; i++)
+						u.ints[i] = startbinding + i;
+
+					glUniform1iv(u.location, u.count, u.ints);
+
+					u.textures = new love::graphics::Texture*[u.count];
+					memset(u.textures, 0, sizeof(Texture *) * u.count);
+				}
 			}
 
 			size_t offset = 0;
@@ -277,7 +309,7 @@ void Shader::mapActiveUniforms()
 		if (builtin != BUILTIN_MAX_ENUM)
 			builtinUniformInfo[(int)builtin] = &uniforms[u.name];
 
-		if (u.baseType == UNIFORM_SAMPLER)
+		if (u.baseType == UNIFORM_SAMPLER || u.baseType == UNIFORM_STORAGETEXTURE)
 		{
 			// Make sure all stored textures have their Volatiles loaded before
 			// the sendTextures call, since it calls getHandle().
@@ -401,7 +433,7 @@ void Shader::mapActiveUniforms()
 			if (p.second.data != nullptr)
 				free(p.second.data);
 
-			if (p.second.baseType == UNIFORM_SAMPLER)
+			if (p.second.baseType == UNIFORM_SAMPLER || p.second.baseType == UNIFORM_STORAGETEXTURE)
 			{
 				for (int i = 0; i < p.second.count; i++)
 				{
@@ -435,6 +467,8 @@ bool Shader::loadVolatile()
 	textureUnits.clear();
 	textureUnits.push_back(TextureUnit());
 
+	activeStorageBufferBindings.clear();
+
 	storageBufferBindingIndexToActiveBinding.resize(gl.getMaxShaderStorageBufferBindings(), std::make_pair(-1, -1));
 	activeStorageBufferBindings.clear();
 	activeWritableStorageBuffers.clear();
@@ -573,7 +607,7 @@ void Shader::attach()
 		// retain/release happens in Graphics::setShader.
 
 		// Make sure all textures are bound to their respective texture units.
-		for (int i = 0; i < (int) textureUnits.size(); ++i)
+		for (int i = 0; i < (int) textureUnits.size(); i++)
 		{
 			const TextureUnit &unit = textureUnits[i];
 			if (unit.active)
@@ -585,6 +619,12 @@ void Shader::attach()
 			}
 		}
 
+		for (size_t i = 0; i < storageTextureBindings.size(); i++)
+		{
+			const auto &binding = storageTextureBindings[i];
+			glBindImageTexture((GLuint) i, binding.gltexture, 0, GL_TRUE, 0, binding.access, binding.internalFormat);
+		}
+
 		for (auto bufferbinding : activeStorageBufferBindings)
 			gl.bindIndexedBuffer(bufferbinding.buffer, BUFFERUSAGE_SHADER_STORAGE, bufferbinding.bindingindex);
 
@@ -648,7 +688,7 @@ void Shader::updateUniform(const UniformInfo *info, int count, bool internalupda
 			break;
 		}
 	}
-	else if (type == UNIFORM_INT || type == UNIFORM_BOOL || type == UNIFORM_SAMPLER || type == UNIFORM_TEXELBUFFER)
+	else if (type == UNIFORM_INT || type == UNIFORM_BOOL || type == UNIFORM_SAMPLER || type == UNIFORM_STORAGETEXTURE || type == UNIFORM_TEXELBUFFER)
 	{
 		switch (info->components)
 		{
@@ -717,7 +757,10 @@ void Shader::sendTextures(const UniformInfo *info, love::graphics::Texture **tex
 
 void Shader::sendTextures(const UniformInfo *info, love::graphics::Texture **textures, int count, bool internalUpdate)
 {
-	if (info->baseType != UNIFORM_SAMPLER)
+	bool issampler = info->baseType == UNIFORM_SAMPLER;
+	bool isstoragetex = info->baseType == UNIFORM_STORAGETEXTURE;
+
+	if (!issampler && !isstoragetex)
 		return;
 
 	bool shaderactive = current == this;
@@ -770,6 +813,26 @@ void Shader::sendTextures(const UniformInfo *info, love::graphics::Texture **tex
 				else
 					throw love::Exception("Texture's data format base type must match the uniform variable declared in the shader (float, int, or uint).");
 			}
+			else if (isstoragetex && !tex->isComputeWritable())
+			{
+				if (internalUpdate)
+					continue;
+				else
+					throw love::Exception("Texture must be created with the computewrite flag set to true in order to be used with a storage texture (image2D etc) shader uniform variable.");
+			}
+			else if (isstoragetex && info->storageTextureFormat != getLinearPixelFormat(tex->getPixelFormat()))
+			{
+				if (internalUpdate)
+					continue;
+				else
+				{
+					const char *texpfstr = "unknown";
+					const char *shaderpfstr = "unknown";
+					love::getConstant(getLinearPixelFormat(tex->getPixelFormat()), texpfstr);
+					love::getConstant(info->storageTextureFormat, shaderpfstr);
+					throw love::Exception("Texture's pixel format (%s) must match the shader uniform variable %s's pixel format (%s)", texpfstr, info->name.c_str(), shaderpfstr);
+				}
+			}
 
 			tex->retain();
 		}
@@ -779,19 +842,39 @@ void Shader::sendTextures(const UniformInfo *info, love::graphics::Texture **tex
 
 		info->textures[i] = tex;
 
-		GLuint gltex = 0;
-		if (textures[i] != nullptr)
-			gltex = (GLuint) tex->getHandle();
+		if (isstoragetex)
+		{
+			GLuint gltex = 0;
+			if (tex != nullptr)
+				gltex = (GLuint) tex->getHandle();
+			else
+				gltex = gl.getDefaultTexture(info->textureType, info->dataBaseType);
+
+			int bindingindex = info->ints[i];
+			auto &binding = storageTextureBindings[bindingindex];
+
+			binding.texture = tex;
+			binding.gltexture = gltex;
+
+			if (shaderactive)
+				glBindImageTexture(bindingindex, binding.gltexture, 0, GL_TRUE, 0, binding.access, binding.internalFormat);
+		}
 		else
-			gltex = gl.getDefaultTexture(info->textureType, info->dataBaseType);
+		{
+			GLuint gltex = 0;
+			if (textures[i] != nullptr)
+				gltex = (GLuint) tex->getHandle();
+			else
+				gltex = gl.getDefaultTexture(info->textureType, info->dataBaseType);
 
-		int texunit = info->ints[i];
+			int texunit = info->ints[i];
 
-		if (shaderactive)
-			gl.bindTextureToUnit(info->textureType, gltex, texunit, false, false);
+			if (shaderactive)
+				gl.bindTextureToUnit(info->textureType, gltex, texunit, false, false);
 
-		// Store texture id so it can be re-bound to the texture unit later.
-		textureUnits[texunit].texture = gltex;
+			// Store texture id so it can be re-bound to the texture unit later.
+			textureUnits[texunit].texture = gltex;
+		}
 	}
 }
 
@@ -1243,6 +1326,67 @@ void Shader::computeUniformTypeInfo(GLenum type, UniformInfo &u)
 		u.dataBaseType = DATA_BASETYPE_UINT;
 		break;
 
+	case GL_IMAGE_2D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_FLOAT;
+		u.textureType = TEXTURE_2D;
+		break;
+	case GL_INT_IMAGE_2D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_INT;
+		u.textureType = TEXTURE_2D;
+		break;
+	case GL_UNSIGNED_INT_IMAGE_2D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_UINT;
+		u.textureType = TEXTURE_2D;
+		break;
+	case GL_IMAGE_2D_ARRAY:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_FLOAT;
+		u.textureType = TEXTURE_2D_ARRAY;
+		break;
+	case GL_INT_IMAGE_2D_ARRAY:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_INT;
+		u.textureType = TEXTURE_2D_ARRAY;
+		break;
+	case GL_UNSIGNED_INT_IMAGE_2D_ARRAY:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_UINT;
+		u.textureType = TEXTURE_2D_ARRAY;
+		break;
+	case GL_IMAGE_3D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_FLOAT;
+		u.textureType = TEXTURE_VOLUME;
+		break;
+	case GL_INT_IMAGE_3D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_INT;
+		u.textureType = TEXTURE_VOLUME;
+		break;
+	case GL_UNSIGNED_INT_IMAGE_3D:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_UINT;
+		u.textureType = TEXTURE_VOLUME;
+		break;
+	case GL_IMAGE_CUBE:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_FLOAT;
+		u.textureType = TEXTURE_CUBE;
+		break;
+	case GL_INT_IMAGE_CUBE:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_INT;
+		u.textureType = TEXTURE_CUBE;
+		break;
+	case GL_UNSIGNED_INT_IMAGE_CUBE:
+		u.baseType = UNIFORM_STORAGETEXTURE;
+		u.dataBaseType = DATA_BASETYPE_UINT;
+		u.textureType = TEXTURE_CUBE;
+		break;
+
 	default:
 		break;
 	}

+ 12 - 2
src/modules/graphics/opengl/Shader.h

@@ -43,6 +43,15 @@ class Shader final : public love::graphics::Shader, public Volatile
 {
 public:
 
+	struct StorageTextureBinding
+	{
+		Texture *texture = nullptr;
+		GLuint gltexture = 0;
+		TextureType type = TEXTURE_2D;
+		GLenum access = GL_READ_ONLY;
+		GLenum internalFormat;
+	};
+
 	Shader(StrongRef<love::graphics::ShaderStage> stages[SHADERSTAGE_MAX_ENUM]);
 	virtual ~Shader();
 
@@ -66,7 +75,7 @@ public:
 	void updateBuiltinUniforms(love::graphics::Graphics *gfx, int viewportW, int viewportH);
 
 	const std::vector<Buffer *> &getActiveWritableStorageBuffers() const { return activeWritableStorageBuffers; }
-	const std::vector<Texture *> &getActiveWritableTextures() const { return activeWritableTextures; }
+	const std::vector<StorageTextureBinding> &getStorageTextureBindings() const { return storageTextureBindings; }
 
 private:
 
@@ -118,11 +127,12 @@ private:
 	// Texture unit pool for setting textures
 	std::vector<TextureUnit> textureUnits;
 
+	std::vector<StorageTextureBinding> storageTextureBindings;
+
 	std::vector<std::pair<int, int>> storageBufferBindingIndexToActiveBinding;
 	std::vector<BufferBinding> activeStorageBufferBindings;
 
 	std::vector<Buffer *> activeWritableStorageBuffers;
-	std::vector<Texture *> activeWritableTextures;
 
 	std::vector<std::pair<const UniformInfo *, int>> pendingUniformUpdates;
 

+ 20 - 6
src/modules/graphics/wrap_Graphics.cpp

@@ -801,6 +801,8 @@ static void luax_checktexturesettings(lua_State *L, int idx, bool opt, bool chec
 	s.linear = luax_boolflag(L, idx, Texture::getConstant(Texture::SETTING_LINEAR), s.linear);
 	s.msaa = luax_intflag(L, idx, Texture::getConstant(Texture::SETTING_MSAA), s.msaa);
 
+	s.computeWrite = luax_boolflag(L, idx, Texture::getConstant(Texture::SETTING_COMPUTE_WRITE), s.computeWrite);
+
 	lua_getfield(L, idx, Texture::getConstant(Texture::SETTING_READABLE));
 	if (!lua_isnoneornil(L, -1))
 		s.readable.set(luax_checkboolean(L, -1));
@@ -2640,6 +2642,7 @@ int w_getTextureFormats(lua_State *L)
 
 	bool rt = luax_checkboolflag(L, 1, Texture::getConstant(Texture::SETTING_RENDER_TARGET));
 	bool linear = luax_boolflag(L, 1, Texture::getConstant(Texture::SETTING_LINEAR), false);
+	bool computewrite = luax_boolflag(L, 1, Texture::getConstant(Texture::SETTING_COMPUTE_WRITE), false);
 
 	OptionalBool readable;
 	lua_getfield(L, 1, Texture::getConstant(Texture::SETTING_READABLE));
@@ -2663,10 +2666,17 @@ int w_getTextureFormats(lua_State *L)
 		if (rt && isPixelFormatDepth(format))
 			continue;
 
-		bool formatReadable = readable.get(!isPixelFormatDepthStencil(format));
 		bool sRGB = isGammaCorrect() && !linear;
 
-		luax_pushboolean(L, instance()->isPixelFormatSupported(format, rt, formatReadable, sRGB));
+		uint32 usage = PIXELFORMATUSAGEFLAGS_NONE;
+		if (rt)
+			usage |= PIXELFORMATUSAGEFLAGS_RENDERTARGET;
+		if (readable.get(!isPixelFormatDepthStencil(format)))
+			usage |= PIXELFORMATUSAGEFLAGS_SAMPLE;
+		if (computewrite)
+			usage |= PIXELFORMATUSAGEFLAGS_COMPUTEWRITE;
+
+		luax_pushboolean(L, instance()->isPixelFormatSupported(format, (PixelFormatUsageFlags) usage, sRGB));
 		lua_setfield(L, -2, name);
 	}
 
@@ -2709,14 +2719,15 @@ int w_getCanvasFormats(lua_State *L)
 		{
 			supported = [](PixelFormat format) -> bool
 			{
-				return instance()->isPixelFormatSupported(format, true, true, false);
+				const uint32 usage = PIXELFORMATUSAGEFLAGS_SAMPLE | PIXELFORMATUSAGEFLAGS_RENDERTARGET;
+				return instance()->isPixelFormatSupported(format, (PixelFormatUsageFlags) usage, false);
 			};
 		}
 		else
 		{
 			supported = [](PixelFormat format) -> bool
 			{
-				return instance()->isPixelFormatSupported(format, true, false, false);
+				return instance()->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_RENDERTARGET, false);
 			};
 		}
 	}
@@ -2725,7 +2736,10 @@ int w_getCanvasFormats(lua_State *L)
 		supported = [](PixelFormat format) -> bool
 		{
 			bool readable = !isPixelFormatDepthStencil(format);
-			return instance()->isPixelFormatSupported(format, true, readable, false);
+			uint32 usage = PIXELFORMATUSAGEFLAGS_RENDERTARGET;
+			if (readable)
+				usage |= PIXELFORMATUSAGEFLAGS_SAMPLE;
+			return instance()->isPixelFormatSupported(format, (PixelFormatUsageFlags) usage, false);
 		};
 	}
 
@@ -2738,7 +2752,7 @@ int w_getImageFormats(lua_State *L)
 
 	const auto supported = [](PixelFormat format) -> bool
 	{
-		return instance()->isPixelFormatSupported(format, false, true, false);
+		return instance()->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_SAMPLE, false);
 	};
 
 	const auto ignore = [](PixelFormat format) -> bool

+ 3 - 1
src/modules/graphics/wrap_Shader.cpp

@@ -317,6 +317,7 @@ static int w_Shader_sendLuaValues(lua_State *L, int startidx, Shader *shader, co
 	case Shader::UNIFORM_BOOL:
 		return w_Shader_sendBooleans(L, startidx, shader, info);
 	case Shader::UNIFORM_SAMPLER:
+	case Shader::UNIFORM_STORAGETEXTURE:
 		return w_Shader_sendTextures(L, startidx, shader, info);
 	case Shader::UNIFORM_TEXELBUFFER:
 	case Shader::UNIFORM_STORAGEBUFFER:
@@ -328,7 +329,8 @@ static int w_Shader_sendLuaValues(lua_State *L, int startidx, Shader *shader, co
 
 static int w_Shader_sendData(lua_State *L, int startidx, Shader *shader, const Shader::UniformInfo *info, bool colors)
 {
-	if (info->baseType == Shader::UNIFORM_SAMPLER || info->baseType == Shader::UNIFORM_TEXELBUFFER || info->baseType == Shader::UNIFORM_STORAGEBUFFER)
+	if (info->baseType == Shader::UNIFORM_SAMPLER || info->baseType == Shader::UNIFORM_STORAGETEXTURE
+		|| info->baseType == Shader::UNIFORM_TEXELBUFFER || info->baseType == Shader::UNIFORM_STORAGEBUFFER)
 		return luaL_error(L, "Only value types (floats, ints, vectors, matrices, etc) be sent to Shaders via Data objects.");
 
 	math::Transform::MatrixLayout layout = math::Transform::MATRIX_ROW_MAJOR;

+ 16 - 0
src/modules/graphics/wrap_Texture.cpp

@@ -279,6 +279,20 @@ int w_Texture_getFormat(lua_State *L)
 	return 1;
 }
 
+int w_Texture_isRenderTarget(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	luax_pushboolean(L, t->isRenderTarget());
+	return 1;
+}
+
+int w_Texture_isComputeWritable(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	luax_pushboolean(L, t->isComputeWritable());
+	return 1;
+}
+
 int w_Texture_isReadable(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
@@ -479,6 +493,8 @@ const luaL_Reg w_Texture_functions[] =
 	{ "setWrap", w_Texture_setWrap },
 	{ "getWrap", w_Texture_getWrap },
 	{ "getFormat", w_Texture_getFormat },
+	{ "isRenderTarget", w_Texture_isRenderTarget },
+	{ "isComputeWritable", w_Texture_isComputeWritable },
 	{ "isReadable", w_Texture_isReadable },
 	{ "getMipmapMode", w_Texture_getMipmapMode },
 	{ "getDepthSampleMode", w_Texture_getDepthSampleMode },