Browse Source

Add texture views. Fixes #2022.

Add love.graphics.newTextureView(basetexture, viewsettings). Requires GLSL4 support.
Add 'viewformats' boolean setting to texture setting tables.
Add Texture:hasViewFormats.

The viewsettings table has the following fields:
  format = nil, -- set this to a pixel format to override the base format. It must have the same number of bytes per pixel as the original, and the 'viewformats' setting on the original texture must be true for this to work. Cannot be used for compressed or depth/stencil textures.
  type = nil, -- set this to a texture type to override the base texture type. 2d base textures can make [2d, array] views. [array, cube] base textures can make [2d, array, cube] views.
  mipmapstart = nil, -- set to a number to use a less detailed mipmap level as a base.
  mipmapcount = nil, -- set to a number to override the number of mipmaps in the texture view.
  layerstart = nil, -- set to a number to use a specific layer or cube face as a base.
  layers = nil, -- set to a number to override the number of layers in an array texture view.
Sasha Szpakowski 1 year ago
parent
commit
42812ad521

+ 18 - 6
src/modules/graphics/Graphics.cpp

@@ -1203,15 +1203,16 @@ bool Graphics::isRenderTargetActive() const
 
 bool Graphics::isRenderTargetActive(Texture *texture) const
 {
+	Texture *roottexture = texture->getRootViewInfo().texture;
 	const auto &rts = states.back().renderTargets;
 
 	for (const auto &rt : rts.colors)
 	{
-		if (rt.texture.get() == texture)
+		if (rt.texture.get() && rt.texture->getRootViewInfo().texture == roottexture)
 			return true;
 	}
 
-	if (rts.depthStencil.texture.get() == texture)
+	if (rts.depthStencil.texture.get() && rts.depthStencil.texture->getRootViewInfo().texture == roottexture)
 		return true;
 
 	return false;
@@ -1219,16 +1220,27 @@ bool Graphics::isRenderTargetActive(Texture *texture) const
 
 bool Graphics::isRenderTargetActive(Texture *texture, int slice) const
 {
+	const auto &rootinfo = texture->getRootViewInfo();
+	slice += rootinfo.startLayer;
+
 	const auto &rts = states.back().renderTargets;
 
 	for (const auto &rt : rts.colors)
 	{
-		if (rt.texture.get() == texture && rt.slice == slice)
-			return true;
+		if (rt.texture.get())
+		{
+			const auto &info = rt.texture->getRootViewInfo();
+			if (rootinfo.texture == info.texture && rt.slice + info.startLayer == slice)
+				return true;
+		}
 	}
 
-	if (rts.depthStencil.texture.get() == texture && rts.depthStencil.slice == slice)
-		return true;
+	if (rts.depthStencil.texture.get())
+	{
+		const auto &info = rts.depthStencil.texture->getRootViewInfo();
+		if (rootinfo.texture == info.texture && rts.depthStencil.slice + info.startLayer == slice)
+			return true;
+	}
 
 	return false;
 }

+ 1 - 0
src/modules/graphics/Graphics.h

@@ -455,6 +455,7 @@ public:
 	virtual ~Graphics();
 
 	virtual Texture *newTexture(const Texture::Settings &settings, const Texture::Slices *data = nullptr) = 0;
+	virtual Texture *newTextureView(Texture *base, const Texture::ViewSettings &viewsettings) = 0;
 
 	Quad *newQuad(Quad::Viewport v, double sw, double sh);
 	Font *newFont(love::font::Rasterizer *data);

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

@@ -1141,7 +1141,7 @@ bool Shader::validateTexture(const UniformInfo *info, Texture *tex, bool interna
 		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()))
+	else if (isstoragetex && info->storageTextureFormat != tex->getPixelFormat())
 	{
 		if (internalUpdate)
 			return false;

+ 203 - 74
src/modules/graphics/Texture.cpp

@@ -166,6 +166,7 @@ Texture::Texture(Graphics *gfx, const Settings &settings, const Slices *slices)
 	, format(settings.format)
 	, renderTarget(settings.renderTarget)
 	, computeWrite(settings.computeWrite)
+	, viewFormats(settings.viewFormats)
 	, readable(true)
 	, mipmapsMode(settings.mipmaps)
 	, width(settings.width)
@@ -179,6 +180,8 @@ Texture::Texture(Graphics *gfx, const Settings &settings, const Slices *slices)
 	, samplerState()
 	, graphicsMemorySize(0)
 	, debugName(settings.debugName)
+	, rootView({this, 0, 0})
+	, parentView({this, 0, 0})
 {
 	const auto &caps = gfx->getCapabilities();
 	int requestedMipmapCount = settings.mipmapCount;
@@ -284,31 +287,7 @@ Texture::Texture(Graphics *gfx, const Settings &settings, const Slices *slices)
 	if (isCompressed() && renderTarget)
 		throw love::Exception("Compressed textures cannot be render targets.");
 
-	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))
-	{
-		const char *fstr = "unknown";
-		love::getConstant(format, fstr);
-
-		const char *readablestr = "";
-		if (readable != !isPixelFormatDepthStencil(format))
-			readablestr = readable ? " readable" : " non-readable";
-
-		const char *rtstr = "";
-		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);
-	}
+	validatePixelFormat(gfx);
 
 	if (!caps.textureTypes[texType])
 	{
@@ -330,10 +309,149 @@ Texture::Texture(Graphics *gfx, const Settings &settings, const Slices *slices)
 	++textureCount;
 }
 
+Texture::Texture(Graphics *gfx, Texture *base, const ViewSettings &viewsettings)
+	: texType(viewsettings.type.get(base->getTextureType()))
+	, format(viewsettings.format.get(base->getPixelFormat()))
+	, renderTarget(base->renderTarget)
+	, computeWrite(base->computeWrite)
+	, viewFormats(base->viewFormats)
+	, readable(base->readable)
+	, mipmapsMode(base->mipmapsMode)
+	, width(1)
+	, height(1)
+	, depth(1)
+	, layers(1)
+	, mipmapCount(1)
+	, pixelWidth(1)
+	, pixelHeight(1)
+	, requestedMSAA(base->requestedMSAA)
+	, samplerState(base->samplerState)
+	, quad(base->quad)
+	, graphicsMemorySize(0)
+	, debugName(viewsettings.debugName)
+	, rootView({base->rootView.texture, 0, 0})
+	, parentView({base, viewsettings.mipmapStart.get(0), viewsettings.layerStart.get(0)})
+{
+	width = base->getHeight(parentView.startMipmap);
+	height = base->getHeight(parentView.startMipmap);
+
+	if (texType == TEXTURE_VOLUME)
+		depth = base->getDepth(parentView.startMipmap);
+
+	if (texType == TEXTURE_2D_ARRAY)
+	{
+		int baselayers = base->getTextureType() == TEXTURE_CUBE ? 6 : base->getLayerCount();
+		layers = viewsettings.layerCount.get(baselayers - parentView.startLayer);
+	}
+
+	mipmapCount = viewsettings.mipmapCount.get(base->getMipmapCount() - parentView.startMipmap);
+
+	pixelWidth = base->getPixelWidth(parentView.startMipmap);
+	pixelHeight = base->getPixelHeight(parentView.startMipmap);
+
+	if (parentView.startMipmap < 0)
+		throw love::Exception("Invalid mipmap start value for texture view (out of range).");
+
+	if (mipmapCount < 0 || parentView.startMipmap + mipmapCount > base->getMipmapCount())
+		throw love::Exception("Invalid mipmap start or count value for texture view (out of range).");
+
+	if (parentView.startLayer < 0)
+		throw love::Exception("Invalid layer start value for texture view (out of range).");
+
+	int baseLayerCount = base->getTextureType() == TEXTURE_CUBE ? 6 : base->getLayerCount();
+	if (layers < 0 || parentView.startLayer + layers > baseLayerCount)
+		throw love::Exception("Invalid layer start or count value for texture view (out of range).");
+
+	if (texType == TEXTURE_CUBE && parentView.startLayer + 6 > baseLayerCount)
+		throw love::Exception("Cube texture view cannot fit in the base texture's layers with the given start layer.");
+
+	ViewInfo nextView = { this, 0, 0 };
+	while (nextView.texture != rootView.texture)
+	{
+		nextView = nextView.texture->parentView;
+		rootView.startMipmap += nextView.startMipmap;
+		rootView.startLayer += nextView.startLayer;
+	}
+
+	const auto &caps = gfx->getCapabilities();
+	if (!caps.features[Graphics::FEATURE_GLSL4])
+		throw love::Exception("Texture views are not supported on this system (GLSL 4 support is necessary.)");
+
+	validatePixelFormat(gfx);
+
+	if (!caps.textureTypes[texType])
+	{
+		const char *textypestr = "unknown";
+		Texture::getConstant(texType, textypestr);
+		throw love::Exception("%s textures are not supported on this system.", textypestr);
+	}
+
+	if (!readable)
+		throw love::Exception("Texture views are not supported for non-readable textures.");
+
+	if (base->getTextureType() == TEXTURE_2D)
+	{
+		if (texType != TEXTURE_2D && texType != TEXTURE_2D_ARRAY)
+			throw love::Exception("Texture views created from a 2D texture must use the 2d or array texture type.");
+	}
+	else if (base->getTextureType() == TEXTURE_2D_ARRAY || base->getTextureType() == TEXTURE_CUBE)
+	{
+		if (texType != TEXTURE_2D && texType != TEXTURE_2D_ARRAY && texType != TEXTURE_CUBE)
+			throw love::Exception("Texture views created from an array or cube texture must use the 2d, array, or cube texture type.");
+	}
+	else if (base->getTextureType() == TEXTURE_VOLUME)
+	{
+		if (texType != TEXTURE_VOLUME)
+			throw love::Exception("Texture views created from a volume texture must use the volume texture type.");
+	}
+	else
+	{
+		throw love::Exception("Unknown texture type.");
+	}
+
+	auto baseformat = base->getPixelFormat();
+
+	if (format != baseformat)
+	{
+		if (isPixelFormatCompressed(baseformat) || isPixelFormatCompressed(format))
+			throw love::Exception("Compressed textures cannot use a different pixel format in a texture view.");
+
+		if (isPixelFormatColor(baseformat) != isPixelFormatColor(format))
+			throw love::Exception("Color-format textures cannot use a depth/stencil pixel format and vice versa, in a texture view.");
+
+		// TODO: depth[24|32f]_stencil8 -> stencil8 can work.
+		if (isPixelFormatDepthStencil(baseformat))
+			throw love::Exception("Using a different pixel format in a texture view is not currently supported for depth or stencil formats.");
+
+		if (!viewFormats)
+			throw love::Exception("Using a different pixel format in a texture view requires the original texture to be created with the 'viewformats' setting set to true.");
+
+		size_t bytes = getPixelFormatBlockSize(format);
+		size_t basebytes = getPixelFormatBlockSize(baseformat);
+
+		if (bytes != basebytes)
+			throw love::Exception("Texture views must have the same bits per pixel as the original texture.");
+	}
+
+	const char *miperr = nullptr;
+	if (mipmapsMode == MIPMAPS_AUTO && !supportsGenerateMipmaps(miperr))
+		mipmapsMode = MIPMAPS_MANUAL;
+
+	rootView.texture->retain();
+	parentView.texture->retain();
+}
+
 Texture::~Texture()
 {
-	--textureCount;
 	setGraphicsMemorySize(0);
+
+	if (this == rootView.texture)
+		--textureCount;
+
+	if (rootView.texture != this && rootView.texture != nullptr)
+		rootView.texture->release();
+	if (parentView.texture != this && parentView.texture != nullptr)
+		parentView.texture->release();
 }
 
 void Texture::setGraphicsMemorySize(int64 bytes)
@@ -352,18 +470,18 @@ void Texture::draw(Graphics *gfx, const Matrix4 &m)
 
 void Texture::draw(Graphics *gfx, Quad *q, const Matrix4 &localTransform)
 {
-	if (!readable)
-		throw love::Exception("Textures with non-readable formats cannot be drawn.");
-
-	if (renderTarget && gfx->isRenderTargetActive(this))
-		throw love::Exception("Cannot render a Texture to itself.");
-
 	if (texType == TEXTURE_2D_ARRAY)
 	{
 		drawLayer(gfx, q->getLayer(), q, localTransform);
 		return;
 	}
 
+	if (!readable)
+		throw love::Exception("Textures with non-readable formats cannot be drawn.");
+
+	if (renderTarget && gfx->isRenderTargetActive(this))
+		throw love::Exception("Cannot render a Texture to itself.");
+
 	const Matrix4 &tm = gfx->getTransform();
 	bool is2D = tm.isAffine2DTransform();
 
@@ -582,36 +700,6 @@ void Texture::generateMipmaps()
 	generateMipmapsInternal();
 }
 
-TextureType Texture::getTextureType() const
-{
-	return texType;
-}
-
-PixelFormat Texture::getPixelFormat() const
-{
-	return format;
-}
-
-Texture::MipmapsMode Texture::getMipmapsMode() const
-{
-	return mipmapsMode;
-}
-
-bool Texture::isRenderTarget() const
-{
-	return renderTarget;
-}
-
-bool Texture::isComputeWritable() const
-{
-	return computeWrite;
-}
-
-bool Texture::isReadable() const
-{
-	return readable;
-}
-
 bool Texture::isCompressed() const
 {
 	return isPixelFormatCompressed(format);
@@ -685,28 +773,39 @@ int Texture::getRequestedMSAA() const
 	return requestedMSAA;
 }
 
-void Texture::setSamplerState(const SamplerState &s)
+const SamplerState &Texture::getSamplerState() const
+{
+	return samplerState;
+}
+
+SamplerState Texture::validateSamplerState(SamplerState s) const
 {
 	if (!readable)
-		return;
+		return s;
 
 	if (s.depthSampleMode.hasValue && !isPixelFormatDepth(format))
 		throw love::Exception("Only depth textures can have a depth sample compare mode.");
 
-	Graphics::flushBatchedDrawsGlobal();
+	if (s.mipmapFilter != SamplerState::MIPMAP_FILTER_NONE && getMipmapCount() == 1)
+		s.mipmapFilter = SamplerState::MIPMAP_FILTER_NONE;
 
-	samplerState = s;
+	if (texType == TEXTURE_CUBE)
+		s.wrapU = s.wrapV = s.wrapW = SamplerState::WRAP_CLAMP;
 
-	if (samplerState.mipmapFilter != SamplerState::MIPMAP_FILTER_NONE && getMipmapCount() == 1)
-		samplerState.mipmapFilter = SamplerState::MIPMAP_FILTER_NONE;
+	if (s.minFilter == SamplerState::FILTER_LINEAR || s.magFilter == SamplerState::FILTER_LINEAR || s.mipmapFilter == SamplerState::MIPMAP_FILTER_LINEAR)
+	{
+		auto gfx = Module::getInstance<Graphics>(Module::M_GRAPHICS);
+		if (!gfx->isPixelFormatSupported(format, PIXELFORMATUSAGEFLAGS_LINEAR))
+		{
+			s.minFilter = s.magFilter = SamplerState::FILTER_NEAREST;
+			if (s.mipmapFilter == SamplerState::MIPMAP_FILTER_LINEAR)
+				s.mipmapFilter = SamplerState::MIPMAP_FILTER_NEAREST;
+		}
+	}
 
-	if (texType == TEXTURE_CUBE)
-		samplerState.wrapU = samplerState.wrapV = samplerState.wrapW = SamplerState::WRAP_CLAMP;
-}
+	Graphics::flushBatchedDrawsGlobal();
 
-const SamplerState &Texture::getSamplerState() const
-{
-	return samplerState;
+	return s;
 }
 
 Quad *Texture::getQuad() const
@@ -781,6 +880,35 @@ bool Texture::validateDimensions(bool throwException) const
 	return success;
 }
 
+void Texture::validatePixelFormat(Graphics *gfx) const
+{
+	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))
+	{
+		const char *fstr = "unknown";
+		love::getConstant(format, fstr);
+
+		const char *readablestr = "";
+		if (readable != !isPixelFormatDepthStencil(format))
+			readablestr = readable ? " readable" : " non-readable";
+
+		const char *rtstr = "";
+		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);
+	}
+}
+
 Texture::Slices::Slices(TextureType textype)
 	: textureType(textype)
 {
@@ -971,6 +1099,7 @@ static StringMap<Texture::SettingType, Texture::SETTING_MAX_ENUM>::Entry setting
 	{ "msaa",         Texture::SETTING_MSAA          },
 	{ "canvas",       Texture::SETTING_RENDER_TARGET },
 	{ "computewrite", Texture::SETTING_COMPUTE_WRITE },
+	{ "viewformats",  Texture::SETTING_VIEW_FORMATS  },
 	{ "readable",     Texture::SETTING_READABLE      },
 	{ "debugname",    Texture::SETTING_DEBUGNAME     },
 };

+ 42 - 10
src/modules/graphics/Texture.h

@@ -174,6 +174,7 @@ public:
 		SETTING_MSAA,
 		SETTING_RENDER_TARGET,
 		SETTING_COMPUTE_WRITE,
+		SETTING_VIEW_FORMATS,
 		SETTING_READABLE,
 		SETTING_DEBUGNAME,
 		SETTING_MAX_ENUM
@@ -194,10 +195,22 @@ public:
 		int msaa = 1;
 		bool renderTarget = false;
 		bool computeWrite = false;
+		bool viewFormats = false;
 		OptionalBool readable;
 		std::string debugName;
 	};
 
+	struct ViewSettings
+	{
+		Optional<PixelFormat> format;
+		Optional<TextureType> type;
+		OptionalInt mipmapStart;
+		OptionalInt mipmapCount;
+		OptionalInt layerStart;
+		OptionalInt layerCount;
+		std::string debugName;
+	};
+
 	struct Slices
 	{
 	public:
@@ -228,10 +241,14 @@ public:
 
 	}; // Slices
 
-	static int64 totalGraphicsMemory;
+	struct ViewInfo
+	{
+		Texture *texture;
+		int startMipmap;
+		int startLayer;
+	};
 
-	Texture(Graphics *gfx, const Settings &settings, const Slices *slices);
-	virtual ~Texture();
+	static int64 totalGraphicsMemory;
 
 	// Drawable.
 	void draw(Graphics *gfx, const Matrix4 &m) override;
@@ -255,13 +272,14 @@ public:
 	virtual ptrdiff_t getRenderTargetHandle() const = 0;
 	virtual ptrdiff_t getSamplerHandle() const = 0;
 
-	TextureType getTextureType() const;
-	PixelFormat getPixelFormat() const;
-	MipmapsMode getMipmapsMode() const;
+	TextureType getTextureType() const { return texType; }
+	PixelFormat getPixelFormat() const { return format; }
+	MipmapsMode getMipmapsMode() const { return mipmapsMode; }
 
-	bool isRenderTarget() const;
-	bool isComputeWritable() const;
-	bool isReadable() const;
+	bool isRenderTarget() const { return renderTarget; }
+	bool isComputeWritable() const { return computeWrite; }
+	bool isReadable() const { return readable; }
+	bool hasViewFormats() const { return viewFormats; }
 
 	bool isCompressed() const;
 	bool isFormatLinear() const;
@@ -285,11 +303,14 @@ public:
 	int getRequestedMSAA() const;
 	virtual int getMSAA() const = 0;
 
-	virtual void setSamplerState(const SamplerState &s);
+	virtual void setSamplerState(const SamplerState &s) = 0;
 	const SamplerState &getSamplerState() const;
 
 	Quad *getQuad() const;
 
+	const ViewInfo &getRootViewInfo() const { return rootView; }
+	const ViewInfo &getParentViewInfo() const { return parentView; }
+
 	const std::string &getDebugName() const { return debugName; }
 
 	static int getTotalMipmapCount(int w, int h);
@@ -310,6 +331,10 @@ public:
 
 protected:
 
+	Texture(Graphics *gfx, const Settings &settings, const Slices *slices);
+	Texture(Graphics *gfx, Texture *base, const ViewSettings &viewsettings);
+	virtual ~Texture();
+
 	void setGraphicsMemorySize(int64 size);
 
 	void uploadImageData(love::image::ImageDataBase *d, int level, int slice, int x, int y);
@@ -318,13 +343,17 @@ protected:
 	bool supportsGenerateMipmaps(const char *&outReason) const;
 	virtual void generateMipmapsInternal() = 0;
 
+	SamplerState validateSamplerState(SamplerState s) const;
+
 	bool validateDimensions(bool throwException) const;
+	void validatePixelFormat(Graphics *gfx) const;
 
 	TextureType texType;
 
 	PixelFormat format;
 	bool renderTarget;
 	bool computeWrite;
+	bool viewFormats;
 	bool readable;
 
 	MipmapsMode mipmapsMode;
@@ -349,6 +378,9 @@ protected:
 
 	std::string debugName;
 
+	ViewInfo rootView;
+	ViewInfo parentView;
+
 }; // Texture
 
 } // graphics

+ 1 - 0
src/modules/graphics/metal/Graphics.h

@@ -61,6 +61,7 @@ public:
 	virtual ~Graphics();
 
 	love::graphics::Texture *newTexture(const Texture::Settings &settings, const Texture::Slices *data = nullptr) override;
+	love::graphics::Texture *newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings) override;
 	love::graphics::Buffer *newBuffer(const Buffer::Settings &settings, const std::vector<Buffer::DataDeclaration> &format, const void *data, size_t size, size_t arraylength) override;
 
 	Matrix4 computeDeviceProjection(const Matrix4 &projection, bool rendertotexture) const override;

+ 5 - 0
src/modules/graphics/metal/Graphics.mm

@@ -433,6 +433,11 @@ love::graphics::Texture *Graphics::newTexture(const Texture::Settings &settings,
 	return new Texture(this, device, settings, data);
 }
 
+love::graphics::Texture *Graphics::newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+{
+	return new Texture(this, device, base, viewsettings);
+}
+
 love::graphics::ShaderStage *Graphics::newShaderStageInternal(ShaderStageType stage, const std::string &cachekey, const std::string &source, bool gles)
 {
 	return new ShaderStage(this, stage, source, gles, cachekey);

+ 5 - 4
src/modules/graphics/metal/Texture.h

@@ -38,6 +38,7 @@ class Texture final : public love::graphics::Texture
 public:
 
 	Texture(love::graphics::Graphics *gfx, id<MTLDevice> device, const Settings &settings, const Slices *data);
+	Texture(love::graphics::Graphics *gfx, id<MTLDevice> device, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings);
 	virtual ~Texture();
 
 	void copyFromBuffer(love::graphics::Buffer *source, size_t sourceoffset, int sourcewidth, size_t size, int slice, int mipmap, const Rect &rect) override;
@@ -58,11 +59,11 @@ private:
 	void uploadByteData(const void *data, size_t size, int level, int slice, const Rect &r) override;
 	void generateMipmapsInternal() override;
 
-	id<MTLTexture> texture;
-	id<MTLTexture> msaaTexture;
-	id<MTLSamplerState> sampler;
+	id<MTLTexture> texture = nil;
+	id<MTLTexture> msaaTexture = nil;
+	id<MTLSamplerState> sampler = nil;
 
-	int actualMSAASamples;
+	int actualMSAASamples = 1;
 
 }; // Texture
 

+ 39 - 8
src/modules/graphics/metal/Texture.mm

@@ -43,10 +43,6 @@ static MTLTextureType getMTLTextureType(TextureType type, int msaa)
 
 Texture::Texture(love::graphics::Graphics *gfxbase, id<MTLDevice> device, const Settings &settings, const Slices *data)
 	: love::graphics::Texture(gfxbase, settings, data)
-	, texture(nil)
-	, msaaTexture(nil)
-	, sampler(nil)
-	, actualMSAASamples(1)
 { @autoreleasepool {
 	auto gfx = (Graphics *) gfxbase;
 
@@ -80,6 +76,8 @@ Texture::Texture(love::graphics::Graphics *gfxbase, id<MTLDevice> device, const
 		desc.usage |= MTLTextureUsageRenderTarget;
 	if (computeWrite)
 		desc.usage |= MTLTextureUsageShaderWrite;
+	if (viewFormats)
+		desc.usage |= MTLTextureUsagePixelFormatView;
 
 	texture = [device newTextureWithDescriptor:desc];
 
@@ -212,6 +210,41 @@ Texture::Texture(love::graphics::Graphics *gfxbase, id<MTLDevice> device, const
 	setSamplerState(samplerState);
 }}
 
+Texture::Texture(love::graphics::Graphics *gfx, id<MTLDevice> device, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+	: love::graphics::Texture(gfx, base, viewsettings)
+{
+	id<MTLTexture> basetex = ((Texture *) base)->texture;
+	auto formatdesc = Metal::convertPixelFormat(device, format);
+	int slices = texType == TEXTURE_CUBE ? 6 : getLayerCount();
+
+	if (formatdesc.swizzled)
+	{
+		if (@available(macOS 10.15, iOS 13, *))
+		{
+			texture = [basetex newTextureViewWithPixelFormat:formatdesc.format
+												 textureType:getMTLTextureType(texType, 1)
+													  levels:NSMakeRange(parentView.startMipmap, mipmapCount)
+													  slices:NSMakeRange(parentView.startLayer, slices)
+													 swizzle:formatdesc.swizzle];
+		}
+	}
+	else
+	{
+		texture = [basetex newTextureViewWithPixelFormat:formatdesc.format
+											 textureType:getMTLTextureType(texType, 1)
+												  levels:NSMakeRange(parentView.startMipmap, mipmapCount)
+												  slices:NSMakeRange(parentView.startLayer, slices)];
+	}
+
+	if (texture == nil)
+		throw love::Exception("Could not create Metal texture view.");
+
+	if (!debugName.empty())
+		texture.label = @(debugName.c_str());
+
+	setSamplerState(samplerState);
+}
+
 Texture::~Texture()
 { @autoreleasepool {
 	texture = nil;
@@ -340,10 +373,8 @@ void Texture::setSamplerState(const SamplerState &s)
 	if (s.depthSampleMode.hasValue && !Graphics::getInstance()->isDepthCompareSamplerSupported())
 		throw love::Exception("Depth comparison sampling in shaders is not supported on this system.");
 
-	// Base class does common validation and assigns samplerState.
-	love::graphics::Texture::setSamplerState(s);
-
-	sampler = Graphics::getInstance()->getCachedSampler(s);
+	samplerState = validateSamplerState(s);
+	sampler = Graphics::getInstance()->getCachedSampler(samplerState);
 }}
 
 } // metal

+ 5 - 0
src/modules/graphics/opengl/Graphics.cpp

@@ -158,6 +158,11 @@ love::graphics::Texture *Graphics::newTexture(const Texture::Settings &settings,
 	return new Texture(this, settings, data);
 }
 
+love::graphics::Texture *Graphics::newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+{
+	return new Texture(this, base, viewsettings);
+}
+
 love::graphics::ShaderStage *Graphics::newShaderStageInternal(ShaderStageType stage, const std::string &cachekey, const std::string &source, bool gles)
 {
 	return new ShaderStage(this, stage, source, gles, cachekey);

+ 1 - 0
src/modules/graphics/opengl/Graphics.h

@@ -57,6 +57,7 @@ public:
 	virtual ~Graphics();
 
 	love::graphics::Texture *newTexture(const Texture::Settings &settings, const Texture::Slices *data = nullptr) override;
+	love::graphics::Texture *newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings) override;
 	love::graphics::Buffer *newBuffer(const Buffer::Settings &settings, const std::vector<Buffer::DataDeclaration> &format, const void *data, size_t size, size_t arraylength) override;
 
 	Matrix4 computeDeviceProjection(const Matrix4 &projection, bool rendertotexture) const override;

+ 2 - 0
src/modules/graphics/opengl/OpenGL.cpp

@@ -2506,6 +2506,8 @@ const char *OpenGL::debugSeverityString(GLenum severity)
 		return "medium";
 	case GL_DEBUG_SEVERITY_LOW:
 		return "low";
+	case GL_DEBUG_SEVERITY_NOTIFICATION:
+		return "notification";
 	default:
 		return "unknown";
 	}

+ 43 - 14
src/modules/graphics/opengl/Texture.cpp

@@ -244,6 +244,25 @@ Texture::Texture(love::graphics::Graphics *gfx, const Settings &settings, const
 	slices.clear();
 }
 
+Texture::Texture(love::graphics::Graphics *gfx, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+	: love::graphics::Texture(gfx, base, viewsettings)
+	, slices(viewsettings.type.get(base->getTextureType()))
+	, fbo(0)
+	, texture(0)
+	, renderbuffer(0)
+	, framebufferStatus(GL_FRAMEBUFFER_COMPLETE)
+	, textureGLError(GL_NO_ERROR)
+	, actualSamples(1)
+{
+	if (!loadVolatile())
+	{
+		if (framebufferStatus != GL_FRAMEBUFFER_COMPLETE)
+			throw love::Exception("Cannot create texture view (OpenGL framebuffer error: %s)", OpenGL::framebufferStatusString(framebufferStatus));
+		if (textureGLError != GL_NO_ERROR)
+			throw love::Exception("Cannot create texture view (OpenGL error: %s)", OpenGL::errorString(textureGLError));
+	}
+}
+
 Texture::~Texture()
 {
 	unloadVolatile();
@@ -254,11 +273,26 @@ void Texture::createTexture()
 	// The base class handles some validation. For example, if ImageData is
 	// given then it must exist for all mip levels, a render target can't use
 	// a compressed format, etc.
-
 	glGenTextures(1, &texture);
+	GLenum gltype = OpenGL::getGLTextureType(texType);
+
+	if (parentView.texture != this)
+	{
+		OpenGL::TextureFormat fmt = gl.convertPixelFormat(format, false);
+		Texture *basetex = (Texture *) parentView.texture;
+		int layers = texType == TEXTURE_CUBE ? 6 : getLayerCount();
+
+		glTextureView(texture, gltype, basetex->texture, fmt.internalformat,
+		              parentView.startMipmap, getMipmapCount(),
+		              parentView.startLayer, layers);
+
+		gl.bindTextureToUnit(this, 0, false);
+		setSamplerState(samplerState);
+		return;
+	}
+
 	gl.bindTextureToUnit(this, 0, false);
 
-	GLenum gltype = OpenGL::getGLTextureType(texType);
 	if (renderTarget && GLAD_ANGLE_texture_usage)
 		glTexParameteri(gltype, GL_TEXTURE_USAGE_ANGLE, GL_FRAMEBUFFER_ATTACHMENT_ANGLE);
 
@@ -371,6 +405,12 @@ bool Texture::loadVolatile()
 	if (texture != 0 || renderbuffer != 0)
 		return true;
 
+	if (parentView.texture != this)
+	{
+		Texture *basetex = (Texture *) parentView.texture;
+		basetex->loadVolatile();
+	}
+
 	OpenGL::TempDebugGroup debuggroup("Texture load");
 
 	// NPOT textures don't support mipmapping without full NPOT support.
@@ -593,18 +633,7 @@ void Texture::setSamplerState(const SamplerState &s)
 	if (s.depthSampleMode.hasValue && !gl.isDepthCompareSampleSupported())
 		throw love::Exception("Depth comparison sampling in shaders is not supported on this system.");
 
-	// Base class does common validation and assigns samplerState.
-	love::graphics::Texture::setSamplerState(s);
-
-	auto supportedflags = OpenGL::getPixelFormatUsageFlags(getPixelFormat());
-
-	if ((supportedflags & PIXELFORMATUSAGEFLAGS_LINEAR) == 0)
-	{
-		samplerState.magFilter = samplerState.minFilter = SamplerState::FILTER_NEAREST;
-
-		if (samplerState.mipmapFilter == SamplerState::MIPMAP_FILTER_LINEAR)
-			samplerState.mipmapFilter = SamplerState::MIPMAP_FILTER_NEAREST;
-	}
+	samplerState = validateSamplerState(s);
 
 	// If we only have limited NPOT support then the wrap mode must be CLAMP.
 	if ((GLAD_ES_VERSION_2_0 && !(GLAD_ES_VERSION_3_0 || GLAD_OES_texture_npot))

+ 2 - 0
src/modules/graphics/opengl/Texture.h

@@ -39,6 +39,7 @@ class Texture final : public love::graphics::Texture, public Volatile
 public:
 
 	Texture(love::graphics::Graphics *gfx, const Settings &settings, const Slices *data);
+	Texture(love::graphics::Graphics *gfx, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings);
 
 	virtual ~Texture();
 
@@ -61,6 +62,7 @@ public:
 	void readbackInternal(int slice, int mipmap, const Rect &rect, int destwidth, size_t size, void *dest);
 
 private:
+
 	void createTexture();
 
 	void uploadByteData(const void *data, size_t size, int level, int slice, const Rect &r) override;

+ 5 - 0
src/modules/graphics/vulkan/Graphics.cpp

@@ -168,6 +168,11 @@ love::graphics::Texture *Graphics::newTexture(const love::graphics::Texture::Set
 	return new Texture(this, settings, data);
 }
 
+love::graphics::Texture *Graphics::newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+{
+	return new Texture(this, base, viewsettings);
+}
+
 love::graphics::Buffer *Graphics::newBuffer(const love::graphics::Buffer::Settings &settings, const std::vector<love::graphics::Buffer::DataDeclaration> &format, const void *data, size_t size, size_t arraylength)
 {
 	return new Buffer(this, settings, format, data, size, arraylength);

+ 1 - 0
src/modules/graphics/vulkan/Graphics.h

@@ -275,6 +275,7 @@ public:
 
 	// implementation for virtual functions
 	love::graphics::Texture *newTexture(const love::graphics::Texture::Settings &settings, const love::graphics::Texture::Slices *data) override;
+	love::graphics::Texture *newTextureView(love::graphics::Texture *base, const Texture::ViewSettings &viewsettings) override;
 	love::graphics::Buffer *newBuffer(const love::graphics::Buffer::Settings &settings, const std::vector<love::graphics::Buffer::DataDeclaration>& format, const void *data, size_t size, size_t arraylength) override;
 	graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Buffer *buffer, size_t offset, size_t size, data::ByteData *dest, size_t destoffset) override;
 	graphics::GraphicsReadback *newReadbackInternal(ReadbackMethod method, love::graphics::Texture *texture, int slice, int mipmap, const Rect &rect, image::ImageData *dest, int destx, int desty) override;

+ 125 - 89
src/modules/graphics/vulkan/Texture.cpp

@@ -47,11 +47,22 @@ Texture::Texture(love::graphics::Graphics *gfx, const Settings &settings, const
 	slices.clear();
 }
 
+Texture::Texture(love::graphics::Graphics *gfx, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings)
+	: love::graphics::Texture(gfx, base, viewsettings)
+	, vgfx(dynamic_cast<Graphics*>(gfx))
+	, slices(viewsettings.type.get(base->getTextureType()))
+	, imageAspect(0)
+{
+	loadVolatile();
+}
+
 bool Texture::loadVolatile()
 {
 	allocator = vgfx->getVmaAllocator();
 	device = vgfx->getDevice();
 
+	bool root = rootView.texture == this;
+
 	if (isPixelFormatDepth(format))
 		imageAspect |= VK_IMAGE_ASPECT_DEPTH_BIT;
 	if (isPixelFormatStencil(format))
@@ -79,82 +90,96 @@ bool Texture::loadVolatile()
 			usageFlags |= VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
 	}
 
-	VkImageCreateFlags createFlags = 0;
-
 	layerCount = 1;
 
 	if (texType == TEXTURE_2D_ARRAY)
 		layerCount = getLayerCount();
 	else if (texType == TEXTURE_CUBE)
-	{
 		layerCount = 6;
-		createFlags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
-	}
 
-	msaaSamples = vgfx->getMsaaCount(requestedMSAA);
-	
-	VkImageCreateInfo imageInfo{};
-	imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
-	imageInfo.flags = createFlags;
-	imageInfo.imageType = Vulkan::getImageType(getTextureType());
-	imageInfo.extent.width = static_cast<uint32_t>(pixelWidth);
-	imageInfo.extent.height = static_cast<uint32_t>(pixelHeight);
-	imageInfo.extent.depth = static_cast<uint32_t>(depth);
-	imageInfo.arrayLayers = static_cast<uint32_t>(layerCount);
-	imageInfo.mipLevels = static_cast<uint32_t>(mipmapCount);
-	imageInfo.format = vulkanFormat.internalFormat;
-	imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
-	imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
-	imageInfo.usage = usageFlags;
-	imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
-	imageInfo.samples = msaaSamples;
-
-	VmaAllocationCreateInfo imageAllocationCreateInfo{};
-
-	if (vmaCreateImage(allocator, &imageInfo, &imageAllocationCreateInfo, &textureImage, &textureImageAllocation, nullptr) != VK_SUCCESS)
-		throw love::Exception("failed to create image");
+	if (root)
+	{
+		VkImageCreateFlags createFlags = 0;
 
-	auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
+		if (viewFormats)
+			createFlags |= VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT;
 
-	if (isPixelFormatDepthStencil(format))
-		imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
-	else if (computeWrite)
-		imageLayout = VK_IMAGE_LAYOUT_GENERAL;
-	else
-		imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+		if (texType == TEXTURE_CUBE || texType == TEXTURE_2D_ARRAY)
+			createFlags |= VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;
 
-	Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
-		VK_IMAGE_LAYOUT_UNDEFINED, imageLayout,
-		0, VK_REMAINING_MIP_LEVELS,
-		0, VK_REMAINING_ARRAY_LAYERS);
+		msaaSamples = vgfx->getMsaaCount(requestedMSAA);
 
-	bool hasdata = slices.get(0, 0) != nullptr;
+		VkImageCreateInfo imageInfo{};
+		imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
+		imageInfo.flags = createFlags;
+		imageInfo.imageType = Vulkan::getImageType(getTextureType());
+		imageInfo.extent.width = static_cast<uint32_t>(pixelWidth);
+		imageInfo.extent.height = static_cast<uint32_t>(pixelHeight);
+		imageInfo.extent.depth = static_cast<uint32_t>(depth);
+		imageInfo.arrayLayers = static_cast<uint32_t>(layerCount);
+		imageInfo.mipLevels = static_cast<uint32_t>(mipmapCount);
+		imageInfo.format = vulkanFormat.internalFormat;
+		imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
+		imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
+		imageInfo.usage = usageFlags;
+		imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
+		imageInfo.samples = msaaSamples;
 
-	if (hasdata)
-	{
-		for (int mip = 0; mip < getMipmapCount(); mip++)
-		{
-			int sliceCount;
-			if (texType == TEXTURE_CUBE)
-				sliceCount = 6;
-			else
-				sliceCount = slices.getSliceCount();
+		VmaAllocationCreateInfo imageAllocationCreateInfo{};
+
+		if (vmaCreateImage(allocator, &imageInfo, &imageAllocationCreateInfo, &textureImage, &textureImageAllocation, nullptr) != VK_SUCCESS)
+			throw love::Exception("failed to create image");
 
-			for (int slice = 0; slice < sliceCount; slice++)
+		auto commandBuffer = vgfx->getCommandBufferForDataTransfer();
+
+		if (isPixelFormatDepthStencil(format))
+			imageLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;
+		else if (computeWrite)
+			imageLayout = VK_IMAGE_LAYOUT_GENERAL;
+		else
+			imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
+
+		Vulkan::cmdTransitionImageLayout(commandBuffer, textureImage,
+			VK_IMAGE_LAYOUT_UNDEFINED, imageLayout,
+			0, VK_REMAINING_MIP_LEVELS,
+			0, VK_REMAINING_ARRAY_LAYERS);
+
+		bool hasdata = slices.get(0, 0) != nullptr;
+
+		if (hasdata)
+		{
+			for (int mip = 0; mip < getMipmapCount(); mip++)
 			{
-				auto id = slices.get(slice, mip);
-				if (id != nullptr)
-					uploadImageData(id, mip, slice, 0, 0);
+				int sliceCount;
+				if (texType == TEXTURE_CUBE)
+					sliceCount = 6;
+				else
+					sliceCount = slices.getSliceCount();
+
+				for (int slice = 0; slice < sliceCount; slice++)
+				{
+					auto id = slices.get(slice, mip);
+					if (id != nullptr)
+						uploadImageData(id, mip, slice, 0, 0);
+				}
 			}
 		}
+		else
+			clear();
 	}
 	else
-		clear();
+	{
+		Texture *roottex = (Texture *) rootView.texture;
+		textureImage = roottex->textureImage;
+		textureImageAllocation = VK_NULL_HANDLE;
+		imageLayout = roottex->imageLayout;
+		msaaSamples = roottex->msaaSamples;
+	}
 
 	createTextureImageView();
-	textureSampler = vgfx->getCachedSampler(samplerState);
+	setSamplerState(samplerState);
 
-	if (!isPixelFormatDepthStencil(format) && slices.getMipmapCount() <= 1 && getMipmapsMode() != MIPMAPS_NONE)
+	if (root && !isPixelFormatDepthStencil(format) && slices.getMipmapCount() <= 1 && getMipmapsMode() != MIPMAPS_NONE)
 		generateMipmaps();
 
 	if (renderTarget)
@@ -172,9 +197,9 @@ bool Texture::loadVolatile()
 				viewInfo.viewType = Vulkan::getImageViewType(getTextureType());
 				viewInfo.format = vulkanFormat.internalFormat;
 				viewInfo.subresourceRange.aspectMask = imageAspect;
-				viewInfo.subresourceRange.baseMipLevel = mip;
+				viewInfo.subresourceRange.baseMipLevel = mip + rootView.startMipmap;
 				viewInfo.subresourceRange.levelCount = 1;
-				viewInfo.subresourceRange.baseArrayLayer = slice;
+				viewInfo.subresourceRange.baseArrayLayer = slice + rootView.startLayer;
 				viewInfo.subresourceRange.layerCount = 1;
 				viewInfo.components.r = vulkanFormat.swizzleR;
 				viewInfo.components.g = vulkanFormat.swizzleG;
@@ -189,15 +214,18 @@ bool Texture::loadVolatile()
 
 	int64 memsize = 0;
 
-	for (int mip = 0; mip < getMipmapCount(); mip++)
+	if (root)
 	{
-		int w = getPixelWidth(mip);
-		int h = getPixelHeight(mip);
-		int slices = getDepth(mip) * layerCount;
-		memsize += getPixelFormatSliceSize(format, w, h) * slices;
-	}
+		for (int mip = 0; mip < getMipmapCount(); mip++)
+		{
+			int w = getPixelWidth(mip);
+			int h = getPixelHeight(mip);
+			int slices = getDepth(mip) * layerCount;
+			memsize += getPixelFormatSliceSize(format, w, h) * slices;
+		}
 
-	memsize *= static_cast<int>(msaaSamples);
+		memsize *= static_cast<int>(msaaSamples);
+	}
 
 	setGraphicsMemorySize(memsize);
 
@@ -207,8 +235,16 @@ bool Texture::loadVolatile()
 		{
 			VkDebugUtilsObjectNameInfoEXT nameInfo{};
 			nameInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_OBJECT_NAME_INFO_EXT;
-			nameInfo.objectType = VK_OBJECT_TYPE_IMAGE;
-			nameInfo.objectHandle = (uint64_t)textureImage;
+			if (root)
+			{
+				nameInfo.objectType = VK_OBJECT_TYPE_IMAGE;
+				nameInfo.objectHandle = (uint64_t)textureImage;
+			}
+			else
+			{
+				nameInfo.objectType = VK_OBJECT_TYPE_IMAGE_VIEW;
+				nameInfo.objectHandle = (uint64_t)textureImageView;
+			}
 			nameInfo.pObjectName = debugName.c_str();
 			vkSetDebugUtilsObjectNameEXT(device, &nameInfo);
 		}
@@ -230,13 +266,15 @@ void Texture::unloadVolatile()
 		textureImageAllocation = textureImageAllocation,
 		textureImageViews = std::move(renderTargetImageViews)] () {
 		vkDestroyImageView(device, textureImageView, nullptr);
-		vmaDestroyImage(allocator, textureImage, textureImageAllocation);
+		if (textureImageAllocation)
+			vmaDestroyImage(allocator, textureImage, textureImageAllocation);
 		for (const auto &views : textureImageViews)
 			for (const auto &view : views)
 				vkDestroyImageView(device, view, nullptr);
 	});
 
 	textureImage = VK_NULL_HANDLE;
+	textureImageAllocation = VK_NULL_HANDLE;
 
 	setGraphicsMemorySize(0);
 }
@@ -278,8 +316,7 @@ ptrdiff_t Texture::getHandle() const
 
 void Texture::setSamplerState(const SamplerState &s)
 {
-	love::graphics::Texture::setSamplerState(s);
-
+	samplerState = validateSamplerState(s);
 	textureSampler = vgfx->getCachedSampler(samplerState);
 }
 
@@ -291,16 +328,15 @@ VkImageLayout Texture::getImageLayout() const
 void Texture::createTextureImageView()
 {
 	auto vulkanFormat = Vulkan::getTextureFormat(format);
-
 	VkImageViewCreateInfo viewInfo{};
 	viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
 	viewInfo.image = textureImage;
 	viewInfo.viewType = Vulkan::getImageViewType(getTextureType());
 	viewInfo.format = vulkanFormat.internalFormat;
 	viewInfo.subresourceRange.aspectMask = imageAspect;
-	viewInfo.subresourceRange.baseMipLevel = 0;
+	viewInfo.subresourceRange.baseMipLevel = rootView.startMipmap;
 	viewInfo.subresourceRange.levelCount = getMipmapCount();
-	viewInfo.subresourceRange.baseArrayLayer = 0;
+	viewInfo.subresourceRange.baseArrayLayer = rootView.startLayer;
 	viewInfo.subresourceRange.layerCount = layerCount;
 	viewInfo.components.r = vulkanFormat.swizzleR;
 	viewInfo.components.g = vulkanFormat.swizzleG;
@@ -404,16 +440,16 @@ void Texture::generateMipmapsInternal()
 	barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
 	barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
 	barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-	barrier.subresourceRange.baseArrayLayer = 0;
+	barrier.subresourceRange.baseArrayLayer = rootView.startLayer;
 	barrier.subresourceRange.layerCount = static_cast<uint32_t>(layerCount);
-	barrier.subresourceRange.baseMipLevel = 0;
+	barrier.subresourceRange.baseMipLevel = rootView.startMipmap;
 	barrier.subresourceRange.levelCount = 1u;
 
 	uint32_t mipLevels = static_cast<uint32_t>(getMipmapCount());
 
 	for (uint32_t i = 1; i < mipLevels; i++)
 	{
-		barrier.subresourceRange.baseMipLevel = i - 1;
+		barrier.subresourceRange.baseMipLevel = rootView.startMipmap + i - 1;
 		barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
 		barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
 		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -429,15 +465,15 @@ void Texture::generateMipmapsInternal()
 		blit.srcOffsets[0] = { 0, 0, 0 };
 		blit.srcOffsets[1] = { getPixelWidth(i - 1), getPixelHeight(i - 1), 1 };
 		blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-		blit.srcSubresource.mipLevel = i - 1;
-		blit.srcSubresource.baseArrayLayer = 0;
+		blit.srcSubresource.mipLevel = rootView.startMipmap + i - 1;
+		blit.srcSubresource.baseArrayLayer = rootView.startLayer;
 		blit.srcSubresource.layerCount = static_cast<uint32_t>(layerCount);
 
 		blit.dstOffsets[0] = { 0, 0, 0 };
 		blit.dstOffsets[1] = { getPixelWidth(i), getPixelHeight(i), 1 };
 		blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
-		blit.dstSubresource.mipLevel = i;
-		blit.dstSubresource.baseArrayLayer = 0;
+		blit.dstSubresource.mipLevel = rootView.startMipmap + i;
+		blit.dstSubresource.baseArrayLayer = rootView.startLayer;
 		blit.dstSubresource.layerCount = static_cast<uint32_t>(layerCount);
 
 		vkCmdBlitImage(commandBuffer, 
@@ -458,7 +494,7 @@ void Texture::generateMipmapsInternal()
 				1, &barrier);
 	}
 
-	barrier.subresourceRange.baseMipLevel = mipLevels - 1;
+	barrier.subresourceRange.baseMipLevel = rootView.startMipmap + mipLevels - 1;
 	barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
 	barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
 	barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
@@ -496,11 +532,11 @@ void Texture::uploadByteData(const void *data, size_t size, int level, int slice
 	region.bufferRowLength = 0;
 	region.bufferImageHeight = 0;
 
-	uint32_t baseLayer;
-	if (getTextureType() == TEXTURE_VOLUME)
-		baseLayer = 0;
-	else
-		baseLayer = slice;
+	uint32_t baseLayer = rootView.startLayer;
+	if (getTextureType() != TEXTURE_VOLUME)
+		baseLayer += slice;
+
+	level += rootView.startMipmap;
 
 	region.imageSubresource.aspectMask = imageAspect;
 	region.imageSubresource.mipLevel = level;
@@ -558,8 +594,8 @@ void Texture::copyFromBuffer(graphics::Buffer *source, size_t sourceoffset, int
 
 	VkImageSubresourceLayers layers{};
 	layers.aspectMask = imageAspect;
-	layers.mipLevel = mipmap;
-	layers.baseArrayLayer = slice;
+	layers.mipLevel = mipmap + rootView.startMipmap;
+	layers.baseArrayLayer = slice + rootView.startLayer;
 	layers.layerCount = 1;
 
 	VkBufferImageCopy region{};
@@ -588,8 +624,8 @@ void Texture::copyToBuffer(graphics::Buffer *dest, int slice, int mipmap, const
 
 	VkImageSubresourceLayers layers{};
 	layers.aspectMask = imageAspect;
-	layers.mipLevel = mipmap;
-	layers.baseArrayLayer = slice;
+	layers.mipLevel = mipmap + rootView.startMipmap;
+	layers.baseArrayLayer = slice + rootView.startLayer;
 	layers.layerCount = 1;
 
 	VkBufferImageCopy region{};

+ 6 - 4
src/modules/graphics/vulkan/Texture.h

@@ -40,11 +40,13 @@ class Texture final
 	, public Volatile
 {
 public:
+
 	Texture(love::graphics::Graphics *gfx, const Settings &settings, const Slices *data);
-	~Texture();
+	Texture(love::graphics::Graphics *gfx, love::graphics::Texture *base, const Texture::ViewSettings &viewsettings);
+	virtual ~Texture();
 
-	virtual bool loadVolatile() override;
-	virtual void unloadVolatile() override;
+	bool loadVolatile() override;
+	void unloadVolatile() override;
 	 
 	void setSamplerState(const SamplerState &s) override;
 
@@ -61,7 +63,7 @@ public:
 
 	void uploadByteData(const void *data, size_t size, int level, int slice, const Rect &r) override;
 
-	void generateMipmapsInternal()  override;
+	void generateMipmapsInternal() override;
 
 	int getMSAA() const override;
 	ptrdiff_t getHandle() const override;

+ 69 - 0
src/modules/graphics/wrap_Graphics.cpp

@@ -799,6 +799,13 @@ static void luax_checktexturesettings(lua_State *L, int idx, bool opt, bool chec
 		if (s.type == TEXTURE_2D_ARRAY || s.type == TEXTURE_VOLUME)
 			s.layers = luax_checkintflag(L, idx, Texture::getConstant(Texture::SETTING_LAYERS));
 	}
+	else
+	{
+		s.width = luax_intflag(L, idx, Texture::getConstant(Texture::SETTING_WIDTH), s.width);
+		s.height = luax_intflag(L, idx, Texture::getConstant(Texture::SETTING_HEIGHT), s.height);
+		if (s.type == TEXTURE_2D_ARRAY || s.type == TEXTURE_VOLUME)
+			s.layers = luax_intflag(L, idx, Texture::getConstant(Texture::SETTING_LAYERS), s.layers);
+	}
 
 	lua_getfield(L, idx, Texture::getConstant(Texture::SETTING_MIPMAPS));
 	if (!lua_isnoneornil(L, -1))
@@ -823,6 +830,7 @@ static void luax_checktexturesettings(lua_State *L, int idx, bool opt, bool chec
 	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);
+	s.viewFormats = luax_boolflag(L, idx, Texture::getConstant(Texture::SETTING_VIEW_FORMATS), s.viewFormats);
 
 	lua_getfield(L, idx, Texture::getConstant(Texture::SETTING_READABLE));
 	if (!lua_isnoneornil(L, -1))
@@ -1240,6 +1248,66 @@ int w_newVolumeImage(lua_State *L)
 	return w_newVolumeTexture(L);
 }
 
+int w_newTextureView(lua_State *L)
+{
+	Texture *base = luax_checktexture(L, 1);
+	luaL_checktype(L, 2, LUA_TTABLE);
+
+	Texture::ViewSettings settings;
+
+	lua_getfield(L, 2, "format");
+	if (!lua_isnoneornil(L, -1))
+	{
+		const char *str = luaL_checkstring(L, -1);
+		if (!getConstant(str, settings.format.value))
+			luax_enumerror(L, "pixel format", str);
+		settings.format.hasValue = true;
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "type");
+	if (!lua_isnoneornil(L, -1))
+	{
+		const char *str = luaL_checkstring(L, -1);
+		if (!Texture::getConstant(str, settings.type.value))
+			luax_enumerror(L, "texture type", Texture::getConstants(settings.type.value), str);
+		settings.type.hasValue = true;
+	}
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "mipmapstart");
+	if (!lua_isnoneornil(L, -1))
+		settings.mipmapStart.set(luaL_checkint(L, -1) - 1);
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "mipmapcount");
+	if (!lua_isnoneornil(L, -1))
+		settings.mipmapCount.set(luaL_checkint(L, -1));
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "layerstart");
+	if (!lua_isnoneornil(L, -1))
+		settings.layerStart.set(luaL_checkint(L, -1) - 1);
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "layers");
+	if (!lua_isnoneornil(L, -1))
+		settings.layerCount.set(luaL_checkint(L, -1));
+	lua_pop(L, 1);
+
+	lua_getfield(L, 2, "debugname");
+	if (!lua_isnoneornil(L, -1))
+		settings.debugName = luaL_checkstring(L, -1);
+	lua_pop(L, 1);
+
+	Texture *t = nullptr;
+	luax_catchexcept(L, [&]() { t = instance()->newTextureView(base, settings); });
+
+	luax_pushtype(L, t);
+	t->release();
+	return 1;
+}
+
 int w_newQuad(lua_State *L)
 {
 	luax_checkgraphicscreated(L);
@@ -3857,6 +3925,7 @@ static const luaL_Reg functions[] =
 	{ "newCubeTexture", w_newCubeTexture },
 	{ "newArrayTexture", w_newArrayTexture },
 	{ "newVolumeTexture", w_newVolumeTexture },
+	{ "newTextureView", w_newTextureView },
 	{ "newQuad", w_newQuad },
 	{ "newFont", w_newFont },
 	{ "newImageFont", w_newImageFont },

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

@@ -300,6 +300,13 @@ int w_Texture_isReadable(lua_State *L)
 	return 1;
 }
 
+int w_Texture_hasViewFormats(lua_State *L)
+{
+	Texture *t = luax_checktexture(L, 1);
+	luax_pushboolean(L, t->hasViewFormats());
+	return 1;
+}
+
 int w_Texture_setDepthSampleMode(lua_State *L)
 {
 	Texture *t = luax_checktexture(L, 1);
@@ -511,6 +518,7 @@ const luaL_Reg w_Texture_functions[] =
 	{ "isCanvas", w_Texture_isCanvas },
 	{ "isComputeWritable", w_Texture_isComputeWritable },
 	{ "isReadable", w_Texture_isReadable },
+	{ "hasViewFormats", w_Texture_hasViewFormats },
 	{ "getMipmapMode", w_Texture_getMipmapMode },
 	{ "getDepthSampleMode", w_Texture_getDepthSampleMode },
 	{ "setDepthSampleMode", w_Texture_setDepthSampleMode },