Sparse Residency Texturen in Vulkan

Texturen machen CGI reich. Die Größe der Texturen ist wichtig. Kleine Texturen ergeben beim Vergrößern ein Bild mit großen Pixeln oder Seife. In diesem Artikel geht es darum, wie man riesige Texturen zeichnet.



Bild



Große Texturprobleme



Die Idee, riesige Texturen zu rendern, ist an sich nicht neu. Es scheint, dass was einfacher sein könnte - eine riesige Textur von einer Million Megapixeln laden und ein Objekt damit zeichnen. Aber wie immer gibt es Nuancen:



  1. Grafik-APIs begrenzen die maximale Größe einer Textur in Breite und Höhe. Dies kann sowohl von der Hardware als auch von den Treibern abhängen. Die maximale Größe für heute beträgt 32768x32768 Pixel.
  2. Selbst wenn wir an diese Grenzen stoßen, benötigt die RGBA-Textur 32768x32768 4 Gigabyte Videospeicher. Der Videospeicher ist schnell, liegt auf einem breiten Bus, ist aber relativ teuer. Daher ist es normalerweise weniger als der Systemspeicher und viel weniger als der Plattenspeicher.


1. Modernes Rendern großer Texturen



Da das Bild nicht in die Grenzen passt, bietet sich natürlich eine Lösung an - brechen Sie es einfach in Teile (Kacheln):







Für die analytische Geometrie werden immer noch verschiedene Variationen dieses Ansatzes verwendet. Dies ist kein universeller Ansatz, sondern erfordert nicht triviale Berechnungen auf der CPU. Jede Kachel wird als separates Objekt gezeichnet, was den Overhead erhöht und die Möglichkeit der Anwendung der bilinearen Texturfilterung ausschließt (zwischen den Kachelgrenzen befindet sich eine sichtbare Linie). Die Beschränkung der Texturgröße kann jedoch durch Texturarrays umgangen werden! Ja, diese Textur hat immer noch eine begrenzte Breite und Höhe, aber es wurden zusätzliche Schichten angezeigt. Die Anzahl der Ebenen ist ebenfalls begrenzt, aber Sie können sich auf 2048 verlassenObwohl die Spezifikation des Vulkans nur 256 verspricht. Auf einer 1060 GTX-Grafikkarte können Sie eine Textur mit 32768 * 32768 * 2048 Pixel erstellen. Es ist einfach nicht möglich, es zu erstellen, da es 8 Terabyte benötigt und nicht so viel Videospeicher vorhanden ist. Wenn Sie den Hardwarekomprimierungsblock BC1 darauf anwenden, würde eine solche Textur "nur" 1 Terabyte belegen. Es passt immer noch nicht in eine Grafikkarte, aber ich werde Ihnen sagen, was Sie weiter damit machen sollen.



Also schneiden wir das Originalbild immer noch in Stücke. Aber jetzt wird es keine separate Textur für jede Kachel sein, sondern nur ein Stück in einem riesigen Texturarray, das alle Kacheln enthält. Jeder Block hat seinen eigenen Index, alle Blöcke sind nacheinander angeordnet. Zuerst nach Spalten, dann nach Zeilen, dann nach Ebenen:







Ein kleiner Exkurs über die Quellen der Testtextur



Zum Beispiel - ich habe von hier aus ein Bild von der Erde gemacht . Ich habe die ursprüngliche Größe 43200x2160 auf 65536x32768 erhöht. Dies fügte natürlich keine Details hinzu, aber ich bekam das Bild, das ich brauchte, das nicht in eine Texturebene passt. Dann habe ich es mit bilinearer Filterung rekursiv halbiert, bis ich eine 512 x 256 Pixel große Kachel bekam. Dann habe ich die resultierenden Schichten in 512x256 Kacheln geschlagen. Komprimierte sie BC1 und schrieb sie nacheinander in eine Datei.







Etwa so: Als Ergebnis haben wir eine Datei mit 1.431.633.920 Bytes erhalten, die aus 21845 Kacheln besteht. Die Größe 512 x 256 ist nicht zufällig. Ein komprimiertes Bild von 512 x 256 BC1 hat genau 65536 Byte, was der Blockgröße des spärlichen Bildes entspricht - dem Helden dieses Artikels. Die Kachelgröße ist für das Rendern nicht wichtig.



Beschreibung der Technik zum Malen großer Texturen



Wir haben also ein Texturarray geladen, in dem die Kacheln aufeinanderfolgende Spalten / Linien / Ebenen sind.



Dann könnte der Shader, der genau diese Textur zeichnet, so aussehen:



layout(set=0, binding=0) uniform sampler2DArray u_Texture;
layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_Color;

int lodBase[8] = { 0, 1, 5, 21, 85, 341, 1365, 5461};

int tilesInWidth = 32768 / 512;
int tilesInHeight = 32768 / 256;
int tilesInLayer = tilesInWidth * tilesInHeight;

void main() {
  float lod = log2(1.0f / (512.0f * dFdx(v_uv.x)));
  int iLod = int(clamp(floor(lod),0,7));
  int cellsSize = int(pow(2,iLod));

  int tX = int(v_uv.x * cellsSize); //column index in current level of detail
  int tY = int(v_uv.y * cellsSize); //row index in current level of detail

  int tileID = lodBase[iLod] + tX + tY * cellsSize; //global tile index

  int layer = tileID / tilesInLayer;
  int row = (tileID % tilesInLayer) / tilesInWidth;
  int column = (tileID % tilesInWidth);
  vec2 inTileUV = fract(v_uv * cellsSize);
  vec2 duv = (inTileUV  + vec2(column,row)) / vec2(tilesInWidth,tilesInHeight);
  out_Color = texelFetch(u_Texture,ivec3(duv * textureSize(u_Texture,0).xy,layer),0);
}


Werfen wir einen Blick auf diesen Shader. Zunächst müssen wir bestimmen, welchen Detaillierungsgrad wir wählen sollen. Die wunderbare Funktion dFdx hilft uns dabei . Zur Vereinfachung wird der Wert zurückgegeben, um den das übergebene Attribut im benachbarten Pixel größer ist. In der Demo zeichne ich ein flaches Rechteck mit Texturkoordinaten im Bereich 0..1. Wenn dieses Rechteck X Pixel breit ist, gibt dFdx (v_uv.x) 1 / X zurück. Somit fällt die Kachel der ersten Ebene mit dFdx == 1/512 Pixel zu Pixel. Der zweite um 1/1024, der dritte um 1/2048 usw. Der Detaillierungsgrad selbst kann wie folgt berechnet werden: log2 (1.0f / (512.0f * dFdx (v_uv.x))). Lassen Sie uns den Bruchteil davon abschneiden. Dann zählen wir, wie viele Kacheln in der Ebene in Breite / Höhe sind.



Betrachten wir die Berechnung des Restes anhand eines Beispiels:







hier ist lod = 2, u = 0,65, v = 0,37,

da lod gleich zwei ist, dann ist cellsSize gleich vier. Das Bild zeigt, dass diese Ebene aus 16 Kacheln (4 Zeilen 4 Spalten) besteht - alles ist korrekt.



tX = int (0,65 · 4) = int (2,6) = 2

tY = int (0,37 · 4) = int (1,48) = 1,



d.h. Innerhalb der Ebene befindet sich diese Kachel in der dritten Spalte und zweiten Zeile (Indizierung von Null).

Wir brauchen auch die lokalen Koordinaten des Fragments (gelbe Pfeile im Bild). Sie können einfach berechnet werden, indem einfach die ursprünglichen Texturkoordinaten mit der Anzahl der Zellen in einer Zeile / Spalte multipliziert und der Bruchteil genommen werden. In den obigen Berechnungen sind sie bereits vorhanden - 0,6 und 0,48.



Jetzt brauchen wir einen globalen Index für diese Kachel. Dafür verwende ich das vorberechnete Array lodBase. Darin werden nach Index die Werte für die Anzahl der Kacheln in allen vorherigen (kleineren) Ebenen gespeichert. Fügen Sie den lokalen Index der Kachel innerhalb der Ebene hinzu. Zum Beispiel stellt sich heraus, dass lodBase [2] + 1 * 4 + 2 = 5 + 4 + 2 = 11. Was auch richtig ist.



Wenn wir den globalen Index kennen, müssen wir jetzt die Koordinaten der Kachel in unserem Texturarray finden. Dazu müssen wir wissen, wie viele Fliesen in Breite und Höhe passen. Ihr Produkt ist, wie viele Fliesen in die Schicht passen. In diesem Beispiel habe ich diese Konstanten der Einfachheit halber direkt in den Shader-Code eingefügt. Als nächstes erhalten wir die Texturkoordinaten und lesen das Texel daraus. Bitte beachten Sie, dass sampler2DArray als Sampler verwendet wird . Daher texelFetch Wir übergeben einen Dreikomponentenvektor in der dritten Koordinate - der Ebenennummer.



Texturen nicht vollständig geladen (Teilresidenzbilder)



Wie ich oben geschrieben habe, verbrauchen riesige Texturen viel Videospeicher. Darüber hinaus wird aus dieser Textur eine sehr kleine Anzahl von Pixeln verwendet. Die Lösung des Problems - Partial Residency Textures erschien 2011. Seine Essenz ist kurz - die Kachel ist möglicherweise nicht physisch im Gedächtnis! Gleichzeitig garantiert die Spezifikation, dass die Anwendung nicht abstürzt, und alle bekannten Implementierungen garantieren, dass Nullen zurückgegeben werden. Außerdem spezifiziert die Spezifikation, dass, wenn die Erweiterung unterstützt wird, die garantierte Blockgröße in Bytes unterstützt wird - 64 kibytes. Die Auflösungen der Bausteine ​​in der Textur sind an diese Größe gebunden:

TEXELGRÖSSE (Bits) Blockform (2D) Blockform (3D)
4-Bit? 512 × 256 × 1 nicht unterstützt
8 Bit 256 × 256 × 1 64 × 32 × 32
16-Bit 256 × 128 × 1 32 × 32 × 32
32-Bit 128 × 128 × 1 32 × 32 × 16
64-Bit 128 × 64 × 1 32 × 16 × 16
128-Bit 64 × 64 × 1 16 × 16 × 16


Tatsächlich enthält die Spezifikation nichts über 4-Bit-Texel, aber wir können sie mithilfe von vkGetPhysicalDeviceSparseImageFormatProperties immer herausfinden .



VkSparseImageFormatProperties sparseProps;
ermy::u32 propsNum = 1;

vkGetPhysicalDeviceSparseImageFormatProperties(hphysicalDevice, VK_FORMAT_BC1_RGB_SRGB_BLOCK, VkImageType::VK_IMAGE_TYPE_2D,
			VkSampleCountFlagBits::VK_SAMPLE_COUNT_1_BIT, VkImageUsageFlagBits::VK_IMAGE_USAGE_SAMPLED_BIT | VkImageUsageFlagBits::VK_IMAGE_USAGE_TRANSFER_DST_BIT
			, VkImageTiling::VK_IMAGE_TILING_OPTIMAL, &propsNum, &sparseProps);

int pageWidth = sparseProps.imageGranularity.width;
int pageHeight = sparseProps.imageGranularity.height;


Die Erzeugung einer solch spärlichen Textur unterscheidet sich von der üblichen.



Erstens muss in VkImageCreateInfo in Flags VK_IMAGE_CREATE_SPARSE_BINDING_BIT und VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT angegeben werden.



Zweitens ist eine Bindung über den Speicher vkBindImageMemory nicht erforderlich.



Sie müssen herausfinden, welche Speichertypen über vkGetImageMemoryRequirements verwendet werden können . Außerdem erfahren Sie, wie viel Speicher zum Laden der gesamten Textur benötigt wird, diese Zahl wird jedoch nicht benötigt.



Stattdessen müssen wir auf Anwendungsebene entscheiden, wie viele Kacheln gleichzeitig sichtbar sein können.



Nach dem Laden einiger Kacheln werden andere entladen, da sie nicht mehr benötigt werden. In der Demo habe ich nur mit dem Finger auf den Himmel gezeigt und eintausendvierundzwanzig Kacheln gespeichert. Klingt verschwenderisch, ist aber nur 50 Megabyte gegenüber 1,4 GB einer voll geladenen Textur. Sie müssen auch Speicher auf dem Host für das Staging zuweisen - einen Puffer.



const int sparseBlockSize = 65536;
int numHotPages = 512; //    
VkMemoryRequirements memReqsOpaque;
vkGetImageMemoryRequirements(device, mySparseImage, &memReqsOpaque); //    memoryTypeBits.       - 

VkMemoryRequirements image_memory_requirements;
image_memory_requirements.alignment = sparseBlockSize ; //  
image_memory_requirements.size = sparseBlockSize * numHotPages;
image_memory_requirements.memoryTypeBits = memReqsOpaque.memoryTypeBits;


Auf diese Weise erhalten wir eine riesige Textur, in die nur einige Teile geladen werden. Es wird ungefähr so ​​aussehen:







Fliesenmanagement



Im Folgenden werde ich den Begriff Kachel verwenden , um ein Stück Textur zu bezeichnen (dunkelgrüne und graue Quadrate in der Abbildung), und den Begriff Seite , um ein Stück in einem großen Block zu bezeichnen, der im Videospeicher vorbelegt ist (hellgrüne und hellblaue Rechtecke in der Abbildung).



Nachdem Sie ein so spärliches VkImage erstellt haben , kann es über die VkImageView im Shader verwendet werden. Dies ist natürlich nutzlos - das Abtasten gibt Nullen zurück, es gibt keine Daten, aber im Gegensatz zu dem üblichen VkImage fällt nichts und die Debug-Ebenen schwören nicht. Die Daten in dieser Textur müssen nicht nur geladen, sondern auch entladen werden, da wir Videospeicher sparen.



Der OpenGL-Ansatz, der die Speicherzuweisung durch den Treiber für jeden Block vorsieht, scheint mir nicht korrekt zu sein. Ja, vielleicht wird dort ein cleverer und schneller Allokator verwendet, weil die Blockgröße fest ist. Dies wird auch durch die Tatsache angedeutet, dass ein ähnlicher Ansatz am Beispiel spärlicher Residenztexturen in einem Vulkan verwendet wird. Wählen Sie in jedem Fall einen großen linearen Seitenblock aus und binden Sie diese Seiten auf der Anwendungsseite an bestimmte Texturkacheln, und füllen Sie sie mit Daten, die definitiv nicht langsamer sind.



Daher enthält die Schnittstelle unserer spärlichen Textur Methoden wie:



void CommitTile(int tileID, void* dataPtr); //    64 
void FreeTile(int tileID);
void Flush();


Die letzte Methode wird benötigt, um das Füllen / Freigeben von Kacheln zu gruppieren. Das Aktualisieren von Kacheln nacheinander ist ziemlich teuer, nur einmal pro Frame. Schauen wir sie uns der Reihe nach an.



//void CommitTile(int tileID, void* dataPtr)
int freePageID = _getFreePageID();

if (freePageID != -1)
{
	tilesByPageIndex[freePageID] = tileID;
	tilesByTileID[tileID] = freePageID;

	memcpy(stagingPtr + freePageID * pageDataSize, tileData, pageDataSize);

	int pagesInWidth = textureWidth / pageWidth;
	int pagesInHeight = textureHeight / pageHeight;
	int pagesInLayer = pagesInWidth * pagesInHeight;

	int layer = tileID / pagesInLayer;
	int row = (tileID % pagesInLayer) / pagesInWidth;
	int column = tileID % pagesInWidth;

	VkSparseImageMemoryBind mbind;
	mbind.subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	mbind.subresource.mipLevel = 0;
	mbind.subresource.arrayLayer = layer;
	mbind.extent.width = pageWidth;
	mbind.extent.height = pageHeight;
	mbind.extent.depth = 1;
	mbind.offset.x = column * pageWidth;
	mbind.offset.y = row * pageHeight;
	mbind.offset.z = 0;
	mbind.memory = optimalTilingMem;
	mbind.memoryOffset = freePageID * pageDataSize;
	mbind.flags = 0;
	memoryBinds.push_back(mbind);

	VkBufferImageCopy copyRegion;
	copyRegion.bufferImageHeight = pageHeight;
	copyRegion.bufferRowLength = pageWidth;
	copyRegion.bufferOffset = mbind.memoryOffset; 
	copyRegion.imageExtent.depth = 1;
	copyRegion.imageExtent.width = pageWidth;
	copyRegion.imageExtent.height = pageHeight;
	copyRegion.imageOffset.x = mbind.offset.x;
	copyRegion.imageOffset.y = mbind.offset.y;
	copyRegion.imageOffset.z = 0;
	copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	copyRegion.imageSubresource.baseArrayLayer = layer;
	copyRegion.imageSubresource.layerCount = 1;
	copyRegion.imageSubresource.mipLevel = 0;
	copyRegions.push_back(copyRegion);

        return true;
}

return false;


Zuerst müssen wir einen freien Block finden. Ich gehe einfach das Array dieser Seiten durch und suche nach der ersten, die die Stub-Nummer -1 enthält. Dies ist der Index der freien Seite. Ich kopiere Daten von der Festplatte mit memcpy in den Staging-Puffer. Die Quelle ist eine Speicherzuordnungsdatei mit einem Versatz für eine bestimmte Kachel. Ferner betrachte ich anhand der ID der Kachel ihre Position (x, y, Ebene) im Texturarray.



Als nächstes beginnt das Interessanteste - das Ausfüllen der VkSparseImageMemoryBind- Struktur . Sie ist es, die den Videospeicher an die Kachel bindet. Seine wichtigen Felder sind:



Erinnerung . Dies ist ein VkDeviceMemory- Objekt . Es hat allen Seiten Speicher zugewiesen.



memoryOffset . Dies ist der Versatz in Bytes zu der Seite, die wir benötigen.



Als nächstes müssen wir Daten aus dem Staging-Puffer in diesen frisch gebundenen Speicher kopieren. Dies erfolgt mit vkCmdCopyBufferToImage .



Da wir viele Abschnitte gleichzeitig kopieren werden, füllen wir an dieser Stelle nur die Struktur mit einer Beschreibung, wo und wo wir kopieren werden. Hier ist bufferOffset wichtig , das den Offset angibt, der sich bereits im Staging-Puffer befindet. In diesem Fall stimmt es mit dem Versatz im Videospeicher überein, aber die Strategien können unterschiedlich sein. Teilen Sie die Fliesen beispielsweise in heiß, warm und kalt. Die heißen befinden sich im Videospeicher, die warmen im RAM und die kalten auf der Festplatte. Dann kann der Staging-Puffer größer sein und der Offset ist unterschiedlich.



//void FreeTile(int tileID)
if (tilesByTileID.count(tileID) > 0)
{
	i16 hotPageID = tilesByTileID[tileID];

	int pagesInWidth = textureWidth / pageWidth;
	int pagesInHeight = textureHeight / pageHeight;
	int pagesInLayer = pagesInWidth * pagesInHeight;

	int layer = tileID / pagesInLayer;
	int row = (tileID % pagesInLayer) / pagesInWidth;
	int column = tileID % pagesInWidth;

	VkSparseImageMemoryBind mbind;
	mbind.memory = optimalTilingMem;
	mbind.subresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	mbind.subresource.mipLevel = 0;
	mbind.subresource.arrayLayer = layer;
	mbind.extent.width = pageWidth;
	mbind.extent.height = pageHeight;
	mbind.extent.depth = 1;
	mbind.offset.x = column * pageWidth;
	mbind.offset.y = row * pageHeight;
	mbind.offset.z = 0;
	mbind.memory = VK_NULL_HANDLE;
	mbind.memoryOffset = 0;
	mbind.flags = 0;

	memoryBinds.push_back(mbind);

	tilesByPageIndex[hotPageID] = -1;
	tilesByTileID.erase(tileID);

	return true;
}

return false;


Hier entkoppeln wir den Speicher von der Kachel. Weisen Sie dazu den Speicher VK_NULL_HANDLE zu .



//void Flush();
cbuff = hostDevice->CreateOneTimeSubmitCommandBuffer();

VkImageSubresourceRange imageSubresourceRange;
imageSubresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageSubresourceRange.baseMipLevel = 0;
imageSubresourceRange.levelCount = 1;	
imageSubresourceRange.baseArrayLayer = 0;
imageSubresourceRange.layerCount = numLayers;

VkImageMemoryBarrier bSamplerToTransfer;
bSamplerToTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
bSamplerToTransfer.pNext = nullptr;
bSamplerToTransfer.srcAccessMask = 0;
bSamplerToTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
bSamplerToTransfer.oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
bSamplerToTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
bSamplerToTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bSamplerToTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bSamplerToTransfer.image = opaqueImage;
bSamplerToTransfer.subresourceRange = imageSubresourceRange;

VkSparseImageMemoryBindInfo imgBindInfo;
imgBindInfo.image = opaqueImage;
imgBindInfo.bindCount = memoryBinds.size();
imgBindInfo.pBinds = memoryBinds.data();

VkBindSparseInfo sparseInfo;
sparseInfo.sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO;
sparseInfo.pNext = nullptr;
sparseInfo.waitSemaphoreCount = 0;
sparseInfo.pWaitSemaphores = nullptr;
sparseInfo.bufferBindCount = 0;
sparseInfo.pBufferBinds = nullptr;
sparseInfo.imageOpaqueBindCount = 0;
sparseInfo.pImageOpaqueBinds = nullptr;
sparseInfo.imageBindCount = 1;
sparseInfo.pImageBinds = &imgBindInfo;
sparseInfo.signalSemaphoreCount = 0;
sparseInfo.pSignalSemaphores = nullptr;

VkImageMemoryBarrier bTransferToSampler;
bTransferToSampler.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
bTransferToSampler.pNext = nullptr;
bTransferToSampler.srcAccessMask = 0;
bTransferToSampler.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
bTransferToSampler.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
bTransferToSampler.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
bTransferToSampler.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bTransferToSampler.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
bTransferToSampler.image = opaqueImage;
bTransferToSampler.subresourceRange = imageSubresourceRange;
	
vkQueueBindSparse(graphicsQueue, 1, &sparseInfo, fence);

vkWaitForFences(device, 1, &fence, true, UINT64_MAX);
vkResetFences(device, 1, &fence);

vkCmdPipelineBarrier(cbuff, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bSamplerToTransfer);

if (copyRegions.size() > 0)
{
	vkCmdCopyBufferToImage(cbuff, stagingBuffer, opaqueImage, VkImageLayout::VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, copyRegions.size(), copyRegions.data());
}
	
vkCmdPipelineBarrier(cbuff, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &bTransferToSampler);

hostDevice->ExecuteCommandBuffer(cbuff);

copyRegions.clear();
memoryBinds.clear();


Die Hauptarbeit findet in dieser Methode statt. Zum Zeitpunkt des Aufrufs haben wir bereits zwei Arrays mit VkSparseImageMemoryBind und VkBufferImageCopy. Wir füllen die Strukturen zum Aufrufen von vkQueueBindSparse aus und rufen es auf. Dies ist keine blockierende Funktion (wie fast alle Funktionen in Vulkan), daher müssen wir explizit auf die Ausführung warten. Dazu wird der letzte Parameter an VkFence übergeben , auf dessen Ausführung wir warten werden. In meinem Fall hatte das Warten auf diese Fenza keinerlei Auswirkungen auf die Leistung des Programms. Aber theoretisch wird es hier benötigt.



Nachdem wir den Kacheln Speicher hinzugefügt haben, müssen wir Bilder darin ausfüllen. Dies erfolgt mit der Funktion vkCmdCopyBufferToImage .

Sie können die Daten in die Textur mit Layout füllenVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL und rufen Sie sie in einem Shader mit dem Layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL ab . Deshalb brauchen wir zwei Barrieren. Bitte beachten Sie, dass wir in VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL ausschließlich von VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL übersetzen , nicht von VK_IMAGE_LAYOUT_UNDEFINED . Da wir nur einen Teil der Textur füllen, ist es wichtig, dass wir nicht die Teile davon verlieren, die zuvor gefüllt wurden.



Hier ist ein Video, wie es funktioniert. Eine Textur. Ein Objekt. Zehntausende Fliesen.





Was hinter den Kulissen bleibt, ist, wie in der Anwendung bestimmt wird, wie tatsächlich herausgefunden wird, welche Kachel geladen und welche entladen werden muss. In dem Abschnitt, der die Vorteile des neuen Ansatzes beschreibt, war einer der Punkte, dass Sie komplexe Geometrie verwenden können. Im selben Test verwende ich selbst die einfachste orthografische Projektion und das einfachste Rechteck. Und ich zähle die ID der Kacheln analytisch. Unsportlich.



Tatsächlich werden die IDs der sichtbaren Kacheln zweimal gezählt. Analytisch auf der CPU und ehrlich auf dem Fragment Shader. Es scheint, warum sie nicht vom Fragment-Shader abholen? Aber so einfach ist das nicht. Dies wird der zweite Artikel sein.



All Articles