Az elmúlt hónapokban a példakódok refaktorálásával foglalkoztam, beleértve a korábbi hibák javítását, egy közös felületet a platformok könnyebb használatához, illetve néhány újdonságot. Amit viszont megtartottam, az a különféle API-k szétválasztása. Ez azt jelenti, hogy ha OpenGL példát nézel, akkor olyan kódot fogsz látni (sosem volt célom absztrakt felületet írni, arra ott van az engine-em).
Tartalomjegyzék
Rögtön ezzel kezdeném, a neten ugyanis nem írják le sehol, hogy mit kell csinálni a glslang-gal, hogy az új feature-öket ki tudja használni (a kódját kellett végignéznem, hogy rájöjjek).
Maga a Vulkan 1.1 beállítása nem bonyolult, egyszerűen azt a verziót adod meg az app_info.apiVersion adattagnak. Egészen meglepődtem, hogy a benti R7 360-as kártyám tudja ezt, és ezáltal a SPIR-V 1.3-at is (ami ugyanis kötelező Vulkan 1.1-ben).
A támogatás a vkEnumerateInstanceVersion() függvénnyel kérdezhető le. Vigyázat, ez a vulkán implementáció verzióját jelenti, szóval ugyanezt el kell végezni a kiválasztott physical device-ra is (deviceProps.apiVersion).
CODE
glslang::TShader shader(stage);
EShMessages messages = (EShMessages)(EShMsgSpvRules|EShMsgVulkanRules);
char* source = ...;
shader.setStrings(&source, 1);
shader.setEnvTarget(glslang::EShTargetSpv, glslang::EShTargetSpv_1_3);
shader.parse(&SPIRVResources, 110, false, messages);
Ahol a 110 azt akarja jelenteni, hogy desktopon fordítok (ha mobilra fordítasz, akkor a kommentek alapján 100-at kell megadni). Ha ezek közül bármelyiket elfelejted, akkor ugyanezt várhatod az újdonságoktól is. A többi dolog hasonlóan megy mint eddig, annyi különbséggel, hogy a glslang összes libjét hozzá kell linkelni a programhoz (még az opcionálisakat is). Ez nem lenne akkora probléma, ha nem foglalnának el közel 1 GB-ot... (megj.: és pont emiatt nem tudtam betenni ezeket a repóba) Ez elég mellbevágó volt, pontosabban az, hogy egy szót nem szólt eddig a validációs réteg. Szerencsére minden hibaüzenetben kiírja a linket a szabvány megfelelő bekezdésére, így könnyen javítható volt.
A lényeg, hogy a memóriának nem csak a címe kell align-olt legyen (ahogy írtam attól függően, hogy lineáris vagy optimális, illetve, hogy milyen után foglalod), hanem a mérete is:
CODE
VkDeviceSize alignment = driverInfo.deviceProps.limits.nonCoherentAtomSize;
size += (alignment - (size % alignment));
buffercreateinfo.size = size;
A memóriára akkor mondjuk, hogy koherens, ha a módosítások azonnal láthatóak annak, aki olvassa. Nem-koherens memória esetén a cache-elési logika érvényesül, és a szabvány megköveteli, hogy ekkor a nonCoherentAtomSize érték többszöröseivel legyen határolva. Ahogy az eredeti cikkben írtam, akkoriban még a szabvány sem volt egyértelmű; ezen természetesen változtattak én meg csak téptem a hajamat, hogy mit jelent egyáltalán a hibaüzenet, amit kapok. Röviden a barrier-ek hat meghatározó eleme (src/dst pipeline stage, src/dst access mask, src/dst image layout) nagyon szigorúan összefügg, amit ez a táblázat rögzít. Értsük ez alatt a következőket:
CODE
VulkanTemporaryCommandBuffer(true, true, [&](VkCommandBuffer transfercmd) -> bool {
VulkanPipelineBarrierBatch barrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT);
{
// az utolsó paraméter mondja meg, hogy generáljon-e mip szinteket; erről majd külön írok
texture->UploadToVRAM(barrier, transfercmd, true);
barrier.Reset(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT);
barrier.ImageLayoutTransition(
texture,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
0, 0, 0, 0);
}
barrier.Enlist(transfercmd);
return true;
});
Látható, hogy kénytelen voltam az src/dst pipeline stage-eket módosíthatóvá tenni, ilyenkor az addig megadott barrier-eket kitörli (bár nem feltétlenül lenne rá szükség, de akkor az Enlist() pillanatában mindegyikre tudnom kellene, hogy melyik pipeline stage-ben volt éppen). Egy másik fontos dolog, ami konkrétan hibát is okozott, hogy a layout transition nem vonja magával az access mask megváltozását is. Hivatkozva az eredeti cikkben írt tile based deferred renderer-re, a compute shader elé az alábbi kódot kellett bebiggyesztenem: CODE
// <g-buffer pass vége, az image-ek access mask-ja VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT>
barrier.Reset(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT);
{
barrier.ImageLayoutTransition(
gbuffernormals->GetImage(),
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_GENERAL,
VK_IMAGE_LAYOUT_GENERAL,
0, 1, 0, 1);
}
barrier.Enlist(primarycmdbuff);
vkCmdBindPipeline(primarycmdbuff, VK_PIPELINE_BIND_POINT_COMPUTE, accumpasspipeline->GetPipeline());
vkCmdDispatch(primarycmdbuff, workgroupsx, workgroupsy, 1);
// <forward pass kezdete>
Amíg ez nem volt a kódban, a képernyő tetején egy jól látható szemetelés történt; annak idején azért nem vettem észre, mert a modell csak egy adott részén jött elő (valószínűleg ott, ahol a legtöbb fényt kivágta). Ebből is látszik, hogy mennyire oda kell figyelni vulkán programozáskor, és ez tipikusan olyan hiba, amit a validációs réteg nem tud észrevenni. Na ettől tényleg megőszültem...összehasonlítva mondjuk a Metal egy, azaz egy darab hívásával, vulkánban ez egy teljes káosz. Először is, ahogy említettem az image minden subresource-a (mip szintek, array layer-ek) ugyanabban a layout-ban kell legyenek. Oké, ez még lenyelhető... Ahhoz viszont, hogy egy adott mip szintből másoljak egy másikba az egyik TRANSFER_SRC, a másik TRANSFER_DST layout-ban kell legyen (azaz minden másolás előtt be kell biggyeszteni egy barrier-t). Beszéljen inkább a kód:
CODE
VkBufferImageCopy region = ...;
VkImageBlit blit = ...;
// feltöltöm az alap (0.) mip szintet; minden subresource-ot TRANSFER_DST-be rakva
barrier.ImageLayoutTransition(
this,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
0, 0, 0, 0);
barrier.Enlist(commandbuffer);
vkCmdCopyBufferToImage(
commandbuffer,
stagingbuffer->GetBuffer(),
image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, ®ion);
// mip-chain generálás
for (uint32_t i = 1; i <= mipmapcount; ++i) {
// az előző mip szintet TRANSFER_SRC-be kell rakni
barrier.ImageLayoutTransition(
this,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_ASPECT_COLOR_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
i - 1, 1, 0, 0);
barrier.Enlist(commandbuffer);
// de az utolsót is, különben később nem lesz konzisztens
if (i < mipmapcount) {
blit.dstOffsets[1].x = Math::Max<uint32_t>(1, extents.width >> i);
blit.dstOffsets[1].y = Math::Max<uint32_t>(1, extents.height >> i);
blit.dstOffsets[1].z = 1;
blit.dstSubresource.mipLevel = i;
vkCmdBlitImage(
commandbuffer,
image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &blit, VK_FILTER_LINEAR);
}
}
Ehhez képest ugyanez metálban: CODE
MetalTemporaryCommandBuffer(queue, true, [=](id<MTLCommandBuffer> transferdmc) -> bool {
id<MTLBlitCommandEncoder> blitencoder = [transferdmc blitCommandEncoder];
[blitencoder generateMipmapsForTexture:ret];
[blitencoder endEncoding];
return true;
});
Tényleg olyan nehéz lett volna a Khronos-nak egy hasonló egysoros függvényt bevennie a szabványba? Persze, hogy nem, de akkor elveszíted az olyan perverziókat, mint pl. hogy csak egy adott array layer-nek generálsz mip szinteket (ami mint tudjuk nagyon reális use-case). Ha már Metal, akkor áttérek most egy kicsit arra. Mivel csak egy vacak Intel Iris Pro kártyám van, egetrengető dolgokat nem tudtam kipróbálni, viszont írtam egy forward+ renderer-t, ami még ezen a diavetítőn is meglepően jól fut (kis felbontáson...4K-ra kirakva Einsten-t meghazudtoló sebességre lassulnak a fények).
CODE
- (void)viewDidLoad {
[super viewDidLoad];
_view = (MTKView*)self.view;
_view.device = MTLCreateSystemDefaultDevice();
if (!_view.device) {
// nincs Metal támogatás
self.view = [[NSView alloc] initWithFrame:self.view.frame];
return;
}
_view.delegate = self;
_view.depthStencilPixelFormat = MTLPixelFormatDepth32Float_Stencil8;
_view.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
_view.sampleCount = 1;
}
CODE
- (void)drawInMTKView:(nonnull MTKView*)view {
// rajzolás; a view-tól elkérhető minden, amire szükség lehet
}
A felsoroltak mellett további előnye, hogy beállítható kézi rajzolásra is, ekkor neked kell meghívni rá a draw üzenetet. Annyival egészítettem ki, hogy reagáljon a billentyűzet/egér üzenetekre, de ezt értelemszerűen nem fogom itt bemutatni. A kérdésem itt az volt, hogy a három lehetséges tárolási mód közül (private, shared, managed) mikor melyiket érdemes használni. A negyedikkel (memoryless) nem foglalkoztam. Először is mit jelentenek ezek a tárolási módok:
Ezt is említettem már ugyan, de ide tartozik a CPU cache mode is:
CODE
MetalMesh::MetalMesh(...)
{
vertexbuffer = [device newBufferWithLength:numvertices * vertexstride
options:MTLResourceStorageModePrivate];
indexbuffer = [device newBufferWithLength:numindices * istride
options:MTLResourceStorageModePrivate];
// megj.: a cache mode nem fért ki (default)
stagingvertexbuffer = [device newBufferWithLength:numvertices * vertexstride
options:MTLResourceStorageModeShared];
stagingindexbuffer = [device newBufferWithLength:numindices * istride
options:MTLResourceStorageModeShared];
mappedvdata = (uint8_t*)[stagingvertexbuffer contents];
mappedidata = (uint8_t*)[stagingindexbuffer contents];
}
CODE
void MetalMesh::UploadToVRAM(id<MTLCommandBuffer> commandbuffer)
{
id<MTLBlitCommandEncoder> encoder = [commandbuffer blitCommandEncoder];
[encoder copyFromBuffer:stagingvertexbuffer
sourceOffset:0
toBuffer:vertexbuffer
destinationOffset:0
size:stagingvertexbuffer.length];
[encoder copyFromBuffer:stagingindexbuffer
sourceOffset:0
toBuffer:indexbuffer
destinationOffset:0
size:stagingindexbuffer.length];
[encoder endEncoding];
}
Ha valamilyen okból szükség van gyakori módosításra, akkor ki lehet egészíteni, de ahogy korábbi cikkekben említettem, a compute shader a legtöbb esetben meg tudja ezt oldani a CPU beavatkozása nélkül. ...és hogy miért fontos. Az egyszerű válasz az, hogy ha nem lenne fontos, akkor nem vették volna be a szabványba (vö.: a geometry shader-t és a tessellation control shader-t kihajították). A nemtriviális válasz az, hogy különbség van egy sima storage buffer és az előre megjelölt vertex adat olvasása között.
CODE
pipelinedesc.vertexFunction = [defaultlibrary newFunctionWithName:@"vs_main"];
NSArray* signature = pipelinedesc.vertexFunction.vertexAttributes;
MTLVertexDescriptor* descriptor = [[MTLVertexDescriptor alloc] init];
NSUInteger offset = 0;
for (int i = 0; i < [signature count]; ++i) {
MTLVertexAttribute* attrib = [signature objectAtIndex:i];
auto formatandsize = MapDataType(attrib.attributeType);
descriptor.attributes[i].bufferIndex = 0;
descriptor.attributes[i].format = formatandsize.first;
descriptor.attributes[i].offset = offset;
offset += formatandsize.second;
}
descriptor.layouts[0].stride = offset;
descriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
pipelinedesc.vertexDescriptor = descriptor;
Az egyes elemeket az [[attribute(n)]] minősítővel kell összeszedni egy struktúrába, amit a [[stage_in]] minősítővel kap meg a vertex shader. Azért említem meg, mert a látszólagos egyszerűsége ellenére sikerült elrontanom. A legegyszerűbb megoldás persze az MTKTextureLoader használata lenne, de keveset lehet tudni a belső működéséről (pl. sRGB vagy lineárisként tölti-e be). Az Igazi Programozó™ emiatt bizalmatlanul áll hozzá és inkább néhány sorral többet kódol: CODE
id<MTLTexture> MetalCreateTextureFromFile(..., const char* file, bool srgb)
{
NSURL* path = [NSURL fileURLWithPath:[NSString stringWithUTF8String:file]];
NSImage* img = [[NSImage alloc] initByReferencingURL:path];
CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
CGContextRef bitmap = CGBitmapContextCreate(
NULL, img.size.width, img.size.height, 8, 0,
colorspace, kCGImageAlphaPremultipliedLast);
NSGraphicsContext* context = [NSGraphicsContext graphicsContextWithCGContext:bitmap flipped:NO];
[NSGraphicsContext setCurrentContext:context];
[img drawInRect:rect];
MTLPixelFormat format = (srgb ? MTLPixelFormatRGBA8Unorm_sRGB : MTLPixelFormatRGBA8Unorm);
MTLTextureDescriptor* texdesc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:format
width:img.size.width
height:img.size.height
mipmapped:YES];
ret = [device newTextureWithDescriptor:texdesc];
[ret replaceRegion:region mipmapLevel:0 withBytes:CGBitmapContextGetData(bitmap) bytesPerRow:bytesperrow];
// ...
}
A mip szintek generálását fent leírtam. Szóval ja, nem mindegy hogy lineáris vagy nemlineáris adatot akarok betölteni: a normal map-ek például lineárisan vannak tárolva, és napokig nem jöttem rá, hogy miért volt rossz a megvilágítás. Azért, mert a Core Graphics oldaláról mindent sRGB-nek kell tekinteni (még ha nem is az). Egyéb formátumok (pl. .dds) betöltése ugyanúgy történik, mint máshol. Ezeket mindhárom API támogatja, a vulkán 1.1-től (subgroup operations), a metál 2-től (SIMD operations), a Direct3D 12 pedig amennyire tudom alapból (wave operations).
Mivel mindegyik máshogy nevezi, én maradok inkább az nVidia terminológiájánál (warp).
CODE
// megj.: a driverInfo struktúrában
VkPhysicalDeviceSubgroupProperties subgroupProps;
CODE
// megj.: physical device kiválasztása után
VkPhysicalDeviceProperties2 props2;
driverInfo.subgroupProps.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES;
driverInfo.subgroupProps.pNext = NULL;
props2.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2;
props2.pNext = &driverInfo.subgroupProps;
vkGetPhysicalDeviceProperties2(driverInfo.selectedDevice, &props2);
Amennyiben a subgroupSize legalább 1, akkor a műveletek támogatva vannak (de 32 alatt nincs benne köszönet). Fontos még a supportedStages változó, ami azt mondja meg, hogy milyen shader stage-ekben használhatóak ezek. A szabvány annyit követel meg, hogy ha van olyan queue, ami támogat compute shader-t, akkor ott használhatóak. Egyéb shader stage-ek nem feltétlenül támogatottak, szóval érdemes erre is figyelni. Magukat a műveleteket is vulkán oldalról mutatom be (zanzásítva), de a többi API-ban is hasonlóak értendőek:
Ez talán a legtriviálisabb példa a használatra: minden subgroup-ból megválasztok egy invokációt a subgroupElect függvénnyel, ami kiszámolja a színt, majd a subgroupBroadcastFirst függvénnyel közvetítem az eredményt az összes többi invokációnak. Persze meg lehetne ezt máshogy is oldani, például az első invokációval kiszámoltatom a színt egy shared változóba, utána barrier() és egyszerűen kiolvasom. Világos azonban, hogy ez a megoldás egyrészt a memóriába nyúl (még ha gyors elérésű is), másrészt megakasztja a végrehajtást. A subgroup memory lényegesen gyorsabb elérésű! Természetesen kísérleteztem a többi függvénnyel is (pl. subgroupBallot), hogy valami kevésbé triviális feltétel alapján kiválogassam az aktív és inaktív invokációkat, de ezt vizuálisan nehéz prezentálni, mert ígyis-úgyis kiszámolja; legfeljebb az egyik megoldás gyorsabb, a másik lassabb. A subgroup műveletek tehát kimondottan alacsony szintű optimalizációra lettek kitalálva, értelmes példákat nehéz konstruálni hozzájuk. A javaslatom ezért az, hogy összetett compute/fragment shader optimalizálásakor vegyétek figyelembe. Minden API-ban közös, hogy storage image formátuma nem lehet sRGB, ami ha belegondolunk logikus is, hiszen rajzoláskor úgyis lineáris színtérben kell számolni, akkor miért állítanál elő nemlineáris adatot? Hacsaknem egy tisztán compute shader alapú raytracer-ben, ami csak a végleges képet adja ki.
Merthogy van sokkal izgalmasabb kérdés is, ugyanis Metal-ban nincs geometry shader (és nem is lesz). Szóval például a vastag vonalakat vagy CPU-n állítod elő, vagy compute shader-t használsz. A neten nincs egyértelmű konszenzus arról, hogy melyik megoldás a nyerő, de a tapasztalataim alapján azt javaslom, hogy ne helyben csináld (4 vertex, 6 index, a vonalat pedig mindig visszafejted).
(megj.: nem írtam el, tényleg megcseréltem a sorrendet; ez attól függ, hogy hogyan indexeled meg a geometry buffer-t) Magát a számolást nem írom le, mert elég intuitív, helyette néhány fontos dologra hívnám fel a figyelmet. Az std430-ra igaz, hogy a struktúrán belül nem paddel 16 bájtra, de attól még az egymás után következő példányok 16 bájtra vannak igazítva! Napokig nem értettem, hogy miért egy katyvaszt kapok eredményül (eredetileg vec3-ban tároltam a pozíciókat), amíg egy fórumbejegyzés lábjegyzetében meg nem találtam ezt az "apróságot". Szerintem ezt piros betűkkel kellene kiemelni, hogy az alignment 16 bájt (kivéve uniform-oknál, ahol 256). A gyakorlatban ez azt jelenti, hogy a pozíció vec4 kell legyen CPU oldalon is! A másik fontos dolog a workgroup mérete. A fentiek értelmében legalább 64 kell legyen, de szinte kötelező minél nagyobbra venni (1024 maximum, ld. GL_MAX_COMPUTE_WORK_GROUP_SIZE), ugyanis a workgroup-ok száma 65535-ben van maximalizálva (ld. GL_MAX_COMPUTE_WORK_GROUP_COUNT). Ezek az értékek Radeon R7 360-ra igazak, más kártyákon eltérő lehet, szóval egy éles implementációban először kérdezd le ezeket, és úgy fordítsd a shader-t (illetve vulkánban használható a specialization constant, ahogy az eredeti cikkben leírtam). Na de számoljunk akkor: a workgroup méret maximum 1024, ezekből lehet maximum 65535, tehát egy mai alsó kategóriás kártyán 33.5 millió vonalat át lehet(ne) így konvertálni egyetlen glDispatchCompute hívással. Na jó, de memória oldalról is meg kell ezt vizsgálni: az SSBO legalább 128 MB-ban kell legyen maximalizálva a szabvány szerint, négy darab pozíció elfoglal 64 bájtot, azaz máris leestünk 2 millió vonalra. Most azt mondjátok, hogy "dehát az még mindig baromi sok..."; hát egy játékban valóban elég, viszont egy CAD programban abszolút nem. Tegyük azért hozzá, hogy ilyenkor már a framerate-ben sincs bocsánat, szóval nem nagy bűn egyszerűen lekorlátozni a buffer méretet és az extra vonalakat nem rajzolni (mellesleg simán előfordulhat, hogy pont az ilyen gigantikus méretek miatt hal meg a geometry shader, amit én első kézből tapasztaltam nem egyszer). Harmadik fontos dolog: a kódról egyértelműen látszik, hogy szuboptimális: minden második invokáció aktív csak. Höfö átírni a példakódot vulkánra (a refaktorálásnak köszönhetően ez nem okozhat különösebb gondot) és subgroup műveletekkel kioptimalizálni (de az még jobb, ha azok nélkül is meg tudod csinálni, de akkor küldd el nekem is). Amit még szeretnék megemlíteni, az a vonalak vágása a közeli vágósíkhoz (ami egyébként geometry shader-ben is szükséges). Az történik ugyanis, hogy ha a vonal egyik pontja a kamera mögött van, akkor a reverse projection miatt (pongyolán mondva) trapézzá alakul (nem tartja meg a konstans vastagságot), illetve ha be van kapcsolva a backface culling, akkor legalább egy háromszög eltűnik. A megoldás erre az, hogy még view space-ben meg kell határozni a közeli vágósíkkal való metszetet: CODE
uniform vec4 nearPlane; // view space-ben
bool ClipToNearPlane(inout vec4 vpos1, inout vec4 vpos2)
{
// sík egyenlete: (a, b, c, d) · (x, y, z, 1) = 0
// (parametrikus) vonal egyenlet: vpos1 + t * (vpos2 - vpos1)
// utóbbi behelyettesítendő az elsőbe, meghatározandó t
vec3 dir = vpos2.xyz - vpos1.xyz;
float t = -(nearPlane.w + dot(nearPlane.xyz, vpos1.xyz)) / dot(nearPlane.xyz, dir);
// hol vannak a pontok a síkhoz képest
float dist1 = dot(vpos1, nearPlane);
float dist2 = dot(vpos2, nearPlane);
bool clipped = false;
if (dist1 < 0) {
// vpos1 a sík mögött van
vpos1.xyz = vpos1.xyz + t * dir;
clipped = true;
} else if (dist2 < 0) {
// vpos2 a sík mögött van
vpos2.xyz = vpos1.xyz + t * dir;
clipped = true;
}
return clipped;
}
Az algoritmus tehát ráhelyezi a pontot a vágósíkra, amennyiben úgy látja, hogy mögötte van. Első kérdés, hogy mi történik akkor, ha mindkét pontra igaz ez? A válasz az, hogy nem érdekes, mert úgysem fog rajzolni semmit. Második kérdés, hogy hogyan kell kiszámolni a közeli vágósíkot view space-ben. Ez szerencsére könnyű, mivel tudjuk róla, hogy kovariáns vektor: nearPlane = Mproj * { 0, 0, 1, 1 } (vigyázat, ez OpenGL-t feltételez!) Az eredményt pedig úgy kell felhasználni, hogy az eredeti pontok helyett ezekkel végzed el a számolást. Azért volt szükségem erre, mert az irradiance map számolóm furcsa eredményt adott egy bizonyos cubemap-re. Megkérdeztem egy külső fejlesztőt is, aki szintén az Unreal 4 algoritmusát használja, és megerősítette, hogy az ő implementációja is ugyanazt a rossz eredményt produkálja.
CODE
#pragma comment(lib, "dxgi.lib")
#include <DXGIType.h>
#include <dxgi1_2.h>
#include <dxgi1_3.h>
#include <DXProgrammableCapture.h>
CODE
IDXGraphicsAnalysis* analysis = nullptr;
DXGIGetDebugInterface1(0, __uuidof(analysis), (void**)&analysis);
analysis->BeginCapture();
{
// a kód, amit debugolni szeretnél
}
analysis->EndCapture();
analysis->Release();
A programot ilyenkor a Debug -> Graphics -> Start Graphics Debugging menüponttal kell indítani (Alt+F5). A Visual Studio ilyenkor egy külön tabot hoz fel, ahol a PIX-hez hasonlóan meg lehet nézni, hogy mi történt. Vigyázat, ha nem így indítod a programot, akkor nullptr-t fogsz visszakapni, szóval normál esetben #ifdef-eld ki az egészet! Az XAudio2 cikkem mostantól elavult, ugyanis sem az Ogg-Vorbis-t, se a korábban írt osztályaimat nem használom. A multithreading-es dolgokhoz a C++ Standard Library-t használom, a hang betöltéséhez pedig a WMF-et (így tudok .mp3-at betölteni, ami lényegesen leegyszerűsíti az életet).
CODE
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfreadwrite.lib")
#pragma comment(lib, "mfuuid.lib")
// számít a sorrend!
#include <Windows.h>
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mferror.h>
IMFSourceReader* reader; // ez olvassa a hangfájlt
IMFSample* sample; // egy beolvasott minta
WAVEFORMATEX format; // ez írja le a hangformátumot (ahogy eddig is)
CODE
MFCreateSourceReaderFromURL(file.c_str(), nullptr, &reader);
// egy fájlban több hang vagy videó stream is lehet
DWORD stream = (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM;
reader->SetStreamSelection(MF_SOURCE_READER_ALL_STREAMS, FALSE);
reader->SetStreamSelection(stream, TRUE);
// ellenőrizni kell, hogy a média típusa megfelelő-e
IMFMediaType* mediatype = nullptr;
GUID subtype = {};
reader->GetNativeMediaType(stream, 0, &mediatype);
mediatype->GetGUID(MF_MT_SUBTYPE, &subtype);
if (subtype != MFAudioFormat_Float && subtype != MFAudioFormat_PCM) {
// jelzem az olvasónak, hogy dekódolást kérek
IMFMediaType* decodertype = nullptr;
MFCreateMediaType(&decodertype);
decodertype->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
decodertype->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM);
reader->SetCurrentMediaType(stream, nullptr, decodertype);
decodertype->Release();
}
mediatype->Release();
A WMF belül tudni fogja, hogy melyik dekódert kell használnia. Az XAudio2 felé elő kell állítani egy megfelelő WAVEFORMATEX struktúrát, ez a következőképpen tehető meg: CODE
UINT32 waveformatsize = 0;
WAVEFORMATEX* waveformat = nullptr;
reader->GetCurrentMediaType(stream, &mediatype);
MFCreateWaveFormatExFromMFMediaType(mediatype, &waveformat, &waveformatsize);
memcpy_s(&format, sizeof(WAVEFORMATEX), waveformat, waveformatsize);
CoTaskMemFree(waveformat);
mediatype->Release();
Ezen a ponton már létre lehet hozni a megfelelő XAudio2 objektumokat. A következő kérdés, hogy hogyan lehet megszerezni az IMFSourceReader-től az adatot (zanzásítva): CODE
DWORD streamflags = 0;
DWORD samplelength = 0;
reader->ReadSample(stream, 0, nullptr, &streamflags, nullptr, &sample);
if (streamflags & MF_SOURCE_READERF_ENDOFSTREAM) {
endofstream = true;
sample = nullptr;
}
IMFMediaBuffer* mediabuffer = nullptr;
BYTE* audiodata = nullptr;
if (sample != nullptr) {
sample->ConvertToContiguousBuffer(&mediabuffer);
mediabuffer->Lock(&audiodata, nullptr, &samplelength);
{
// hozzáadás a bufferhez, ami majd lemegy az Xaudio2-nek
}
mediabuffer->Unlock();
mediabuffer->Release();
}
Amennyiben betelt a buffer, mehet le az XAudio2SourceVoice::SubmitSourceBuffer() metódussal, és lehet kezdeni feltölteni a következőt. Most jön a kérdés, hogy "jó, de mi köze ennek a gépéúhoz?". Az egyik maga a megvalósítás, az ugyanis echte megegyezik a vulkános frame queueing implementációmmal. A másik pedig höfö: ennek segítségével videót lejátszani vulkánban egy kocka valamelyik oldalán. A refaktorálás egészen pontosan nyolc hónapig tartott, de a nehézség csak a vulkános példaprogramok átalakításakor jelentkezett (többek között a fent említett dolgok miatt). A Metal példakódok macOS-en futnak, mivel sejtésem szerint nincs mindenkinek iKütyüje (meg szerettem volna látni a különbségeket, amikből egyébként alig van). Ahogy említettem írtam egy forward+ renderer-t, szóval lehet mélyebben tanulmányozni a Metal lelki világát is.
Irodalomjegyzék https://vulkan.lunarg.com/doc/.../16-vulkan_1_1_changes.html - Vulkan 1.1 Changes (LunarG, 2017) https://www.khronos.org/blog/vulkan-subgroup-tutorial - Vulkan Subgroup Tutorial (Khronos, 2018) https://www.khronos.org/assets/uploads/.../06-subgroups.pdf - Vulkan Subgroup Explained (nVidia, 2018) |