From 53ea87fc29a43845a63af35aac2d7daf85833092 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 12:57:14 +0300 Subject: [PATCH 01/34] bench: record CPU frame time excluding present --- include/public/Core/Application.hpp | 1 + src/Core/Application.cpp | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/include/public/Core/Application.hpp b/include/public/Core/Application.hpp index b847a51f..61a3dd4f 100644 --- a/include/public/Core/Application.hpp +++ b/include/public/Core/Application.hpp @@ -221,6 +221,7 @@ class ENGINE_API Application { Benchmark* m_benchmark = nullptr; GraphicsConfig m_graphicsConfig; float DeltaTime; + float m_cpuFrameMs = 0.0f; Timer FrameTimer; diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index c7e7dc73..27162d97 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -145,6 +146,8 @@ namespace Sleak { m_benchmark = new Benchmark(); m_benchmark->Initialize(renderer); renderer->RegisterBenchmarkMetrics(*m_benchmark); + m_benchmark->RegisterMetric("CPU_frame_ms", + [this]() { return m_cpuFrameMs; }); { auto& cfg = m_DebugOverlay->GetConfig(); cfg.ShowCameraPanel = false; @@ -252,6 +255,7 @@ namespace Sleak { Debug::ShaderHotReload::Poll(); renderer->BeginRender(); + const auto cpuFrameStart = std::chrono::steady_clock::now(); // Update active scene if present if (Game && Game->GetActiveScene()) { @@ -285,6 +289,10 @@ namespace Sleak { if (queue && context) queue->ExecuteCommands(context); + m_cpuFrameMs = std::chrono::duration( + std::chrono::steady_clock::now() - + cpuFrameStart) + .count(); renderer->EndRender(); if (captureArmed && !renderer->IsFrameCapturePending()) From 86fec83f62d9fd7386ebb8d3b014ca281c989672 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:08:15 +0300 Subject: [PATCH 02/34] shaders: linearize SSAO sample depth instead of two mat4 transforms --- assets/shaders/ssao.frag | 18 +++++++++++------- assets/shaders/ssao.frag.spv | Bin 8420 -> 8492 bytes 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/assets/shaders/ssao.frag b/assets/shaders/ssao.frag index b3e8d035..4cb478ce 100644 --- a/assets/shaders/ssao.frag +++ b/assets/shaders/ssao.frag @@ -43,6 +43,12 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { return world.xyz / world.w; } +// View-space Z for a depth value, solved from the projection's z and w rows. +float ViewZFromDepth(float depth) { + return (Projection[3][2] - depth * Projection[3][3]) / + (depth * Projection[2][3] - Projection[2][2]); +} + void main() { // Skip sky pixels — sky should read 1.0 (no occlusion). float depth = texture(gDepth, fragUV).r; @@ -92,24 +98,22 @@ void main() { if (sampleUV.x < 0.0 || sampleUV.x > 1.0 || sampleUV.y < 0.0 || sampleUV.y > 1.0) continue; - // Reconstruct the world-space position at this screen location and - // transform into view space for depth comparison. + // View-space depth at this screen location for the comparison. float sampleDepth = texture(gDepth, sampleUV).r; if (sampleDepth >= 0.9999) continue; // sky — no occluder - vec3 sampleWorld = ReconstructWorldPos(sampleUV, sampleDepth); - vec3 sampleViewPos = (View * vec4(sampleWorld, 1.0)).xyz; + float sampleViewZ = ViewZFromDepth(sampleDepth); // Range check — sample contributes less when the depth difference // between the frag and the occluder is much larger than the radius. // (avoids haloing around silhouettes) float rangeCheck = smoothstep(0.0, 1.0, - Radius / max(abs(viewPos.z - sampleViewPos.z), 0.0001)); + Radius / max(abs(viewPos.z - sampleViewZ), 0.0001)); // In view space the camera looks down -Z — "closer to the camera" // means a LESS negative Z (i.e. larger value). The occluder blocks - // the sample when sampleViewPos.z >= samplePos.z + bias. - occlusion += (sampleViewPos.z >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck; + // the sample when sampleViewZ >= samplePos.z + bias. + occlusion += (sampleViewZ >= samplePos.z + Bias ? 1.0 : 0.0) * rangeCheck; } float ao = 1.0 - (occlusion / float(kSize)) * Intensity; diff --git a/assets/shaders/ssao.frag.spv b/assets/shaders/ssao.frag.spv index b9ed2607944ab8c975651d95f031435d0ea18e52..90a81ddfd806fe13b709bd9a84be671c33a18bcd 100644 GIT binary patch literal 8492 zcmZ9R34oPV6~{lA8Fmqr%}^L5Srk!F5hR!a!2v~PWDv{Bah!RKqr<#u7D!tJ&9cp; zY+tk;D@)rnD*>(4RxB$mGb<}ATg|n!e!uU12d?wQ@%+#KEce`V@Auw=s)o)So@GO_ z(b+-S#vxh$jL(L`WN^cB-LS5C-Tc1(_W38Cbi4s$v#LDKn1i$7Sv6YO(%RXr<7G%Y zGLATT764UIHOuZGsX7~#HLPoCXl!g)x4NaRr>CW_|B|j!OIK%isinQMx760(xwT~6 zh$ODFyRB=WJq@TKB3-v88%b|jwxQHk?(XaF9cb%6r`+4s-c;_Jvvu>rrJENl&1&dl z3^4;+GjKJ#rq@zW|ApupIz0rvsk5~0+|zo?TUP0qV_F_N{spmB*(B`l_O{$N3%t$6 z&^7$;XmoS)ipHka-qtOB8I!DV(0P6JF#czpb8PA@U*x{aOfY!owYGb!?daZL!|9(< z>g_Id9f)-f&27D3%P}8 z&uJdo$8*|%K5$Oe*(U6c{Cp~Y@7(6z){e7r)Ub-@P#)-SY~#}OEbJ{wD~2IhI!q5E6AJ4)UCdEPGU3p#@_rYgG;ZsW@JxqmmfoNUy! z_rhIVIfgv`F6>UkKK7yeTDSCcl`2Cp{vo*X=FNSj{z}Z_`(vDFg`3Jnt>je(Z>^*D zbNO2XQ=QF%J220hY%VtY?kJslVX5unO8=|iTFY71K>S?ldmqOk4eZT#!n?l)pV-HH zyoUdV?OioK^1LAWwU6o}790NaVS8x}NsnJUR2WBhf=u!R<*n-%I26kPva! zmb*LQ+`rtugmX=~`x4IgQSSbP^L>^ z&Om&^J`(Kgw7nquQ(M1}e&5Y$xmB@!H&fg9DXp)a=KGH5r=7f>cK8p4m7l@AsVZV7 zft+V8GMV+AqqcFi*v?Q+(RO|jW4yC7-g+~*3C>NmPi3WzIE-^YXBR^ixvXviUA2I%+lF>e`pwh!zRT&~jJBrtDc1GA`@|gbF9K}O`AW30=108u z-zVa)1^by|{dc3Ck!xswuE5rbzHbKm=--1j*7-)gJHW1~|4y{^D)+ZKyRWb(pd!dn>G|%yEx((W4mV9Yq5Qg!fwL$U0cMiT-!VD;|!jLaj(w89Df&B zOWWCYB(^m!Ol;#XN^Ik|46?TkvM(QGUo*(QZjikzv7P;miS7Ek6MGMM?;!iG#J0bE ziS1{~Ly7JF{GN{eTi@^K)b{&1wf$~RZNHmS+wW!VWz5re;~d2ILRa8+C2tBM%;MsBfndl7lXhY{!Z`fB#CzYj6S&;O>PmwvGO3%R0*9e^`l|Cl0nE7+Q= z5&ibRd10k)oJt>5`vj+VF1M)W(8orOId{Vv2i>iqnEcn{+Iu0_l-=DmpgmBeVj z4>3bTn<1{<$G_WKcV>{q}0^)pyL z=5{?;F6@tjok`dq8|43Z;lGx>`g!yT#2ESL`;%bn=!^b71->597yW%29R2CHzw6NQ zvHlI-$99SHW^=?kxqE=H3ctFFuj`HLzTodt1So8=re$2iv!O+E?)3 z0Nay2xMuL*1iPN~trzdu?Oc_5iKv?jlLQ2owyVIEyQ=?4#as*Mt>WT_nfr9 zgV_IEMBjA8T6@v@#CM^mAkXlLa5viTEcKJr{4V-?h>!m7qxGv}%^!fR5%v$k*1w59 zThKp3jFI>IQTram+SU@seYh9At+1~|{}^!wQTr!g*IL1CxextQ#2G9{^cgn+{WC;g zyz@UVIC&pu;u-FwNAL6fh-(?=eCyG_K&<@$VqNd&FA@28=N<%`E5Dpuc< z-_Gd|$is*+_WBT7&R$2N|A@#fLShDw7JSV5Pen}3`p?*MijVzO`lpyVv zmlb#OzhJpo`#G>&y7u$f^6A?D!**@w<=WcD#o7)ndW*G(I7sAH_S6C#3YNPWyo-M1 zs<7q#eO!a~@%@*tM&x||#j(~f@ZhzE!^z)PtTh5G=RNi_De9=7;B(KlM}l4Z9$k5lXdnBMAC1V_mpJ-52pltt8H|OKj~N^cmU9O76?N3n*Eq0i+pF=e5q*sZ z>$k5%(LVMie+VLHU*hO%0(fv=6XE1zUX#FbF|Vkjj=m;?UE5xbcMbbE3>|$KGX;^e z53&37`$1dY*=)o<9J~pwJsf=mwtdF$vT4}H$jAFO9W3wTJ@oH|axw2(uw48dYzA2F zJmT$jCfLWB={pjUvo~?tt9;a*4Ysap&O&Q1Dc;pN;6tb>um32tob#`TKN>s_oqk6g z11BG6G!N`|S{)K=9%~*F_I$9l{oUs0#&KX{oTysJ=$y3TMlQh@mW^~Hda1j>cP9oi8WVX z%Ngrl^~t68!nC-uCa*EXyjz# zMxe(OcqIBD#F-w9_TS1Tq9-AKo`gNCuT)(OTcm=@tf`xIP2?wo~qh$2Zn+DZ1ow3cn3Vg@rdVsD0&5_U7zd99F~EXBXvmh zva;}peJZv!B6k(o8AfipkJa${<2+9Tn=c>d*#LG8eX+(GaJt4?>{!FIO4m3YTYs!^ l2H1T0=y4r5%}@7tCO-Yq&w8*mD$Y1gu{G}E%OvOU*Q`~B{_-*A0LKF|OAf6MvKch9+ZW}1f17?C7H zlF`ZT$>~Fqbc|1i!X$9b>D;!mW96LwTKAlzk2z9@u}M?vr_UbAh-4VEvaV1nYiLDQ zAmi|pX8}+XS(9W3LBo>WlD3tdZL3$etz6RC)z{bAU%RBY*x6eu7dyL4)nZqzw7#g{ z$edrP+|@hKop%_HM}FPm$tY?|k~PJyO1ZyQ9q6i^Q>pfLw^#aStzSED{@S_oli}1c z2A_fT33wR0PLl3oU+n^BZF@48ySq}`bnpffq_*M7Oy-V`g{#{O)xx^|gznTi=;Z8D zaf5R+|LJFM+N+fd-Aje81aA+Pm)GmAc`%0^T~Vx-i@m#i-CIXjwOA~7lrE(okz+Xg zs!FN9*wIz!P5F8q&G2gq-KBwCZT5aqsQ_bq-ESngy|STLO}To#qrl6{wPLxyRJ$a{ z?*^{d6*bgzjE3**D|CAxBCmawo15e8uiQLl`#X!-Vee01-qrhI$;s$Fi|Y?Vdn@|d zYN6+>v-NLI&s1fgwz`Xh(4Tfyet5D9zw|lrpn+-b)y#c9r%XSiK|d2cjUm|FXQB73 zs#Mn%de>A2dOQwko^zSk`>;rDO{GkCP1p9JR}1Ct%DS_QT^V;VbJ^!}W>J{NUBz50 zlzWQhS{ipP`uU|`^l3`2gFAE4s?@#(T*+_*FF5k}yuh5&5y$55`qn$no zbG5+JpPu5W7ZkfLs_*k;xI!gK+Q@qlb$D*aA#L1IPb51+X)CibIa|_aqeI4{v79; zau4P>@2%WJInH}4_ehTO&dNQOc&p0 z>#f?=C#U+x1>rvheF(A-cAtm2T`%khf?JUNV7)`y<)(sWG8+@3J@)Kcd9l{K?tTu2 zvtQd9)}wwX5+&-obiv!0VO zCpqg;_uM(OFJLy-J!)5PMdV!HwU!{&=PzF{kt&yhjXRCmnzS3I?)j9{emb)`eg3ho z=hG4U?yRfnbK=vT`Whep=?5U^e;L?!fcf9ZY|mUn{e2lWPt<)q*rEMKW_^8TvHne9 z>(hQSv-$G&yU=4lcV~9bO2pp_cK^oj$n5H2-w7U)>E?YHagC_&aWEr2x5gcU^Q3JL z93Sy>(XBc3Md;q6(3hiow~nPN=Jkv_tob<@du1<;@#h6|srzjF28V7Azxko-?>9em z{r%?Wb-(R-z0{!lZO_}UYS8_LhuvrA_d9gg-;&dB1aEE7{eFkP_4y4C-FM1;IoQ+TRALpgJ5UyR7zga1K8ycyX2%QoYg>lMc{ar6^v?SJ ze36qXuigCC*p9R!`guOoW9>Cy*Vg7)sq4+@ed<6Tix{I#AJ3b7^ur&bd>_PG=YU(0 z=*O$T`pB>5lzUHKjmRqwT1>@Bat-^}{u)G|Nk}^#l6x)U{z9(Ed|wBrzxFYi@9V+l zw13*I+jB3!kT~DX=OMEY-(A)|gSmj5m+7u~KH`4kId)|n(;(NKajqAB#f)=L?K}a` z#ahI8{k6M~&D5f;2XSqEqDL2Gyf%mV&F5acv)W@X7lIr2auJ-oHMzz*M9vXwl)>_f zBi4wR3Y;}x%KsV*#8>hbF)wC1Bkxz z@vPT_&F8b$ZhuP5@^MBl0biWy-iJ$(R>c1K{dp5&|1Lw~9KSi^uf)fg%Ms(`<7{sP zn_FA#brX1FCQO5SjkEn0IDPVG`wH|p*UoXa-wM`Wd+hg0aO_vR``y4SAANfpST6Ld z!S*Edw>Q||k=d_fufC7oiRdFAb-xR29&J(IyTPudE$X`l9QA3pzN?t!WBvDl^Yv^- z*Jli!_Bp>7F~?#=U+w0(mRVcmc|X{=&_4jyC-e`3UB~n6zCMH);|T7Op9Wi#HMnN* zp8>nx4T$;Tj(rwvF5~pEpVu+Vi?=Xek9a3;X8s)Fowy0HpA(orkI4I+)W3jO{~?IB zDTukYGHVmx!aNaqo;Sj+%zm4kpCRY%%y%FT?O$Zp?i_1=32ctgzYI42ZPdAx`74M% z@_rxHZ$r#&E^(ZPJHh8>dN1==5ql81zXo=#g`AeVnZJ(MgS!xI`i*102hkRH{u>!5 z@31F6!_Cy_dESn=mVWkk2lF=(bKi@Y*Yo);L_Y4^x53*H`Au1_?||KhHv7AcS-}dQ# z#I^OY*84KfT1SB&K;-5j(Sx64yt+Alis%!)elX+Y9ntGJ|38B>N66J#-+m5ftoB&{ z7hrQwL}HH*f%SD*vp#YW^GmQXp7n_N71$VSF-9M`h~;{2FYGy);H2xrq4< zIBJXDJO(H4H_7wzTjVk1ab#1*{SGWI7rCAQKZ>|d*HYIn?7s)co?@*(z{y{poy|Xj zm@9j>vic#j(~u!HsME3r_y7Y^{HT1#~%kU|o^NIqLc^*tM-yf7gh*{s-1> zT`w^^tV{kyM9#XzQP<1h#=1P@^3ks$=yK7o$m1M!4aHtx8&|MvSjRA98P=gs6C!6F zV)uCmc6E7sGar3KitG9a^pWV+89$eeLf1z=?%Qr)d57oF-y7wk-=o2D@jKWUu-p>- zt#vHeVXfMBN93$coUc{hywL+lseMj#F9?NWuHhuiv zR^I31yM7YbcYP;fPT%$Wz{#tdXENBh=)=BXIY)3)GEO?U{W329uHPTdc>Pm5{+p>~ z9P?B-*VZTA)dzs(6^B#2@BF=6yL;IIKMm|&?nR8$pdp8ekeg2&(dUq69ep6QK z(O~1`yLcL!C8O2|BnajD<3{5fVU76Yo3TMr?30hCYQe} z-f!b4Fwe(!3-d;5RzC?{ZYS7yZF0un3ij^Z&K&EXg3UD|-vacl=zZjOZ>{KZ@%$Eo zjggODo(i^?iwfc7>xyaEbM&g_-1M@Gv$J*koL>;HW zS%-fcj2tV#@^Lm-g87&1iE*9lVN`z+a0l| zdocU=#qrF0BEH8$pPuQ~Y0O@TIsDlo>fAfyqhI@=%S}R}U)C&d&mz};U~^4DBG>+4 z`N%aDY##Z@bpY5r+9Iw6?0(|s)M;SX)VBrE-(I;tZPwsB^)SS~ABvdEck1DYyt?nY zIf!xb?mGgkkGwhUkzDv636_iZqVEPd#lfSZ>%01J6j=XxNbKimuzdWSdkk1EB!134 O7S8 Date: Mon, 2 Nov 2026 13:10:01 +0300 Subject: [PATCH 03/34] container: hash HashTable keys by value --- .../public/Utility/Container/HashTable.hpp | 169 ++++++++++++------ 1 file changed, 116 insertions(+), 53 deletions(-) diff --git a/include/public/Utility/Container/HashTable.hpp b/include/public/Utility/Container/HashTable.hpp index 9f54953d..dfa19249 100644 --- a/include/public/Utility/Container/HashTable.hpp +++ b/include/public/Utility/Container/HashTable.hpp @@ -1,8 +1,10 @@ #ifndef _HASH_TABLE_H_ #define _HASH_TABLE_H_ -#include // For uint64_t -#include // For std::move (can be removed if unwanted) +#include +#include +#include +#include namespace Sleak { @@ -23,7 +25,7 @@ namespace Sleak * * Implementation Details: * - Uses a hash function to map keys to indices in an internal array. - * - Handles collisions using chaining or open addressing (specify which one you used). + * - Handles collisions with linear-probing open addressing and tombstones. * - Provides methods for inserting, deleting, and retrieving key-value pairs. * @ingroup utility */ @@ -45,28 +47,53 @@ namespace Sleak Entry* table; size_t capacity; size_t size; + size_t tombstones; float loadFactor; static constexpr size_t DEFAULT_CAPACITY = 16; static constexpr float DEFAULT_LOAD_FACTOR = 0.7f; + static constexpr float MAX_LOAD_FACTOR = 1.0f; + static constexpr size_t MIN_CAPACITY = 1; + static constexpr size_t GROWTH_FACTOR = 2; - /// Maps a key to a starting slot index via linear probing. + /// Home slot of key in a table with tableCapacity slots. + static size_t slotFor(const Key& key, size_t tableCapacity) + { + return std::hash{}(key) % tableCapacity; + } + + /// Home slot of key in the current table. size_t hash(const Key& key) const { - return reinterpret_cast(&key) % capacity; + return slotFor(key, capacity); } - /// Doubles capacity and rehashes every live entry into the new table. - void resize() + /// Slot holding a live entry for key, or capacity if there is none. + size_t findSlot(const Key& key) const + { + size_t index = hash(key); + for (size_t probes = 0; probes < capacity && table[index].occupied; + ++probes) + { + if (!table[index].deleted && table[index].key == key) + { + return index; + } + index = (index + 1) % capacity; + } + return capacity; + } + + /// Moves live entries into a newCapacity table, dropping tombstones. + void rehash(size_t newCapacity) { - size_t newCapacity = capacity * 2; Entry* newTable = new Entry[newCapacity]; for (size_t i = 0; i < capacity; ++i) { if (table[i].occupied && !table[i].deleted) { - size_t index = hash(table[i].key) % newCapacity; + size_t index = slotFor(table[i].key, newCapacity); while (newTable[index].occupied) { index = (index + 1) % newCapacity; @@ -78,15 +105,59 @@ namespace Sleak delete[] table; table = newTable; capacity = newCapacity; + tombstones = 0; + } + + /// Grows if live entries fill the table, else purges tombstones only. + void resize() + { + const bool mostlyLive = + size * GROWTH_FACTOR >= capacity * loadFactor; + rehash(mostlyLive ? capacity * GROWTH_FACTOR : capacity); } public: HashTable(size_t initCapacity = DEFAULT_CAPACITY, float loadFactor = DEFAULT_LOAD_FACTOR) - : capacity(initCapacity), size(0), loadFactor(loadFactor) + : capacity(initCapacity > MIN_CAPACITY ? initCapacity + : MIN_CAPACITY), + size(0), + tombstones(0), + loadFactor(loadFactor > 0.0f && loadFactor <= MAX_LOAD_FACTOR + ? loadFactor + : DEFAULT_LOAD_FACTOR) { table = new Entry[capacity]; } + /// Deep-copies other's slots, including its capacity and load factor. + HashTable(const HashTable& other) + : table(new Entry[other.capacity]), + capacity(other.capacity), + size(other.size), + tombstones(other.tombstones), + loadFactor(other.loadFactor) + { + for (size_t i = 0; i < capacity; ++i) + { + table[i] = other.table[i]; + } + } + + /// Replaces this table with a deep copy of other. + HashTable& operator=(const HashTable& other) + { + if (this != &other) + { + HashTable copy(other); + std::swap(table, copy.table); + std::swap(capacity, copy.capacity); + std::swap(size, copy.size); + std::swap(tombstones, copy.tombstones); + std::swap(loadFactor, copy.loadFactor); + } + return *this; + } + ~HashTable() { delete[] table; @@ -95,85 +166,77 @@ namespace Sleak /// Inserts or updates the value for key, resizing first if over the load factor. void insert(const Key& key, const Value& value) { - if (size >= capacity * loadFactor) + const size_t existing = findSlot(key); + if (existing != capacity) { - resize(); + table[existing].value = value; + return; } - size_t index = hash(key) % capacity; - while (table[index].occupied && !table[index].deleted && table[index].key != key) + if (size + tombstones >= capacity * loadFactor) { - index = (index + 1) % capacity; + resize(); } - if (!table[index].occupied || table[index].deleted) + size_t index = hash(key); + while (table[index].occupied && !table[index].deleted) { - table[index].key = key; - table[index].value = value; - table[index].occupied = true; - table[index].deleted = false; - ++size; + index = (index + 1) % capacity; } - else + + if (table[index].deleted) { - table[index].value = value; + --tombstones; } + table[index].key = key; + table[index].value = value; + table[index].occupied = true; + table[index].deleted = false; + ++size; } /// Tombstones the entry for key; returns false if it wasn't present. bool remove(const Key& key) { - size_t index = hash(key) % capacity; - while (table[index].occupied) + const size_t index = findSlot(key); + if (index == capacity) { - if (!table[index].deleted && table[index].key == key) - { - table[index].deleted = true; - --size; - return true; - } - index = (index + 1) % capacity; + return false; } - return false; + table[index].key = Key(); + table[index].value = Value(); + table[index].deleted = true; + --size; + ++tombstones; + return true; } /// Looks up key and writes its value into outValue; returns false if not found. bool get(const Key& key, Value& outValue) const { - size_t index = hash(key) % capacity; - while (table[index].occupied) + const size_t index = findSlot(key); + if (index == capacity) { - if (!table[index].deleted && table[index].key == key) - { - outValue = table[index].value; - return true; - } - index = (index + 1) % capacity; + return false; } - return false; + outValue = table[index].value; + return true; } /// True if key currently has a live entry. bool contains(const Key& key) const { - size_t index = hash(key) % capacity; - while (table[index].occupied) - { - if (!table[index].deleted && table[index].key == key) - { - return true; - } - index = (index + 1) % capacity; - } - return false; + return findSlot(key) != capacity; } /// Drops every entry and reallocates the table at its current capacity. void clear() { + Entry* emptyTable = new Entry[capacity]; delete[] table; - table = new Entry[capacity]; + table = emptyTable; size = 0; + tombstones = 0; } size_t getSize() const { return size; } From ce27bd70ece7eb2b8f332344c770c064f69b23c1 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:11:01 +0300 Subject: [PATCH 04/34] render: allocate queued commands from per-frame arenas --- .../Graphics/Common/RenderCommandQueue.hpp | 68 ++++++-- .../Graphics/Common/RenderCommands.hpp | 2 +- src/Graphics/Common/RenderCommandQueue.cpp | 145 +++++++++++------- src/Graphics/Common/RenderCommands.cpp | 12 +- 4 files changed, 153 insertions(+), 74 deletions(-) diff --git a/include/private/Graphics/Common/RenderCommandQueue.hpp b/include/private/Graphics/Common/RenderCommandQueue.hpp index 0ce63ed9..9d7b8b14 100644 --- a/include/private/Graphics/Common/RenderCommandQueue.hpp +++ b/include/private/Graphics/Common/RenderCommandQueue.hpp @@ -3,8 +3,12 @@ #include #include "RenderCommands.hpp" -#include #include +#include +#include +#include +#include +#include namespace Sleak { namespace RenderEngine { @@ -61,7 +65,7 @@ namespace Sleak { void ExecuteShadowPass(RenderContext* context); - bool HasCachedShadowDraws() const { return cachedShadowDraws.GetSize() > 0; } + bool HasCachedShadowDraws() const { return !cachedShadowDraws.empty(); } /// Drops queued commands for the current frame, keeping the cached shadow draw list. void Clear(); @@ -82,18 +86,64 @@ namespace Sleak { /// A draw command paired with the transform/bone buffers it needs to replay in the shadow pass. struct ShadowDrawEntry { - RefPtr command; - RefPtr transformBuffer; // last-bound slot-0 buffer - RefPtr boneBuffer; // slot-3 bone UBO for skinned draws + RenderCommandBase* command = nullptr; + const RefPtr* transformBuffer = nullptr; // last-bound slot-0 buffer + const RefPtr* boneBuffer = nullptr; // slot-3 bone UBO for skinned draws }; private: + /// Bump allocator owning one frame's commands until the slot is recycled. + class CommandArena { + public: + CommandArena() = default; + CommandArena(const CommandArena&) = delete; + CommandArena& operator=(const CommandArena&) = delete; + ~CommandArena() { Reset(); } + + /// Constructs a command in arena memory and appends it to the frame's list. + template + T* Create(Args&&... args) { + void* memory = Allocate(sizeof(T), alignof(T)); + T* command = new (memory) T(std::forward(args)...); + m_commands.push_back(command); + return command; + } + + /// Destroys every command and rewinds the blocks for reuse. + void Reset(); + + const std::vector& Commands() const { return m_commands; } + + private: + struct Block { + std::unique_ptr data; + size_t size = 0; + }; + + /// Returns aligned storage from the current block, adding blocks as needed. + void* Allocate(size_t size, size_t alignment); + + static constexpr size_t BLOCK_SIZE = 64 * 1024; + std::vector m_blocks; + size_t m_blockIndex = 0; + size_t m_offset = 0; + std::vector m_commands; + }; + + /// Moves submission to the next arena, destroying the commands it last held. + void AdvanceArena(); + + CommandArena& CurrentArena() { return m_arenas[m_currentArena]; } + + // A frame's commands stay alive while its shadow replay and GPU frames in flight may use them static constexpr int RETIRE_FRAMES = 3; + static constexpr int ARENA_COUNT = RETIRE_FRAMES + 2; static RenderCommandQueue* Instance; - Queue> commands; - List cachedShadowDraws; - List m_retiredShadowDraws[RETIRE_FRAMES]; - int m_retireIndex = 0; + CommandArena m_arenas[ARENA_COUNT]; + int m_currentArena = 0; + std::vector cachedShadowDraws; + std::vector m_pendingState; + std::vector m_forwardCmds; }; } } diff --git a/include/private/Graphics/Common/RenderCommands.hpp b/include/private/Graphics/Common/RenderCommands.hpp index 5e4539ef..33661c07 100644 --- a/include/private/Graphics/Common/RenderCommands.hpp +++ b/include/private/Graphics/Common/RenderCommands.hpp @@ -148,7 +148,7 @@ namespace Sleak { RENDER_COMMAND(BindConstantBuffer) - RefPtr GetBuffer() const { return constantBuffer; } + const RefPtr& GetBuffer() const { return constantBuffer; } int GetSlot() const { return slot; } private: diff --git a/src/Graphics/Common/RenderCommandQueue.cpp b/src/Graphics/Common/RenderCommandQueue.cpp index b5946090..1f51bdb5 100644 --- a/src/Graphics/Common/RenderCommandQueue.cpp +++ b/src/Graphics/Common/RenderCommandQueue.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Sleak { namespace RenderEngine { @@ -15,8 +16,9 @@ namespace Sleak { List> constantBuffers, uint32_t vertexCount, uint32_t startVertexLocation) { - auto command = RefPtr(new DrawCommand(vertexBuffer,constantBuffers,vertexCount, startVertexLocation)); - commands.push(std::move(command)); + CurrentArena().Create( + std::move(vertexBuffer), std::move(constantBuffers), + vertexCount, startVertexLocation); } void RenderCommandQueue::SubmitDrawIndexed( @@ -24,43 +26,35 @@ namespace Sleak { List> constantBuffers, uint32_t indexCount, uint32_t startIndexLocation, int32_t baseVertexLocation, bool castsShadow) { - auto command = RefPtr(new DrawIndexedCommand( - vertexBuffer, indexBuffer, constantBuffers, indexCount, - startIndexLocation, baseVertexLocation)); + auto* command = CurrentArena().Create( + std::move(vertexBuffer), std::move(indexBuffer), + std::move(constantBuffers), indexCount, startIndexLocation, + baseVertexLocation); command->SetCastsShadow(castsShadow); - commands.push(std::move(command)); } void RenderCommandQueue::SubmitBindConstantBuffer(RefPtr buffer, uint8_t slot) { - auto command = RefPtr(new BindConstantBufferCommand(buffer, slot)); - commands.push(std::move(command)); + CurrentArena().Create(std::move(buffer), slot); } void RenderCommandQueue::SubmitUpdateConstantBuffer(RefPtr buffer, void* Data, uint16_t Size) { - auto command = RefPtr(new UpdateConstantBufferCommand(buffer, Data, Size)); - commands.push(std::move(command)); + CurrentArena().Create(std::move(buffer), Data, Size); } void RenderCommandQueue::SubmitBindMaterial(::Sleak::Material* material) { - auto command = RefPtr(new BindMaterialCommand(material)); - commands.push(std::move(command)); + CurrentArena().Create(material); } void RenderCommandQueue::SubmitSetRenderMode(RenderMode mode) { - auto command = RefPtr(new SetRenderModeCommand(mode)); - commands.push(command); + CurrentArena().Create(mode); } void RenderCommandQueue::SubmitSetRenderFace(RenderFace face) { - auto command = RefPtr(new SetRenderFaceCommand(face)); - commands.push(command); + CurrentArena().Create(face); } void RenderCommandQueue::SubmitCustomCommand(CustomCommand::ExecuteFunction function) { - auto command = - RefPtr(new CustomCommand(function)); - commands.push(std::move(command)); - + CurrentArena().Create(std::move(function)); } void RenderCommandQueue::ExecuteCommands(RenderContext* context) { @@ -68,27 +62,26 @@ namespace Sleak { SortCommands(); OptimizeBatching(); + const auto& commands = CurrentArena().Commands(); + // Cache draw commands for shadow pass replay next frame, // pairing each with the last-bound slot-0 (transform) buffer. - m_retiredShadowDraws[m_retireIndex].clear(); - m_retiredShadowDraws[m_retireIndex] = std::move(cachedShadowDraws); - m_retireIndex = (m_retireIndex + 1) % RETIRE_FRAMES; cachedShadowDraws.clear(); { - RefPtr lastSlot0Buffer; - RefPtr lastSlot3Buffer; - for (auto& cmd : commands) { + const RefPtr* lastSlot0Buffer = nullptr; + const RefPtr* lastSlot3Buffer = nullptr; + for (RenderCommandBase* cmd : commands) { auto type = cmd->GetType(); if (type == CommandType::BindConstantBuffer) { - auto* bindCmd = static_cast(cmd.get()); + auto* bindCmd = static_cast(cmd); if (bindCmd->GetSlot() == 0) { - lastSlot0Buffer = bindCmd->GetBuffer(); + lastSlot0Buffer = &bindCmd->GetBuffer(); } else if (bindCmd->GetSlot() == 3) { - lastSlot3Buffer = bindCmd->GetBuffer(); + lastSlot3Buffer = &bindCmd->GetBuffer(); } } if ((type == CommandType::Draw || type == CommandType::DrawIndexed) - && lastSlot0Buffer && cmd->CastsShadow()) { + && lastSlot0Buffer && *lastSlot0Buffer && cmd->CastsShadow()) { ShadowDrawEntry entry; entry.command = cmd; entry.transformBuffer = lastSlot0Buffer; @@ -96,7 +89,7 @@ namespace Sleak { // casters don't accidentally inherit a stale bone UBO. if (cmd->IsSkinned()) entry.boneBuffer = lastSlot3Buffer; - cachedShadowDraws.add(entry); + cachedShadowDraws.push_back(entry); } } } @@ -112,8 +105,10 @@ namespace Sleak { if (context->IsDeferredEnabled()) { // Per-group accumulator: state commands preceding the current draw. - List> pendingState; - List> forwardCmds; + auto& pendingState = m_pendingState; + auto& forwardCmds = m_forwardCmds; + pendingState.clear(); + forwardCmds.clear(); ::Sleak::Material* lastMaterial = nullptr; bool lastMaterialForward = false; @@ -125,31 +120,31 @@ namespace Sleak { || t == CommandType::SetFace; }; - for (auto& cmd : commands) { + for (RenderCommandBase* cmd : commands) { auto type = cmd->GetType(); if (type == CommandType::BindMaterial) { - auto* bmc = static_cast(cmd.get()); + auto* bmc = static_cast(cmd); lastMaterial = bmc->GetMaterial(); lastMaterialForward = lastMaterial && lastMaterial->IsForwardRendered(); // Always add material bind to pending state - pendingState.add(cmd); + pendingState.push_back(cmd); continue; } if (type == CommandType::CustomCommand) { // Skybox, debug lines — always rendered after the lighting pass. // Flush any pending state to forward group first so state is correct. - for (auto& s : pendingState) forwardCmds.add(s); + forwardCmds.insert(forwardCmds.end(), pendingState.begin(), pendingState.end()); pendingState.clear(); - forwardCmds.add(cmd); + forwardCmds.push_back(cmd); lastMaterial = nullptr; lastMaterialForward = false; continue; } if (isStateCmd(type)) { - pendingState.add(cmd); + pendingState.push_back(cmd); continue; } @@ -160,8 +155,8 @@ namespace Sleak { const bool isSkinned = cmd->IsSkinned(); if (lastMaterialForward || isSkinned) { // Transparent/forward/skinned draw — defer to forward pass - for (auto& s : pendingState) forwardCmds.add(s); - forwardCmds.add(cmd); + forwardCmds.insert(forwardCmds.end(), pendingState.begin(), pendingState.end()); + forwardCmds.push_back(cmd); } else { // Opaque draw — execute now in geometry pass for (auto& s : pendingState) s->Execute(context); @@ -179,32 +174,31 @@ namespace Sleak { context->ExecuteDeferredLightingPass(); context->BeginForwardTransparentPass(); - for (auto& cmd : forwardCmds) + for (RenderCommandBase* cmd : forwardCmds) cmd->Execute(context); context->EndForwardTransparentPass(); - commands.clear(); + AdvanceArena(); return; } // ---- Forward rendering path (unchanged behavior) ---- - for (auto& cmd : commands) + for (RenderCommandBase* cmd : commands) cmd->Execute(context); - commands.clear(); + AdvanceArena(); } void RenderCommandQueue::ExecuteShadowPass(RenderContext* context) { - for (size_t i = 0; i < cachedShadowDraws.GetSize(); ++i) { - auto& entry = cachedShadowDraws[i]; + for (const ShadowDrawEntry& entry : cachedShadowDraws) { // Bind the transform buffer (slot 0) before drawing — shadow mode // in VulkanRenderer::BindConstantBuffer computes LightVP * World - if (entry.transformBuffer) { - context->BindConstantBuffer(entry.transformBuffer, 0); + if (entry.transformBuffer && *entry.transformBuffer) { + context->BindConstantBuffer(*entry.transformBuffer, 0); } // Skinned casters need their bone matrices bound so the shadow // depth vert can transform verts into the posed skeleton space. - if (entry.boneBuffer) { - context->BindBoneBuffer(entry.boneBuffer); + if (entry.boneBuffer && *entry.boneBuffer) { + context->BindBoneBuffer(*entry.boneBuffer); } entry.command->ExecuteShadow(context); } @@ -224,17 +218,52 @@ namespace Sleak { } void RenderCommandQueue::Clear() { - while (!commands.isEmpty()) - commands.pop(); + CurrentArena().Reset(); } void RenderCommandQueue::ClearAll() { - while (!commands.isEmpty()) - commands.pop(); cachedShadowDraws.clear(); - for (int i = 0; i < RETIRE_FRAMES; ++i) - m_retiredShadowDraws[i].clear(); - m_retireIndex = 0; + m_pendingState.clear(); + m_forwardCmds.clear(); + for (CommandArena& arena : m_arenas) + arena.Reset(); + m_currentArena = 0; + } + + void RenderCommandQueue::AdvanceArena() { + m_pendingState.clear(); + m_forwardCmds.clear(); + m_currentArena = (m_currentArena + 1) % ARENA_COUNT; + CurrentArena().Reset(); + } + + void* RenderCommandQueue::CommandArena::Allocate(size_t size, + size_t alignment) { + for (;;) { + if (m_blockIndex < m_blocks.size()) { + Block& block = m_blocks[m_blockIndex]; + const size_t start = + (m_offset + alignment - 1) & ~(alignment - 1); + if (start + size <= block.size) { + m_offset = start + size; + return block.data.get() + start; + } + ++m_blockIndex; + m_offset = 0; + continue; + } + const size_t blockSize = std::max(BLOCK_SIZE, size + alignment); + m_blocks.push_back( + {std::make_unique(blockSize), blockSize}); + } + } + + void RenderCommandQueue::CommandArena::Reset() { + for (RenderCommandBase* command : m_commands) + command->~RenderCommandBase(); + m_commands.clear(); + m_blockIndex = 0; + m_offset = 0; } void RenderCommandQueue::Shutdown() { diff --git a/src/Graphics/Common/RenderCommands.cpp b/src/Graphics/Common/RenderCommands.cpp index 5e97575b..1ba2b040 100644 --- a/src/Graphics/Common/RenderCommands.cpp +++ b/src/Graphics/Common/RenderCommands.cpp @@ -13,8 +13,8 @@ namespace RenderEngine { DrawCommand::DrawCommand(RefPtr buffer, List> constantBuffers, uint32_t vertexCount, uint32_t vertexLocation) - : m_vertexBuffer(buffer), - m_constantBuffers(constantBuffers), + : m_vertexBuffer(std::move(buffer)), + m_constantBuffers(std::move(constantBuffers)), m_vertexCount(vertexCount), m_startVertexLocation(vertexLocation) {} @@ -51,9 +51,9 @@ DrawIndexedCommand::DrawIndexedCommand(RefPtr vertexBuffer, uint32_t indexCount, uint32_t startIndexLocation, int32_t baseVertexLocation) - : m_vertexBuffer(vertexBuffer), - m_indexBuffer(indexBuffer), - m_constantBuffers(constantBuffers), + : m_vertexBuffer(std::move(vertexBuffer)), + m_indexBuffer(std::move(indexBuffer)), + m_constantBuffers(std::move(constantBuffers)), m_indexCount(indexCount), m_startIndexLocation(startIndexLocation), m_baseVertexLocation(baseVertexLocation) {} @@ -113,7 +113,7 @@ bool DrawIndexedCommand::IsSkinned() const { UpdateConstantBufferCommand::UpdateConstantBufferCommand(RefPtr buffer, void* Data, uint16_t Size) : - constantBuffer(buffer), + constantBuffer(std::move(buffer)), Size(Size) { if (Size <= INLINE_CAPACITY) { // Fast path: no heap allocation for small buffers From fe33e445b984393035b6116e2b5f82c638f68029 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:00 +0300 Subject: [PATCH 05/34] physics: query broadphase only from movable bodies --- src/Physics/PhysicsWorld.cpp | 92 +++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/src/Physics/PhysicsWorld.cpp b/src/Physics/PhysicsWorld.cpp index d4d6783e..43e447fa 100644 --- a/src/Physics/PhysicsWorld.cpp +++ b/src/Physics/PhysicsWorld.cpp @@ -10,6 +10,31 @@ namespace Sleak { namespace Physics { +namespace { + +/// Rigidbody that collision response can push, or nullptr if none. +RigidbodyComponent* GetMovableBody(ColliderComponent* collider) { + GameObject* owner = collider->GetOwner(); + if (!owner) return nullptr; + auto* rb = owner->GetComponent(); + if (!rb || rb->GetBodyType() == BodyType::Static) return nullptr; + return rb; +} + +/// World position and scale the narrowphase uses for a collider. +void GetColliderPose(ColliderComponent* collider, Vector3D& pos, + Vector3D& scale) { + GameObject* owner = collider->GetOwner(); + if (auto* transform = owner->GetComponent()) { + pos = transform->GetWorldPosition() + collider->GetOffset(); + scale = transform->GetWorldScale(); + } else if (auto* cam = dynamic_cast(owner)) { + pos = cam->GetPosition() + collider->GetOffset(); + } +} + +} // namespace + void PhysicsWorld::RegisterCollider(ColliderComponent* collider) { if (!collider) return; @@ -109,63 +134,56 @@ void PhysicsWorld::FindPairsAndResolve() { ColliderComponent* colliderA = m_colliders[i]; if (colliderA->GetProxyId() < 0) continue; + // Passive colliders never drive queries; their pairs have no response + RigidbodyComponent* rbA = GetMovableBody(colliderA); + if (!rbA) continue; + AABB worldA = colliderA->GetWorldAABB(); m_tree.Query(worldA, [&](int proxyId) -> bool { - auto* colliderB = static_cast(m_tree.GetUserData(proxyId)); + auto* colliderB = static_cast( + m_tree.GetUserData(proxyId)); if (colliderB == colliderA) return true; - // Skip duplicate pairs: only process when A < B (pointer order) - if (colliderA > colliderB) return true; + // Movable pairs are visited from both sides: keep pointer order + RigidbodyComponent* rbB = GetMovableBody(colliderB); + if (rbB && colliderA > colliderB) return true; // Layer filtering if ((colliderA->GetLayer() & colliderB->GetMask()) == 0) return true; if ((colliderB->GetLayer() & colliderA->GetMask()) == 0) return true; - // Get transform data - auto* ownerA = colliderA->GetOwner(); - auto* ownerB = colliderB->GetOwner(); - if (!ownerA || !ownerB) return true; + // Skip if both are triggers + if (colliderA->IsTrigger() && colliderB->IsTrigger()) return true; - Vector3D posA, posB; - Vector3D scaleA(1, 1, 1), scaleB(1, 1, 1); + if (!colliderA->GetOwner() || !colliderB->GetOwner()) return true; - auto* transformA = ownerA->GetComponent(); - if (transformA) { - posA = transformA->GetWorldPosition() + colliderA->GetOffset(); - scaleA = transformA->GetWorldScale(); - } else if (auto* camA = dynamic_cast(ownerA)) { - posA = camA->GetPosition() + colliderA->GetOffset(); - } + // Narrowphase argument order stays pointer-ordered + const bool aFirst = colliderA < colliderB; + ColliderComponent* first = aFirst ? colliderA : colliderB; + ColliderComponent* second = aFirst ? colliderB : colliderA; + RigidbodyComponent* rbFirst = aFirst ? rbA : rbB; + RigidbodyComponent* rbSecond = aFirst ? rbB : rbA; - auto* transformB = ownerB->GetComponent(); - if (transformB) { - posB = transformB->GetWorldPosition() + colliderB->GetOffset(); - scaleB = transformB->GetWorldScale(); - } else if (auto* camB = dynamic_cast(ownerB)) { - posB = camB->GetPosition() + colliderB->GetOffset(); - } + Vector3D posFirst, posSecond; + Vector3D scaleFirst(1, 1, 1), scaleSecond(1, 1, 1); + GetColliderPose(first, posFirst, scaleFirst); + GetColliderPose(second, posSecond, scaleSecond); CollisionManifold manifold = TestCollision( - colliderA->GetShape(), posA, scaleA, - colliderB->GetShape(), posB, scaleB); + first->GetShape(), posFirst, scaleFirst, + second->GetShape(), posSecond, scaleSecond); if (!manifold.hasCollision) return true; - // Skip if both are triggers - if (colliderA->IsTrigger() && colliderB->IsTrigger()) return true; - // Resolve: push rigidbodies apart - auto* rbA = ownerA->GetComponent(); - auto* rbB = ownerB->GetComponent(); - - if (rbA && rbA->GetBodyType() != BodyType::Static) { - rbA->ResolveCollision(manifold.contact.normal * -1.0f, - manifold.contact.penetration); + if (rbFirst) { + rbFirst->ResolveCollision(manifold.contact.normal * -1.0f, + manifold.contact.penetration); } - if (rbB && rbB->GetBodyType() != BodyType::Static) { - rbB->ResolveCollision(manifold.contact.normal, - manifold.contact.penetration); + if (rbSecond) { + rbSecond->ResolveCollision(manifold.contact.normal, + manifold.contact.penetration); } return true; From 6553ecf6ab3f0ce8cceb93aba21dad27f3a27f15 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:21 +0300 Subject: [PATCH 06/34] d3d11: release the replaced depth-stencil state --- include/private/Graphics/DirectX11/DirectX11Renderer.hpp | 2 +- src/Graphics/DirectX11/DirectX11Renderer.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index be8d4612..25111800 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -141,7 +141,7 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { ID3D11RenderTargetView* renderTargetView; // Render target view ID3D11InputLayout* layout; - ID3D11DepthStencilState* depthStencilState; + ID3D11DepthStencilState* depthStencilState = nullptr; ID3D11DepthStencilView* depthStencilView; ID3D11Texture2D* depthStencilBuffer; diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 3d96421b..1a9b2ab9 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -395,12 +395,15 @@ void DirectX11Renderer::SetDepthStencilState(bool enableDepth, depthStencilDesc.BackFace.StencilPassOp = D3D11_STENCIL_OP_KEEP; depthStencilDesc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS; - HRESULT hr = device->CreateDepthStencilState(&depthStencilDesc, &depthStencilState); + ID3D11DepthStencilState* state = nullptr; + HRESULT hr = device->CreateDepthStencilState(&depthStencilDesc, &state); if (FAILED(hr)) { SLEAK_ERROR("Failed to create depth stencil state!"); return; } + if (depthStencilState) depthStencilState->Release(); + depthStencilState = state; deviceContext->OMSetDepthStencilState(depthStencilState, 1); } From 0e32ae8dd0dda4a9d28b632fe829c00bf086f10b Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:22 +0300 Subject: [PATCH 07/34] vulkan: reuse a material's PBR set when it is rebound in the same frame --- .../Graphics/Vulkan/VulkanRenderer.hpp | 11 +++ src/Graphics/Vulkan/VulkanDescriptors.cpp | 79 ++++++++++++------- src/Graphics/Vulkan/VulkanRenderer.cpp | 1 + 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 95b56fae..73b5c206 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include "../Common/RenderContext.hpp" @@ -735,6 +736,16 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { m_pbrMaterialChunks; VkDeviceSize m_pbrMaterialUBOStride = 0; uint32_t m_pbrMaterialSlot[MAX_FRAMES_IN_FLIGHT] = {}; + static constexpr uint32_t PBR_TEXTURE_COUNT = 6; + /// A material's written set contents, reused when it is bound again this frame. + struct PBRMaterialBinding { + PBRMaterialParams params{}; + std::array images{}; + VkDescriptorSet set = VK_NULL_HANDLE; + }; + std::array, + MAX_FRAMES_IN_FLIGHT> + m_pbrMaterialBindings; bool m_pbrMaterialResourcesCreated = false; VkPipelineLayout m_gbufferGeomLayout = VK_NULL_HANDLE; diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index 67199c0d..b493e6a3 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -2,6 +2,7 @@ #include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" #include +#include #include #include #include @@ -660,17 +661,6 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { vkCmdBindPipeline(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferPipeline); m_activeCustomFormat = 0; - // Claim this material's own ring slot (own set + own UBO region) so the - // set is never rewritten while already bound by a prior draw this frame. - const uint32_t slot = m_pbrMaterialSlot[currentFrame]; - auto& chunks = m_pbrMaterialChunks[currentFrame]; - const uint32_t chunkIdx = slot / PBR_SETS_PER_FRAME; - if (chunkIdx >= chunks.size() && !AddPBRMaterialChunk(currentFrame)) return; - PBRMaterialChunk& chunk = chunks[chunkIdx]; - const uint32_t local = slot % PBR_SETS_PER_FRAME; - const VkDescriptorSet set = chunk.sets[local]; - const VkDeviceSize uboOffset = local * m_pbrMaterialUBOStride; - // Build PBRMaterialParams from Material properties PBRMaterialParams params{}; auto dc = material->GetDiffuseColor(); @@ -699,10 +689,6 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { params.hasAOMap = material->HasAOTexture() ? 1u : 0u; params.hasEmissiveMap = material->HasEmissiveTexture() ? 1u : 0u; - if (chunk.mapped) - memcpy(static_cast(chunk.mapped) + uboOffset, ¶ms, - sizeof(params)); - // Resolve textures — fall back to the default white 1×1 texture if absent auto resolveView = [&](Texture* tex) -> VkImageView { auto* vt = tex ? static_cast(tex) : nullptr; @@ -715,7 +701,7 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { return vt ? vt->GetSampler() : VK_NULL_HANDLE; }; - Texture* textures[6] = { + Texture* textures[PBR_TEXTURE_COUNT] = { material->GetDiffuseTexture(), material->HasNormalTexture() ? material->GetNormalTexture() : nullptr, material->HasMetallicTexture() ? material->GetMetallicTexture() : nullptr, @@ -724,20 +710,54 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { material->HasEmissiveTexture() ? material->GetEmissiveTexture() : nullptr, }; - std::array imageInfos{}; - for (uint32_t i = 0; i < 6; ++i) { + std::array imageInfos{}; + for (uint32_t i = 0; i < PBR_TEXTURE_COUNT; ++i) { imageInfos[i].imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; imageInfos[i].imageView = resolveView(textures[i]); imageInfos[i].sampler = resolveSampler(textures[i]); } + // Same material with unchanged contents this frame: rebind its set + auto& bindings = m_pbrMaterialBindings[currentFrame]; + auto cached = bindings.find(material); + if (cached != bindings.end() && + memcmp(&cached->second.params, ¶ms, sizeof(params)) == 0 && + std::equal(imageInfos.begin(), imageInfos.end(), + cached->second.images.begin(), + [](const VkDescriptorImageInfo& a, + const VkDescriptorImageInfo& b) { + return a.sampler == b.sampler && + a.imageView == b.imageView && + a.imageLayout == b.imageLayout; + })) { + vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, + m_gbufferGeomLayout, 0, 1, + &cached->second.set, 0, nullptr); + return; + } + + // Claim this material's own ring slot (own set + own UBO region) so the + // set is never rewritten while already bound by a prior draw this frame. + const uint32_t slot = m_pbrMaterialSlot[currentFrame]; + auto& chunks = m_pbrMaterialChunks[currentFrame]; + const uint32_t chunkIdx = slot / PBR_SETS_PER_FRAME; + if (chunkIdx >= chunks.size() && !AddPBRMaterialChunk(currentFrame)) return; + PBRMaterialChunk& chunk = chunks[chunkIdx]; + const uint32_t local = slot % PBR_SETS_PER_FRAME; + const VkDescriptorSet set = chunk.sets[local]; + const VkDeviceSize uboOffset = local * m_pbrMaterialUBOStride; + + if (chunk.mapped) + memcpy(static_cast(chunk.mapped) + uboOffset, ¶ms, + sizeof(params)); + VkDescriptorBufferInfo bufInfo{}; bufInfo.buffer = chunk.ubo; bufInfo.offset = uboOffset; bufInfo.range = sizeof(PBRMaterialParams); - std::array writes{}; - for (uint32_t i = 0; i < 6; ++i) { + std::array writes{}; + for (uint32_t i = 0; i < PBR_TEXTURE_COUNT; ++i) { writes[i].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[i].dstSet = set; writes[i].dstBinding = i; @@ -746,19 +766,22 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { writes[i].descriptorCount = 1; writes[i].pImageInfo = &imageInfos[i]; } - writes[6].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; - writes[6].dstSet = set; - writes[6].dstBinding = 6; - writes[6].dstArrayElement = 0; - writes[6].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; - writes[6].descriptorCount = 1; - writes[6].pBufferInfo = &bufInfo; + writes[PBR_TEXTURE_COUNT].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[PBR_TEXTURE_COUNT].dstSet = set; + writes[PBR_TEXTURE_COUNT].dstBinding = PBR_TEXTURE_COUNT; + writes[PBR_TEXTURE_COUNT].dstArrayElement = 0; + writes[PBR_TEXTURE_COUNT].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[PBR_TEXTURE_COUNT].descriptorCount = 1; + writes[PBR_TEXTURE_COUNT].pBufferInfo = &bufInfo; - vkUpdateDescriptorSets(device, 7, writes.data(), 0, nullptr); + vkUpdateDescriptorSets(device, static_cast(writes.size()), + writes.data(), 0, nullptr); vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, m_gbufferGeomLayout, 0, 1, &set, 0, nullptr); + bindings[material] = {params, imageInfos, set}; + // Advance the ring for the next material this frame. m_pbrMaterialSlot[currentFrame] = slot + 1; } diff --git a/src/Graphics/Vulkan/VulkanRenderer.cpp b/src/Graphics/Vulkan/VulkanRenderer.cpp index 177a1db2..9a3562f3 100644 --- a/src/Graphics/Vulkan/VulkanRenderer.cpp +++ b/src/Graphics/Vulkan/VulkanRenderer.cpp @@ -291,6 +291,7 @@ void VulkanRenderer::BeginRender() { bFrameStarted = true; m_pbrMaterialSlot[currentFrame] = 0; // reset PBR material ring for this frame + m_pbrMaterialBindings[currentFrame].clear(); m_boneSlot[currentFrame] = 1; // slot 0 is the identity palette BeginGpuFrame(); From ffe9a47225b070c3e001cca4a83560f6533330d9 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:26 +0300 Subject: [PATCH 08/34] container: make Queue pop amortized O(1) --- include/public/Utility/Container/Queue.hpp | 92 +++++++++++++++++----- 1 file changed, 73 insertions(+), 19 deletions(-) diff --git a/include/public/Utility/Container/Queue.hpp b/include/public/Utility/Container/Queue.hpp index f0847813..962bedd0 100644 --- a/include/public/Utility/Container/Queue.hpp +++ b/include/public/Utility/Container/Queue.hpp @@ -2,7 +2,9 @@ #define _QUEUE_H_ #include "List.hpp" +#include #include +#include namespace Sleak { @@ -23,7 +25,7 @@ namespace Sleak * - Simulating real-world queues, such as waiting lines. * * Implementation Details: - * - Uses an underlying List to store the queue elements. + * - Uses an underlying List plus a head index; pop is amortized O(1). * - Provides methods for enqueueing (adding) and dequeueing (removing) elements. * - Maintains the FIFO order of elements. * @ingroup utility @@ -32,21 +34,43 @@ namespace Sleak class Queue { public: Queue() = default; + Queue(const Queue&) = default; + Queue& operator=(const Queue&) = default; + + /// Takes other's elements, leaving it empty. + Queue(Queue&& other) noexcept + : data(std::move(other.data)), head(std::exchange(other.head, 0)) {} + + /// Replaces this queue with other's elements, leaving other empty. + Queue& operator=(Queue&& other) noexcept { + if (this != &other) { + data = std::move(other.data); + head = std::exchange(other.head, 0); + } + return *this; + } /// Adds value to the back of the queue. void push(const T& value) { data.add(value); } + /// Moves value onto the back of the queue. + void push(T&& value) { + data.add(std::move(value)); + } + /// Removes and returns the front element; throws if empty. T pop() { if (isEmpty()) { throw Sleak::EmptyContainerException(); } - T front = std::move(data[0]); - - data.erase(0); + T* slots = data.GetData(); + T front = std::move(slots[head]); + slots[head] = T(); + ++head; + compactIfSparse(); return front; } @@ -56,42 +80,72 @@ namespace Sleak if (isEmpty()) { throw Sleak::EmptyContainerException(); } - return data[0]; + return data.GetData()[head]; } const T& front() const { if (isEmpty()) { throw Sleak::EmptyContainerException(); } - return data[0]; + return data.GetData()[head]; } - /// Reverses the queue's element order in place. - const void reverse() { - data.reverse(); - } + /// Reverses the queue's element order in place. + const void reverse() { + std::reverse(begin(), end()); + } bool isEmpty() const { - return data.GetSize() == 0; + return size() == 0; } - size_t size() const { return data.GetSize(); } + size_t size() const { return data.GetSize() - head; } void clear() { data.clear(); + head = 0; } - /// Copies out the underlying List, front-to-back. - List GetData() { return data; } + /// Copies out the queued elements, front-to-back. + List GetData() { + List out; + out.resize(size()); + for (const T& value : *this) { + out.add(value); + } + return out; + } - // Iterators - T* begin() { return data.begin(); } - const T* begin() const { return data.begin(); } - T* end() { return data.end(); } - const T* end() const { return data.end(); } + // Iterators + T* begin() { return data.begin() + head; } + const T* begin() const { return data.begin() + head; } + T* end() { return data.end(); } + const T* end() const { return data.end(); } private: + /// Slides live elements to the front once popped slots are the majority. + void compactIfSparse() { + const size_t live = size(); + if (head < live) { + return; + } + if (live == 0) { + clear(); + return; + } + + T* slots = data.GetData(); + for (size_t i = 0; i < live; ++i) { + slots[i] = std::move(slots[head + i]); + } + while (data.GetSize() > live) { + data.erase(data.GetSize() - 1); + } + head = 0; + } + List data; + size_t head = 0; }; } From cc7ad296383e188f5cf4f28ca02272a3fc178bc7 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:41 +0300 Subject: [PATCH 09/34] physics: enlarge dynamic proxies along velocity --- include/public/Physics/PhysicsWorld.hpp | 3 ++- src/Physics/PhysicsWorld.cpp | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/include/public/Physics/PhysicsWorld.hpp b/include/public/Physics/PhysicsWorld.hpp index 4a1e3d3d..a6150299 100644 --- a/include/public/Physics/PhysicsWorld.hpp +++ b/include/public/Physics/PhysicsWorld.hpp @@ -108,7 +108,8 @@ namespace Sleak { float maxDist, uint32_t layerMask = 0xFFFFFFFF) const; private: - void UpdateBroadphase(); + /// Refits every proxy, enlarging dynamic ones along their velocity. + void UpdateBroadphase(float dt); void FindPairsAndResolve(); DynamicAABBTree m_tree; diff --git a/src/Physics/PhysicsWorld.cpp b/src/Physics/PhysicsWorld.cpp index 43e447fa..96552521 100644 --- a/src/Physics/PhysicsWorld.cpp +++ b/src/Physics/PhysicsWorld.cpp @@ -6,6 +6,7 @@ #include #include #include +#include namespace Sleak { namespace Physics { @@ -33,6 +34,20 @@ void GetColliderPose(ColliderComponent* collider, Vector3D& pos, } } +/// Distance a dynamic body travels next step, zero for anything else. +Vector3D PredictDisplacement(ColliderComponent* collider, float dt) { + Vector3D none(0, 0, 0); + RigidbodyComponent* rb = GetMovableBody(collider); + if (!rb || rb->GetBodyType() != BodyType::Dynamic) return none; + + Vector3D d = rb->GetVelocity() * dt; + if (!std::isfinite(d.GetX()) || !std::isfinite(d.GetY()) || + !std::isfinite(d.GetZ())) { + return none; + } + return d; +} + } // namespace void PhysicsWorld::RegisterCollider(ColliderComponent* collider) { @@ -115,17 +130,17 @@ void PhysicsWorld::Step(float dt) { } } - UpdateBroadphase(); + UpdateBroadphase(dt); FindPairsAndResolve(); } -void PhysicsWorld::UpdateBroadphase() { +void PhysicsWorld::UpdateBroadphase(float dt) { for (auto* collider : m_colliders) { int proxyId = collider->GetProxyId(); if (proxyId < 0) continue; AABB newAABB = collider->GetWorldAABB(); - m_tree.MoveProxy(proxyId, newAABB, Vector3D(0, 0, 0)); + m_tree.MoveProxy(proxyId, newAABB, PredictDisplacement(collider, dt)); } } From 6716d8e08da7a05092f525bbb601d7318e1e19f2 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:12:53 +0300 Subject: [PATCH 10/34] transform: cache world matrix by value behind a dirty flag --- .../ECS/Components/TransformComponent.hpp | 14 ++++-- src/Scene/TransformComponent.cpp | 49 ++++++++----------- 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/include/public/ECS/Components/TransformComponent.hpp b/include/public/ECS/Components/TransformComponent.hpp index e4939684..c051dfd0 100644 --- a/include/public/ECS/Components/TransformComponent.hpp +++ b/include/public/ECS/Components/TransformComponent.hpp @@ -38,7 +38,7 @@ namespace Sleak { /// Refreshes and rebinds the transform constant buffer for this frame. void Update(float DeltaTime) override; - // Transformation matrix calculation + /// World matrix (S*R*T), recomputed only after a transform change. Matrix4 GetTransformMatrix(); // Transformations @@ -91,18 +91,22 @@ namespace Sleak { }; protected: + /// Flags the cached world matrix stale; call after writing fields directly. + void MarkWorldDirty() { m_worldDirty = true; } + Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); Quaternion rotation = Quaternion(); Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); - TransformMatrix* Transform = nullptr; private: RefPtr ConstantBuffer; + Math::Matrix4 m_world = Math::Matrix4::Identity(); + bool m_worldDirty = true; - /// Rebuilds the transform matrix and pushes it to the GPU constant buffer. + /// Pushes the world matrix and main camera matrices to the GPU constant buffer. void UpdateConstantBuffer(); - /// Recomputes the cached Translation/Rotation/Scaling matrices from position/rotation/scale. - void UpdateTransform(); + /// Returns the cached world matrix, rebuilding it first if dirty. + const Math::Matrix4& WorldMatrix(); /// Combines the transform with the main camera's view and projection matrices. Math::Matrix4 GetMVP(); }; diff --git a/src/Scene/TransformComponent.cpp b/src/Scene/TransformComponent.cpp index a0be4276..7c92fb56 100644 --- a/src/Scene/TransformComponent.cpp +++ b/src/Scene/TransformComponent.cpp @@ -26,18 +26,12 @@ namespace Sleak { scale(scale) {} - TransformComponent::~TransformComponent() { - delete Transform; - Transform = nullptr; - } + TransformComponent::~TransformComponent() = default; bool TransformComponent::Initialize() { - if (!Transform) Transform = new TransformMatrix(); - UpdateTransform(); if (ConstantBuffer.IsValid()) return true; - auto world = GetTransformMatrix(); - RenderEngine::TransformBuffer tb(world, + RenderEngine::TransformBuffer tb(WorldMatrix(), Camera::GetMainViewMatrix(), Camera::GetMainProjectionMatrix()); ConstantBuffer = RefPtr( @@ -54,16 +48,23 @@ namespace Sleak { RenderEngine::RenderCommandQueue::GetInstance()->SubmitBindConstantBuffer(ConstantBuffer, 0); } - // Transformation matrix calculation Matrix4 TransformComponent::GetTransformMatrix() { - return Transform->CalculateTransformMatrix(); + return WorldMatrix(); + } + + const Matrix4& TransformComponent::WorldMatrix() { + if (m_worldDirty) { + m_world = (Matrix4::Scale(scale) * Matrix4::Rotate(rotation)) * + Matrix4::Translate(position); + m_worldDirty = false; + } + return m_world; } // Transformations void TransformComponent::Translate(const Vector3D& translation) { position += translation; - Transform->Translation = Matrix4::Translate(position); - UpdateTransform(); + m_worldDirty = true; } @@ -73,7 +74,7 @@ namespace Sleak { void TransformComponent::Rotate(const Quaternion& rotation) { this->rotation *= rotation; - UpdateTransform(); + m_worldDirty = true; } void TransformComponent::Scale(float amount) { @@ -82,20 +83,23 @@ namespace Sleak { void TransformComponent::Scale(const Vector3D& scale) { this->scale = scale * this->scale; - UpdateTransform(); + m_worldDirty = true; } // Setters void TransformComponent::SetPosition(const Vector3D& position) { this->position = position; + m_worldDirty = true; } void TransformComponent::SetRotation(const Quaternion& rotation) { this->rotation = rotation; + m_worldDirty = true; } void TransformComponent::SetScale(const Vector3D& scale) { this->scale = scale; + m_worldDirty = true; } // Direction vectors @@ -124,6 +128,7 @@ namespace Sleak { Vector3D direction = (target - position).Normalize(); Vector3D Up = VECTOR_Up; rotation = Quaternion::LookRotation(direction, Up); + m_worldDirty = true; } // Getters @@ -139,25 +144,13 @@ namespace Sleak { return scale; } - void TransformComponent::UpdateTransform() { - Transform->Translation = Matrix4::Translate(position); - Transform->Rotation = Matrix4::Rotate(rotation); - Transform->Scaling = Matrix4::Scale(scale); - - Transform->CalculateTransformMatrix(); - } - - void TransformComponent::UpdateConstantBuffer() { if (!ConstantBuffer) { SLEAK_ERROR("Constant buffer is not found to update for object {}", owner->GetName()); return; } - UpdateTransform(); - - auto world = GetTransformMatrix(); - RenderEngine::TransformBuffer tb(world, + RenderEngine::TransformBuffer tb(WorldMatrix(), Camera::GetMainViewMatrix(), Camera::GetMainProjectionMatrix()); @@ -166,7 +159,7 @@ namespace Sleak { Math::Matrix4 TransformComponent::GetMVP() { - auto mat = GetTransformMatrix() * + auto mat = WorldMatrix() * Camera::GetMainViewMatrix() * Camera::GetMainProjectionMatrix(); From b5e3c8017bdb051974e177259f12b01c71901065 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:13:03 +0300 Subject: [PATCH 11/34] gameobject: add transform before mesh in plane and cylinder factories --- src/Scene/GameObject.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Scene/GameObject.cpp b/src/Scene/GameObject.cpp index 227b4822..17394171 100644 --- a/src/Scene/GameObject.cpp +++ b/src/Scene/GameObject.cpp @@ -167,9 +167,9 @@ namespace Sleak { MeshData meshData = GetPlaneMesh(width, height); object->AddComponent(meshData, Physics::ColliderType::AABB); object->AddComponent(BodyType::Static); + object->AddComponent(position); object->AddComponent(CreateDefaultMaterial()); object->AddComponent(std::move(meshData)); - object->AddComponent(position); object->Initialize(); return object; @@ -223,9 +223,9 @@ namespace Sleak { MeshData meshData = GetCylinderMesh(segments, height, radius); object->AddComponent(meshData, Physics::ColliderType::AABB); object->AddComponent(BodyType::Static); + object->AddComponent(position); object->AddComponent(CreateDefaultMaterial()); object->AddComponent(std::move(meshData)); - object->AddComponent(position); object->Initialize(); return object; From 591006e64d4d1875e3975fc893aa0e5d256eb50b Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:13:04 +0300 Subject: [PATCH 12/34] build: enable link-time optimization for Release engine builds --- CMakeLists.txt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index c74dc440..33d61c2d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,13 @@ set_target_properties(Engine PROPERTIES ) target_compile_features(Engine PUBLIC cxx_std_23) +# --- Release link-time optimization --- +include(CheckIPOSupported) +check_ipo_supported(RESULT ENGINE_IPO_SUPPORTED LANGUAGES CXX) +if(ENGINE_IPO_SUPPORTED) + set_property(TARGET Engine PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) +endif() + # --- Generated version header --- set(ENGINE_GENERATED_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/include) configure_file( From 5d2160fcc56f7a932e53f8ece561182a2de4c90c Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:13:04 +0300 Subject: [PATCH 13/34] build: drop math errno in Release engine builds --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 33d61c2d..8e3979e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -305,6 +305,8 @@ check_ipo_supported(RESULT ENGINE_IPO_SUPPORTED LANGUAGES CXX) if(ENGINE_IPO_SUPPORTED) set_property(TARGET Engine PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE ON) endif() +target_compile_options(Engine PRIVATE + $<$,$>:-fno-math-errno>) # --- Generated version header --- set(ENGINE_GENERATED_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated/include) From b2d9b2fbd7dce09e5d8ed27da18aaacbd2c8ece2 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:13:59 +0300 Subject: [PATCH 14/34] d3d11: cache rasterizer and depth-stencil states --- .../Graphics/DirectX11/DirectX11Renderer.hpp | 18 +++ src/Graphics/DirectX11/DirectX11Renderer.cpp | 107 ++++++++++++++---- 2 files changed, 100 insertions(+), 25 deletions(-) diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index 25111800..26a8384e 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -10,12 +10,15 @@ #include "Graphics/Common/ResourceManager.hpp" #include #include +#include #include #include #include #include #include #include +#include +#include namespace Sleak { namespace RenderEngine { @@ -112,6 +115,13 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool CreateRenderTargetView(); /// Creates and binds the rasterizer state matching current fill/cull settings. bool SetRasterState(); + /// Binds the depth state matching the tracked depth write and compare. + void ApplyDepthState(); + /// Returns the cached rasterizer state for desc, creating it on a miss. + ID3D11RasterizerState* GetRasterState(const D3D11_RASTERIZER_DESC& desc); + /// Returns the cached depth-stencil state for desc, creating it on a miss. + ID3D11DepthStencilState* GetDepthState( + const D3D11_DEPTH_STENCIL_DESC& desc); /// Creates the alpha-blend state used for transparent draws. bool CreateBlendState(); @@ -166,6 +176,14 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { bool m_depthWriteEnabled = true; D3D11_COMPARISON_FUNC m_depthFunc = D3D11_COMPARISON_LESS; + // Immutable state objects keyed by description, owned until Cleanup + std::vector>> + m_rasterStates; + std::vector>> + m_depthStates; + ImGuiContext* ImCon; // Shadow mapping diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 1a9b2ab9..062e0bbc 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -45,6 +45,56 @@ namespace Sleak { } } + /// Compares two rasterizer descriptions field by field. + bool SameDesc(const D3D11_RASTERIZER_DESC& a, + const D3D11_RASTERIZER_DESC& b) { + return a.FillMode == b.FillMode && a.CullMode == b.CullMode && + a.FrontCounterClockwise == b.FrontCounterClockwise && + a.DepthBias == b.DepthBias && + a.DepthBiasClamp == b.DepthBiasClamp && + a.SlopeScaledDepthBias == b.SlopeScaledDepthBias && + a.DepthClipEnable == b.DepthClipEnable && + a.ScissorEnable == b.ScissorEnable && + a.MultisampleEnable == b.MultisampleEnable && + a.AntialiasedLineEnable == b.AntialiasedLineEnable; + } + + /// Compares two stencil face descriptions field by field. + bool SameDesc(const D3D11_DEPTH_STENCILOP_DESC& a, + const D3D11_DEPTH_STENCILOP_DESC& b) { + return a.StencilFailOp == b.StencilFailOp && + a.StencilDepthFailOp == b.StencilDepthFailOp && + a.StencilPassOp == b.StencilPassOp && + a.StencilFunc == b.StencilFunc; + } + + /// Compares two depth-stencil descriptions field by field. + bool SameDesc(const D3D11_DEPTH_STENCIL_DESC& a, + const D3D11_DEPTH_STENCIL_DESC& b) { + return a.DepthEnable == b.DepthEnable && + a.DepthWriteMask == b.DepthWriteMask && + a.DepthFunc == b.DepthFunc && + a.StencilEnable == b.StencilEnable && + a.StencilReadMask == b.StencilReadMask && + a.StencilWriteMask == b.StencilWriteMask && + SameDesc(a.FrontFace, b.FrontFace) && + SameDesc(a.BackFace, b.BackFace); + } + + /// Finds the state cached for desc, or creates and caches it on a miss. + template + State* FindOrCreateState( + std::vector>>& cache, + const Desc& desc, Create create) { + for (const auto& [key, state] : cache) { + if (SameDesc(key, desc)) return state.Get(); + } + Microsoft::WRL::ComPtr state; + if (FAILED(create(&desc, state.GetAddressOf()))) return nullptr; + cache.emplace_back(desc, state); + return state.Get(); + } + } // namespace /// Registers this backend's buffer/shader/texture factories with ResourceManager. @@ -312,6 +362,9 @@ void DirectX11Renderer::Cleanup() { blendState = nullptr; } + m_rasterStates.clear(); + m_depthStates.clear(); + if(query) { query->Release(); @@ -665,7 +718,6 @@ void DirectX11Renderer::ConfigureRenderFace() { } bool DirectX11Renderer::SetRasterState() { - ID3D11RasterizerState* rasterState; D3D11_RASTERIZER_DESC RasterDesc = {}; RasterDesc.AntialiasedLineEnable = true; RasterDesc.MultisampleEnable = true; @@ -678,16 +730,32 @@ bool DirectX11Renderer::SetRasterState() { RasterDesc.FillMode = fillMode; RasterDesc.FrontCounterClockwise = false; - if (FAILED(device->CreateRasterizerState(&RasterDesc, &rasterState))) { - return false; - } else { - deviceContext->RSSetState(rasterState); - rasterState->Release(); - } + ID3D11RasterizerState* rasterState = GetRasterState(RasterDesc); + if (!rasterState) return false; + deviceContext->RSSetState(rasterState); return true; } +ID3D11RasterizerState* DirectX11Renderer::GetRasterState( + const D3D11_RASTERIZER_DESC& desc) { + return FindOrCreateState( + m_rasterStates, desc, + [this](const D3D11_RASTERIZER_DESC* d, ID3D11RasterizerState** out) { + return device->CreateRasterizerState(d, out); + }); +} + +ID3D11DepthStencilState* DirectX11Renderer::GetDepthState( + const D3D11_DEPTH_STENCIL_DESC& desc) { + return FindOrCreateState( + m_depthStates, desc, + [this](const D3D11_DEPTH_STENCIL_DESC* d, + ID3D11DepthStencilState** out) { + return device->CreateDepthStencilState(d, out); + }); +} + void DirectX11Renderer::SetRenderMode(RenderMode mode) { this->Mode = mode; ConfigureRenderMode(); @@ -781,19 +849,19 @@ Texture* DirectX11Renderer::CreateCubemapTextureFromPanorama( void DirectX11Renderer::SetDepthWrite(bool enabled) { m_depthWriteEnabled = enabled; + ApplyDepthState(); +} +void DirectX11Renderer::ApplyDepthState() { D3D11_DEPTH_STENCIL_DESC desc = {}; desc.DepthEnable = TRUE; - desc.DepthWriteMask = enabled ? D3D11_DEPTH_WRITE_MASK_ALL - : D3D11_DEPTH_WRITE_MASK_ZERO; + desc.DepthWriteMask = m_depthWriteEnabled ? D3D11_DEPTH_WRITE_MASK_ALL + : D3D11_DEPTH_WRITE_MASK_ZERO; desc.DepthFunc = m_depthFunc; desc.StencilEnable = FALSE; - ID3D11DepthStencilState* state = nullptr; - if (SUCCEEDED(device->CreateDepthStencilState(&desc, &state))) { + if (ID3D11DepthStencilState* state = GetDepthState(desc)) deviceContext->OMSetDepthStencilState(state, 1); - state->Release(); - } } void DirectX11Renderer::SetDepthCompare(DepthCompare compare) { @@ -808,18 +876,7 @@ void DirectX11Renderer::SetDepthCompare(DepthCompare compare) { case DepthCompare::Never: m_depthFunc = D3D11_COMPARISON_NEVER; break; } - D3D11_DEPTH_STENCIL_DESC desc = {}; - desc.DepthEnable = TRUE; - desc.DepthWriteMask = m_depthWriteEnabled ? D3D11_DEPTH_WRITE_MASK_ALL - : D3D11_DEPTH_WRITE_MASK_ZERO; - desc.DepthFunc = m_depthFunc; - desc.StencilEnable = FALSE; - - ID3D11DepthStencilState* state = nullptr; - if (SUCCEEDED(device->CreateDepthStencilState(&desc, &state))) { - deviceContext->OMSetDepthStencilState(state, 1); - state->Release(); - } + ApplyDepthState(); } void DirectX11Renderer::SetCullEnabled(bool enabled) { From 3c41c3d1c26680f493fcb912c2e784f507c82d5f Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:14:03 +0300 Subject: [PATCH 15/34] opengl: cache vertex layout state with separate attribute formats --- .../Graphics/OpenGL/OpenGLRenderer.hpp | 14 +- src/Graphics/OpenGL/OpenGLRenderer.cpp | 200 +++++++++++------- 2 files changed, 132 insertions(+), 82 deletions(-) diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index 4f926baf..40ce1ae7 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -8,11 +8,14 @@ #include #include #include +#include #include #include namespace Sleak { +struct VertexLayoutDesc; + namespace RenderEngine { /// OpenGL 3.3+ backend: deferred GBuffer pipeline, SSAO, shadow mapping, and baked IBL. @@ -104,6 +107,12 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { GLuint m_VAO = 0; bool m_debugLineMode = false; + // Vertex layout currently applied to m_VAO + static constexpr uint32_t NO_CONFIGURED_LAYOUT = UINT32_MAX; + uint32_t m_configuredLayout = NO_CONFIGURED_LAYOUT; + const VertexLayoutDesc* m_configuredLayoutDesc = nullptr; + uint32_t m_enabledAttribMask = 0; + // MSAA FBO resources GLuint m_msaaFBO = 0; GLuint m_msaaColorRBO = 0; @@ -115,8 +124,11 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { virtual void ConfigureRenderMode() override; virtual void ConfigureRenderFace() override; - /// Configures the shared VAO's vertex attribute layout for the standard mesh format. + /// Creates the shared mesh VAO and resets its cached attribute layout. void SetupVertexLayout(); + /// Applies a vertex layout's attribute formats to m_VAO unless it is + /// already the active one; a null desc selects the default Vertex. + void ConfigureVertexLayout(uint32_t layout, const VertexLayoutDesc* desc); // Shadow mapping GLuint m_shadowFBO = 0; diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 225ca347..50ec6cee 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -28,6 +28,36 @@ namespace Sleak { namespace RenderEngine { +namespace { + +constexpr GLuint VERTEX_BUFFER_BINDING = 0; +constexpr uint32_t DEFAULT_VERTEX_LAYOUT = 0; +constexpr uint32_t ATTRIB_MASK_BITS = 32; + +// Default Vertex attribute locations +constexpr GLuint ATTRIB_POSITION = 0; +constexpr GLuint ATTRIB_NORMAL = 1; +constexpr GLuint ATTRIB_TANGENT = 2; +constexpr GLuint ATTRIB_COLOR = 3; +constexpr GLuint ATTRIB_UV = 4; +constexpr GLuint ATTRIB_BONE_IDS = 5; +constexpr GLuint ATTRIB_BONE_WEIGHTS = 6; + +/// Sets one attribute's format on vao and routes it to the shared binding. +void SetAttribFormat(GLuint vao, GLuint location, GLint components, GLenum type, + bool integer, size_t offset) { + const GLuint relativeOffset = static_cast(offset); + if (integer) + glVertexArrayAttribIFormat(vao, location, components, type, + relativeOffset); + else + glVertexArrayAttribFormat(vao, location, components, type, GL_FALSE, + relativeOffset); + glVertexArrayAttribBinding(vao, location, VERTEX_BUFFER_BINDING); +} + +} // namespace + /// Registers this backend's buffer/shader/texture factories with ResourceManager. OpenGLRenderer::OpenGLRenderer(Window* window) : m_Window(window) { @@ -117,7 +147,83 @@ bool OpenGLRenderer::Initialize() { } void OpenGLRenderer::SetupVertexLayout() { - glGenVertexArrays(1, &m_VAO); + glCreateVertexArrays(1, &m_VAO); + m_configuredLayout = NO_CONFIGURED_LAYOUT; + m_configuredLayoutDesc = nullptr; + m_enabledAttribMask = 0; +} + +void OpenGLRenderer::ConfigureVertexLayout(uint32_t layout, + const VertexLayoutDesc* desc) { + if (layout == m_configuredLayout) return; + m_configuredLayout = layout; + m_configuredLayoutDesc = desc; + + uint32_t enabledMask = 0; + if (desc) { + for (const auto& attr : desc->attributes) { + switch (attr.format) { + case VertexAttribFormat::Float1: + SetAttribFormat(m_VAO, attr.location, 1, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float2: + SetAttribFormat(m_VAO, attr.location, 2, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float3: + SetAttribFormat(m_VAO, attr.location, 3, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::Float4: + SetAttribFormat(m_VAO, attr.location, 4, GL_FLOAT, false, + attr.offset); + break; + case VertexAttribFormat::UInt1: + SetAttribFormat(m_VAO, attr.location, 1, GL_UNSIGNED_INT, + true, attr.offset); + break; + case VertexAttribFormat::Int1: + SetAttribFormat(m_VAO, attr.location, 1, GL_INT, true, + attr.offset); + break; + } + if (attr.location < ATTRIB_MASK_BITS) + enabledMask |= (1u << attr.location); + else + glEnableVertexArrayAttrib(m_VAO, attr.location); + } + } else { + // Standard 96-byte Vertex layout: 7 attributes + SetAttribFormat(m_VAO, ATTRIB_POSITION, 3, GL_FLOAT, false, + offsetof(Vertex, px)); + SetAttribFormat(m_VAO, ATTRIB_NORMAL, 3, GL_FLOAT, false, + offsetof(Vertex, nx)); + SetAttribFormat(m_VAO, ATTRIB_TANGENT, 4, GL_FLOAT, false, + offsetof(Vertex, tx)); + SetAttribFormat(m_VAO, ATTRIB_COLOR, 4, GL_FLOAT, false, + offsetof(Vertex, r)); + SetAttribFormat(m_VAO, ATTRIB_UV, 2, GL_FLOAT, false, + offsetof(Vertex, u)); + SetAttribFormat(m_VAO, ATTRIB_BONE_IDS, 4, GL_INT, true, + offsetof(Vertex, boneIDs)); + SetAttribFormat(m_VAO, ATTRIB_BONE_WEIGHTS, 4, GL_FLOAT, false, + offsetof(Vertex, boneWeights)); + for (GLuint loc = ATTRIB_POSITION; loc <= ATTRIB_BONE_WEIGHTS; ++loc) + enabledMask |= (1u << loc); + } + + // Toggle only the slots whose enable state differs from the last layout + const uint32_t changed = enabledMask ^ m_enabledAttribMask; + for (uint32_t loc = 0; loc < ATTRIB_MASK_BITS; ++loc) { + const uint32_t bit = 1u << loc; + if (!(changed & bit)) continue; + if (enabledMask & bit) + glEnableVertexArrayAttrib(m_VAO, loc); + else + glDisableVertexArrayAttrib(m_VAO, loc); + } + m_enabledAttribMask = enabledMask; } void OpenGLRenderer::BeginRender() { @@ -398,89 +504,21 @@ void OpenGLRenderer::BindVertexBuffer(RefPtr buffer, auto* glBuf = dynamic_cast(buffer.get()); if (!glBuf) return; - glBindBuffer(GL_ARRAY_BUFFER, glBuf->GetGLBuffer()); - - // A registered layout drives the attribute pointers; 0 means default Vertex - const uint32_t customFormat = buffer->GetVertexFormat(); - const VertexLayoutDesc* customDesc = - customFormat != 0 ? VertexFormatRegistry::Get(customFormat) : nullptr; + // A registered layout drives the attribute formats; 0 means default Vertex + const uint32_t format = buffer->GetVertexFormat(); + const VertexLayoutDesc* desc = m_configuredLayoutDesc; + if (format != m_configuredLayout) { + desc = format != 0 ? VertexFormatRegistry::Get(format) : nullptr; + ConfigureVertexLayout(desc ? format : DEFAULT_VERTEX_LAYOUT, desc); + } - if (customDesc && customDesc->transparentReadsScene && - m_inForwardTransparentPass) + if (desc && desc->transparentReadsScene && m_inForwardTransparentPass) PrepareSceneRead(); - if (customDesc) { - // One attribute pointer per registry entry, tracking which slots stay live - const GLsizei stride = static_cast(customDesc->stride); - uint32_t enabledMask = 0; - for (const auto& attr : customDesc->attributes) { - const void* off = reinterpret_cast( - static_cast(attr.offset)); - glEnableVertexAttribArray(attr.location); - switch (attr.format) { - case VertexAttribFormat::Float1: - glVertexAttribPointer(attr.location, 1, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float2: - glVertexAttribPointer(attr.location, 2, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float3: - glVertexAttribPointer(attr.location, 3, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::Float4: - glVertexAttribPointer(attr.location, 4, GL_FLOAT, GL_FALSE, stride, off); - break; - case VertexAttribFormat::UInt1: - glVertexAttribIPointer(attr.location, 1, GL_UNSIGNED_INT, stride, off); - break; - case VertexAttribFormat::Int1: - glVertexAttribIPointer(attr.location, 1, GL_INT, stride, off); - break; - } - if (attr.location < 32) enabledMask |= (1u << attr.location); - } - // Disable attribute slots left enabled by a previous bind - constexpr uint32_t TRACKED_ATTRIB_SLOTS = 8; - for (uint32_t loc = 0; loc < TRACKED_ATTRIB_SLOTS; ++loc) { - if (!(enabledMask & (1u << loc))) glDisableVertexAttribArray(loc); - } - } else { - // Standard 96-byte Vertex layout: 7 attributes - // Position: 3 floats at offset 0 - glEnableVertexAttribArray(0); - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, px)); - - // Normal: 3 floats at offset 12 - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, nx)); - - // Tangent: 4 floats at offset 24 - glEnableVertexAttribArray(2); - glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, tx)); - - // Color: 4 floats at offset 40 - glEnableVertexAttribArray(3); - glVertexAttribPointer(3, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, r)); - - // UV: 2 floats at offset 56 - glEnableVertexAttribArray(4); - glVertexAttribPointer(4, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, u)); - - // BoneIDs: 4 ints (must use IPointer for integer attributes) - glEnableVertexAttribArray(5); - glVertexAttribIPointer(5, 4, GL_INT, sizeof(Vertex), - (void*)offsetof(Vertex, boneIDs)); - - // BoneWeights: 4 floats - glEnableVertexAttribArray(6); - glVertexAttribPointer(6, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)offsetof(Vertex, boneWeights)); - } + const GLsizei stride = desc ? static_cast(desc->stride) + : static_cast(sizeof(Vertex)); + glVertexArrayVertexBuffer(m_VAO, VERTEX_BUFFER_BINDING, + glBuf->GetGLBuffer(), 0, stride); } void OpenGLRenderer::BindIndexBuffer(RefPtr buffer, From 3aba7c6c0c121725c0dd2b48f4d34eaeaf738356 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:14:31 +0300 Subject: [PATCH 16/34] physics: test meshes in place with a bounds early-out --- src/Physics/CollisionDetection.cpp | 63 ++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Physics/CollisionDetection.cpp b/src/Physics/CollisionDetection.cpp index 2f2e5124..84b32700 100644 --- a/src/Physics/CollisionDetection.cpp +++ b/src/Physics/CollisionDetection.cpp @@ -300,15 +300,48 @@ static BoundingCapsule TransformCapsule(const BoundingCapsule& c, const Vector3D return BoundingCapsule(c.center * scale + pos, c.radius * maxScale, c.halfHeight * axisScale, c.axis); } -static TriangleMesh TransformMesh(const TriangleMesh& m, const Vector3D& pos, const Vector3D& scale) { - TriangleMesh result; - result.indices = m.indices; - result.vertices.resize(m.vertices.size()); - for (size_t i = 0; i < m.vertices.size(); ++i) { - result.vertices[i] = m.vertices[i] * scale + pos; +/// Relative padding that keeps the mesh bounds early-out exact under rounding. +static constexpr float MESH_BOUNDS_SLACK = 1e-4f; + +/// Largest absolute component of v. +static float MaxAbsComponent(const Vector3D& v) { + return std::max( + {std::abs(v.GetX()), std::abs(v.GetY()), std::abs(v.GetZ())}); +} + +/// False only when the sphere cannot touch anything inside bounds. +static bool SphereMayTouchBounds(const BoundingSphere& s, const AABB& bounds) { + float magnitude = 1.0f + std::abs(s.radius) + MaxAbsComponent(s.center) + + MaxAbsComponent(bounds.min) + MaxAbsComponent(bounds.max); + float reach = s.radius + MESH_BOUNDS_SLACK * magnitude; + Vector3D diff = s.center - ClosestPointOnAABB(bounds, s.center); + return diff.Dot(diff) <= reach * reach; +} + +/// Sphere vs a mesh placed at pos with per-axis scale, without copying it. +static CollisionManifold TestSphereVsPlacedMesh(const BoundingSphere& sphere, + const TriangleMesh& mesh, + const Vector3D& pos, + const Vector3D& scale) { + CollisionManifold deepest; + AABB worldBounds = TransformAABB(mesh.bounds, pos, scale); + if (!SphereMayTouchBounds(sphere, worldBounds)) return deepest; + + const std::vector& verts = mesh.vertices; + const std::vector& idx = mesh.indices; + for (size_t i = 0; i + 2 < idx.size(); i += 3) { + Vector3D v0 = verts[idx[i]] * scale + pos; + Vector3D v1 = verts[idx[i + 1]] * scale + pos; + Vector3D v2 = verts[idx[i + 2]] * scale + pos; + + CollisionManifold m = TestSphereVsTriangle(sphere, v0, v1, v2); + if (m.hasCollision && + m.contact.penetration > deepest.contact.penetration) { + deepest = m; + } } - result.bounds = TransformAABB(m.bounds, pos, scale); - return result; + + return deepest; } CollisionManifold TestCollision(const ColliderShape& shapeA, const Vector3D& posA, const Vector3D& scaleA, @@ -351,18 +384,24 @@ CollisionManifold TestCollision(const ColliderShape& shapeA, const Vector3D& pos return TestCapsuleVsCapsule(TransformCapsule(a, posA, scaleA), TransformCapsule(b, posB, scaleB)); } else if constexpr (std::is_same_v && std::is_same_v) { - return TestSphereVsMesh(TransformSphere(a, posA, scaleA), TransformMesh(b, posB, scaleB)); + return TestSphereVsPlacedMesh(TransformSphere(a, posA, scaleA), + b, posB, scaleB); } else if constexpr (std::is_same_v && std::is_same_v) { - auto m = TestSphereVsMesh(TransformSphere(b, posB, scaleB), TransformMesh(a, posA, scaleA)); + auto m = TestSphereVsPlacedMesh(TransformSphere(b, posB, scaleB), + a, posA, scaleA); m.contact.normal = m.contact.normal * -1.0f; return m; } else if constexpr (std::is_same_v && std::is_same_v) { - return TestAABBvsMesh(TransformAABB(a, posA, scaleA), TransformMesh(b, posB, scaleB)); + return TestSphereVsPlacedMesh( + BoundingSphere::FromAABB(TransformAABB(a, posA, scaleA)), b, + posB, scaleB); } else if constexpr (std::is_same_v && std::is_same_v) { - auto m = TestAABBvsMesh(TransformAABB(b, posB, scaleB), TransformMesh(a, posA, scaleA)); + auto m = TestSphereVsPlacedMesh( + BoundingSphere::FromAABB(TransformAABB(b, posB, scaleB)), a, + posA, scaleA); m.contact.normal = m.contact.normal * -1.0f; return m; } From 7bb24f45d832fdfb63dd6858d89d9fead5e0ff99 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:15:00 +0300 Subject: [PATCH 17/34] opengl: restore shadow pass viewport from tracked state --- .../Graphics/OpenGL/OpenGLRenderer.hpp | 5 ++++ src/Graphics/OpenGL/OpenGLRenderer.cpp | 26 ++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index 40ce1ae7..abfccb3a 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -113,6 +113,11 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { const VertexLayoutDesc* m_configuredLayoutDesc = nullptr; uint32_t m_enabledAttribMask = 0; + // Last viewport set outside the shadow pass (x, y, width, height) + std::array m_viewport = {}; + /// Sets the GL viewport and records it so passes can restore it later. + void ApplyViewport(GLint x, GLint y, GLsizei width, GLsizei height); + // MSAA FBO resources GLuint m_msaaFBO = 0; GLuint m_msaaColorRBO = 0; diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 50ec6cee..3a3b2fa1 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -111,7 +111,7 @@ bool OpenGLRenderer::Initialize() { int width, height; SDL_GetWindowSize(m_Window->GetSDLWindow(), &width, &height); - glViewport(0, 0, width, height); + ApplyViewport(0, 0, width, height); glEnable(GL_DEPTH_TEST); glEnable(GL_BLEND); @@ -358,7 +358,8 @@ bool OpenGLRenderer::ReloadShaders(const std::vector& stems) { #endif void OpenGLRenderer::Resize(uint32_t width, uint32_t height) { - glViewport(0, 0, width, height); + ApplyViewport(0, 0, static_cast(width), + static_cast(height)); // Recreate MSAA FBO at new size if (m_msaaFBO != 0) { CleanupMSAAFramebuffer(); @@ -442,12 +443,18 @@ void OpenGLRenderer::SetRenderMode(RenderMode mode) { void OpenGLRenderer::SetViewport(float x, float y, float width, float height, float minDepth, float maxDepth) { - glViewport(static_cast(x), static_cast(y), - static_cast(width), - static_cast(height)); + ApplyViewport(static_cast(x), static_cast(y), + static_cast(width), static_cast(height)); glDepthRangef(minDepth, maxDepth); } +void OpenGLRenderer::ApplyViewport(GLint x, GLint y, GLsizei width, + GLsizei height) { + glViewport(x, y, width, height); + // GL rejects negative sizes and keeps the previous viewport + if (width >= 0 && height >= 0) m_viewport = {x, y, width, height}; +} + void OpenGLRenderer::ClearRenderTarget(float r, float g, float b, float a) { glClearColor(r, g, b, a); @@ -833,9 +840,7 @@ void OpenGLRenderer::RenderShadowPass() { if (!CreateShadowMapResources()) return; } - // Save current viewport - GLint savedViewport[4]; - glGetIntegerv(GL_VIEWPORT, savedViewport); + const std::array savedViewport = m_viewport; // Bind shadow FBO glBindFramebuffer(GL_FRAMEBUFFER, m_shadowFBO); @@ -875,7 +880,8 @@ void OpenGLRenderer::RenderShadowPass() { else glBindFramebuffer(GL_FRAMEBUFFER, 0); - glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]); + ApplyViewport(savedViewport[0], savedViewport[1], savedViewport[2], + savedViewport[3]); // Bind shadow map texture at unit 3 glActiveTexture(GL_TEXTURE3); @@ -1441,7 +1447,7 @@ void OpenGLRenderer::ExecuteSSAOPass() { // ---- Pass 1: SSAO ---- glBindFramebuffer(GL_FRAMEBUFFER, m_ssaoFBO); - glViewport(0, 0, m_gbufferWidth, m_gbufferHeight); + ApplyViewport(0, 0, m_gbufferWidth, m_gbufferHeight); glDisable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glDisable(GL_BLEND); From 1632d29ba5075c17951023cd183d8ee6608af87d Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:15:01 +0300 Subject: [PATCH 18/34] d3d11: bind buffers with static_cast instead of dynamic_cast --- src/Graphics/DirectX11/DirectX11Renderer.cpp | 119 ++++++++----------- 1 file changed, 47 insertions(+), 72 deletions(-) diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 062e0bbc..2eae5f70 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -591,95 +591,70 @@ void DirectX11Renderer::ClearDepthStencil(bool clearDepth, bool clearStencil, // Buffer binding void DirectX11Renderer::BindVertexBuffer(RefPtr buffer, uint32_t slot) { - if (!buffer) - return; + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; + ID3D11Buffer* d3d11Buffer = dx11Buf->GetD3DBuffer(); UINT stride = sizeof(Sleak::Vertex); UINT offset = 0; - - try - { - auto d3d11Buffer = dynamic_cast - (buffer.get()) - ->GetD3DBuffer(); - - deviceContext->IASetVertexBuffers(slot, 1, &d3d11Buffer, &stride, &offset); - - } - catch (std::exception& e) - { - SLEAK_ERROR("Failed to cast Vertex Buffer! {}", e.what()); - } + deviceContext->IASetVertexBuffers(slot, 1, &d3d11Buffer, &stride, &offset); } void DirectX11Renderer::BindIndexBuffer(RefPtr buffer, uint32_t slot) { - if (!buffer) return; - - UINT offset = 0; - - try { - auto d3d11Buffer = - dynamic_cast(buffer.get())->GetD3DBuffer(); + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; - deviceContext->IASetIndexBuffer(d3d11Buffer, DXGI_FORMAT_R32_UINT, 0); - - } catch (std::exception& e) { - SLEAK_ERROR("Failed to cast Index Buffer! {}", e.what()); - } + deviceContext->IASetIndexBuffer(dx11Buf->GetD3DBuffer(), + DXGI_FORMAT_R32_UINT, 0); } void DirectX11Renderer::BindConstantBuffer(RefPtr buffer, uint32_t slot) { - try { - auto* dx11Buf = dynamic_cast(buffer.get()); - if (!dx11Buf) return; - - // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && dx11Buf->GetCPUShadowCopySize() >= 128) { - const float* srcWorld = reinterpret_cast( - static_cast(dx11Buf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; + auto* dx11Buf = static_cast(buffer.get()); + if (!dx11Buf) return; + + // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 + if (m_inShadowPass && slot == 0 && dx11Buf->GetCPUShadowCopySize() >= 128) { + const float* srcWorld = reinterpret_cast( + static_cast(dx11Buf->GetCPUShadowCopy()) + 64); + + float shadowPC[32]; + for (int r = 0; r < 4; ++r) { + for (int c = 0; c < 4; ++c) { + float sum = 0.0f; + for (int k = 0; k < 4; ++k) { + sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; } + shadowPC[r * 4 + c] = sum; } - std::memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); - - // Create dedicated shadow transform CB on first use - if (!m_shadowTransformCB) { - D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = 128; - desc.Usage = D3D11_USAGE_DYNAMIC; - desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; - desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB); - } - - // Update and bind the shadow CB instead of the original - D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { - std::memcpy(mapped.pData, shadowPC, sizeof(shadowPC)); - deviceContext->Unmap(m_shadowTransformCB, 0); - } - deviceContext->VSSetConstantBuffers(slot, 1, &m_shadowTransformCB); - deviceContext->PSSetConstantBuffers(slot, 1, &m_shadowTransformCB); - return; + } + std::memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + + // Create dedicated shadow transform CB on first use + if (!m_shadowTransformCB) { + D3D11_BUFFER_DESC desc{}; + desc.ByteWidth = 128; + desc.Usage = D3D11_USAGE_DYNAMIC; + desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; + device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB); } - auto d3d11Buffer = dx11Buf->GetD3DBuffer(); - deviceContext->VSSetConstantBuffers(slot, 1, &d3d11Buffer); - deviceContext->PSSetConstantBuffers(slot, 1, &d3d11Buffer); - - } catch (std::exception& e) { - SLEAK_ERROR("Failed to cast Constant Buffer! {}", e.what()); + // Update and bind the shadow CB instead of the original + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { + std::memcpy(mapped.pData, shadowPC, sizeof(shadowPC)); + deviceContext->Unmap(m_shadowTransformCB, 0); + } + deviceContext->VSSetConstantBuffers(slot, 1, &m_shadowTransformCB); + deviceContext->PSSetConstantBuffers(slot, 1, &m_shadowTransformCB); + return; } + + auto d3d11Buffer = dx11Buf->GetD3DBuffer(); + deviceContext->VSSetConstantBuffers(slot, 1, &d3d11Buffer); + deviceContext->PSSetConstantBuffers(slot, 1, &d3d11Buffer); } void DirectX11Renderer::ConfigureRenderMode() { From 6389ef6746755ffe0385c236246f85bae54a3409 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:15:26 +0300 Subject: [PATCH 19/34] d3d11: yield instead of spinning on the stats query --- include/private/Graphics/DirectX11/DirectX11Renderer.hpp | 2 +- src/Graphics/DirectX11/DirectX11Renderer.cpp | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index 26a8384e..0c4529a9 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -162,7 +162,7 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { ID3D11Texture2D* msaaDepthStencilBuffer = nullptr; ID3D11DepthStencilView* msaaDepthStencilView = nullptr; - ID3D11Query* query; + ID3D11Query* query = nullptr; D3D11_PRIMITIVE_TOPOLOGY topology; D3D11_FILL_MODE fillMode; diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 2eae5f70..c586597c 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "Graphics/Common/RenderCommandQueue.hpp" @@ -229,9 +230,9 @@ bool DirectX11Renderer::Initialize() { if (FAILED(device->CreateQuery(&queryDesc, &query))) SLEAK_WARN("Could not create pipeline statics object!"); - if(bEnabledPerformanceCounter){ - if(query) - while (deviceContext->GetData(query, nullptr, 0, 0) == S_FALSE); + if (bEnabledPerformanceCounter && query) { + while (deviceContext->GetData(query, nullptr, 0, 0) == S_FALSE) + std::this_thread::yield(); deviceContext->Begin(query); } From ba331d07e172b7d747ce371eb56c8e61aab912f4 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:15:44 +0300 Subject: [PATCH 20/34] assets: parse models once and post-process in one pass --- src/Assets/ModelLoader.cpp | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Assets/ModelLoader.cpp b/src/Assets/ModelLoader.cpp index 5cc3dc0c..c4637ea2 100644 --- a/src/Assets/ModelLoader.cpp +++ b/src/Assets/ModelLoader.cpp @@ -45,7 +45,6 @@ GameObject* ModelLoader::Load(const std::string& filePath, const ModelLoadOptions& options) { Assimp::Importer importer; - // First pass: read without PreTransformVertices to check for animations unsigned int baseFlags = aiProcess_Triangulate | aiProcess_GenSmoothNormals | aiProcess_CalcTangentSpace @@ -55,7 +54,9 @@ GameObject* ModelLoader::Load(const std::string& filePath, if (options.flipUVs) baseFlags |= aiProcess_FlipUVs; - const aiScene* scene = importer.ReadFile(filePath, baseFlags); + // Parse once without post-processing to learn whether it is animated + constexpr unsigned int rawImportFlags = 0; + const aiScene* scene = importer.ReadFile(filePath, rawImportFlags); if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { SLEAK_ERROR("Assimp: Failed to load model '{}': {}", filePath, @@ -65,6 +66,18 @@ GameObject* ModelLoader::Load(const std::string& filePath, bool hasAnimations = scene->HasAnimations(); + // Static models also get PreTransformVertices, in the same pipeline order + unsigned int postFlags = baseFlags; + if (!hasAnimations) + postFlags |= aiProcess_PreTransformVertices; + scene = importer.ApplyPostProcessing(postFlags); + + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { + SLEAK_ERROR("Assimp: Failed to post-process model '{}': {}", filePath, + importer.GetErrorString()); + return nullptr; + } + SLEAK_INFO("Model loaded: {} ({} meshes, {} materials, {} textures, {} animations)", filePath, scene->mNumMeshes, scene->mNumMaterials, scene->mNumTextures, scene->mNumAnimations); @@ -75,18 +88,6 @@ GameObject* ModelLoader::Load(const std::string& filePath, MaterialCache materialCache; if (!hasAnimations) { - // Static model: reimport with PreTransformVertices for optimization - importer.FreeScene(); - unsigned int staticFlags = baseFlags | aiProcess_PreTransformVertices; - scene = importer.ReadFile(filePath, staticFlags); - - if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { - SLEAK_ERROR("Assimp: Failed to reimport model '{}': {}", filePath, - importer.GetErrorString()); - delete root; - return nullptr; - } - ProcessNode(scene->mRootNode, scene, root, directory, options, textureCache, materialCache); } else { From 08da8334a8d30f10e7fa3ffe873750dbd7205aab Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:16:01 +0300 Subject: [PATCH 21/34] opengl: memoize shadow caster light-space transform --- .../Graphics/OpenGL/OpenGLRenderer.hpp | 4 ++ src/Graphics/OpenGL/OpenGLRenderer.cpp | 58 ++++++++++++------- 2 files changed, 41 insertions(+), 21 deletions(-) diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index abfccb3a..1234c4dd 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -149,6 +149,10 @@ class ENGINE_API OpenGLRenderer : public Renderer, public RenderContext { bool m_shadowMapRendered = false; bool m_inShadowPass = false; GLuint m_shadowTransformUBO = 0; + // Last shadow transform uploaded: [LightVP*World][World], keyed on World + float m_shadowTransformCache[32] = {}; + float m_shadowWorldCache[16] = {}; + bool m_shadowTransformCacheValid = false; void SetLightVP(const float* mat) override; /// Uploads light and shadow data, sampling with the matrix the shadow /// map was rendered with. diff --git a/src/Graphics/OpenGL/OpenGLRenderer.cpp b/src/Graphics/OpenGL/OpenGLRenderer.cpp index 3a3b2fa1..ac4cb249 100644 --- a/src/Graphics/OpenGL/OpenGLRenderer.cpp +++ b/src/Graphics/OpenGL/OpenGLRenderer.cpp @@ -43,6 +43,12 @@ constexpr GLuint ATTRIB_UV = 4; constexpr GLuint ATTRIB_BONE_IDS = 5; constexpr GLuint ATTRIB_BONE_WEIGHTS = 6; +// Transform CB layout read by the shadow pass: [WVP][World] +constexpr int MAT4_DIM = 4; +constexpr size_t MAT4_FLOATS = MAT4_DIM * MAT4_DIM; +constexpr size_t MAT4_BYTES = MAT4_FLOATS * sizeof(float); +constexpr size_t TRANSFORM_CB_BYTES = 2 * MAT4_BYTES; + /// Sets one attribute's format on vao and routes it to the shared binding. void SetAttribFormat(GLuint vao, GLuint location, GLint components, GLenum type, bool integer, size_t offset) { @@ -542,33 +548,42 @@ void OpenGLRenderer::BindConstantBuffer(RefPtr buffer, if (!glBuf) return; // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && glBuf->GetCPUShadowCopySize() >= 128) { + if (m_inShadowPass && slot == 0 && + glBuf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { const float* srcWorld = reinterpret_cast( - static_cast(glBuf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + static_cast(glBuf->GetCPUShadowCopy()) + MAT4_BYTES); // Create dedicated shadow transform UBO on first use if (m_shadowTransformUBO == 0) { - glGenBuffers(1, &m_shadowTransformUBO); - glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO); - glBufferData(GL_UNIFORM_BUFFER, 128, nullptr, GL_DYNAMIC_DRAW); - glBindBuffer(GL_UNIFORM_BUFFER, 0); + glCreateBuffers(1, &m_shadowTransformUBO); + glNamedBufferData(m_shadowTransformUBO, TRANSFORM_CB_BYTES, nullptr, + GL_DYNAMIC_DRAW); + m_shadowTransformCacheValid = false; + } + + // LightVP is fixed for the pass, so only a new World needs an upload + static_assert(sizeof(m_shadowTransformCache) == TRANSFORM_CB_BYTES); + static_assert(sizeof(m_shadowWorldCache) == MAT4_BYTES); + if (!m_shadowTransformCacheValid || + memcmp(srcWorld, m_shadowWorldCache, MAT4_BYTES) != 0) { + for (int r = 0; r < MAT4_DIM; ++r) { + for (int c = 0; c < MAT4_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MAT4_DIM; ++k) { + sum += srcWorld[r * MAT4_DIM + k] * + m_lightVP[k * MAT4_DIM + c]; + } + m_shadowTransformCache[r * MAT4_DIM + c] = sum; + } + } + memcpy(&m_shadowTransformCache[MAT4_FLOATS], srcWorld, MAT4_BYTES); + memcpy(m_shadowWorldCache, srcWorld, MAT4_BYTES); + m_shadowTransformCacheValid = true; + glNamedBufferSubData(m_shadowTransformUBO, 0, + sizeof(m_shadowTransformCache), + m_shadowTransformCache); } - glBindBuffer(GL_UNIFORM_BUFFER, m_shadowTransformUBO); - glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(shadowPC), shadowPC); - glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBufferBase(GL_UNIFORM_BUFFER, slot, m_shadowTransformUBO); return; } @@ -862,6 +877,7 @@ void OpenGLRenderer::RenderShadowPass() { // Execute shadow draw commands m_inShadowPass = true; + m_shadowTransformCacheValid = false; if (queue) { queue->ExecuteShadowPass(this); } From 71dd371b2204dc9f3b338c3db804ed8d6847e8d3 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:16:27 +0300 Subject: [PATCH 22/34] animation: binary search keyframes --- src/Animation/AnimatorComponent.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Animation/AnimatorComponent.cpp b/src/Animation/AnimatorComponent.cpp index 919f9ed4..5bec552d 100644 --- a/src/Animation/AnimatorComponent.cpp +++ b/src/Animation/AnimatorComponent.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include namespace Sleak { @@ -223,11 +224,14 @@ void AnimatorComponent::ProcessNodeHierarchy(int nodeIndex, /// Finds the keyframe pair straddling time and the lerp factor between them. template static std::pair FindKeyframe(const std::vector>& keys, float time) { - int idx = 0; - for (int i = 0; i < static_cast(keys.size()) - 1; ++i) { - if (time < keys[i + 1].time) { idx = i; break; } - idx = i; - } + if (keys.size() < 2) + return {0, 0.0f}; + + // First key in [1, size - 1) later than time; the last key otherwise + auto upper = std::upper_bound( + keys.begin() + 1, keys.end() - 1, time, + [](float t, const Keyframe& key) { return t < key.time; }); + int idx = static_cast(upper - keys.begin()) - 1; int next = idx + 1; if (next >= static_cast(keys.size())) return {idx, 0.0f}; From bbb0bfb69a735fcd463ff3414f65e515966d26f3 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:16:40 +0300 Subject: [PATCH 23/34] d3d11: memoize shadow caster transforms per world matrix --- .../Graphics/DirectX11/DirectX11Renderer.hpp | 3 + src/Graphics/DirectX11/DirectX11Renderer.cpp | 75 +++++++++++++------ 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index 0c4529a9..d7b3d81a 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -204,6 +204,9 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; ID3D11Buffer* m_shadowTransformCB = nullptr; // Dedicated CB for shadow pass transforms + // World last uploaded to m_shadowTransformCB, valid until lightVP changes + float m_shadowWorldCache[16] = {}; + bool m_shadowCBCacheValid = false; bool m_inShadowPass = false; void SetLightVP(const float* mat) override; /// Allocates the shadow-pass depth texture, DSV/SRV, and comparison sampler. diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index c586597c..7a41e98d 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -34,6 +34,29 @@ namespace Sleak { constexpr const char* TONEMAP_STEM = "tonemap"; #endif + // Transform CB layout: [WVP][World]; shadow CB: [LightVP*World][World] + constexpr int MATRIX_DIM = 4; + constexpr int MATRIX_FLOATS = MATRIX_DIM * MATRIX_DIM; + constexpr int TRANSFORM_CB_FLOATS = 2 * MATRIX_FLOATS; + constexpr UINT TRANSFORM_CB_BYTES = TRANSFORM_CB_FLOATS * sizeof(float); + constexpr size_t TRANSFORM_WORLD_OFFSET = MATRIX_FLOATS * sizeof(float); + + /// Writes World * LightVP (row-major) followed by World into out. + void BuildShadowTransform(const float* world, const float* lightVP, + float* out) { + for (int r = 0; r < MATRIX_DIM; ++r) { + for (int c = 0; c < MATRIX_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MATRIX_DIM; ++k) { + sum += world[r * MATRIX_DIM + k] * + lightVP[k * MATRIX_DIM + c]; + } + out[r * MATRIX_DIM + c] = sum; + } + } + std::memcpy(out + MATRIX_FLOATS, world, MATRIX_FLOATS * sizeof(float)); + } + /// Logs a failed D3DCompileFromFile call with the compiler output. void LogCompileError(const char* path, const char* entry, HRESULT hr, ID3DBlob* errors) { @@ -247,8 +270,10 @@ void DirectX11Renderer::BeginRender() { ApplyMSAAChange(); // Commit staged lightVP before shadow pass so shadow + main agree. - if (m_hasPendingLightVP) { + if (m_hasPendingLightVP && + std::memcmp(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)) != 0) { std::memcpy(m_lightVP, m_pendingLightVP, sizeof(m_lightVP)); + m_shadowCBCacheValid = false; } // Create tonemap resources on demand @@ -616,37 +641,42 @@ void DirectX11Renderer::BindConstantBuffer(RefPtr buffer, if (!dx11Buf) return; // Shadow pass: use dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && dx11Buf->GetCPUShadowCopySize() >= 128) { + if (m_inShadowPass && slot == 0 && + dx11Buf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { const float* srcWorld = reinterpret_cast( - static_cast(dx11Buf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - std::memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + static_cast(dx11Buf->GetCPUShadowCopy()) + + TRANSFORM_WORLD_OFFSET); // Create dedicated shadow transform CB on first use if (!m_shadowTransformCB) { D3D11_BUFFER_DESC desc{}; - desc.ByteWidth = 128; + desc.ByteWidth = TRANSFORM_CB_BYTES; desc.Usage = D3D11_USAGE_DYNAMIC; desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB); + if (FAILED(device->CreateBuffer(&desc, nullptr, + &m_shadowTransformCB))) + return; + m_shadowCBCacheValid = false; } - // Update and bind the shadow CB instead of the original - D3D11_MAPPED_SUBRESOURCE mapped{}; - if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) { - std::memcpy(mapped.pData, shadowPC, sizeof(shadowPC)); - deviceContext->Unmap(m_shadowTransformCB, 0); + // The CB keeps its contents, so only a new World needs an upload + if (!m_shadowCBCacheValid || + std::memcmp(srcWorld, m_shadowWorldCache, + sizeof(m_shadowWorldCache)) != 0) { + float shadowPC[TRANSFORM_CB_FLOATS]; + BuildShadowTransform(srcWorld, m_lightVP, shadowPC); + + D3D11_MAPPED_SUBRESOURCE mapped{}; + if (SUCCEEDED(deviceContext->Map(m_shadowTransformCB, 0, + D3D11_MAP_WRITE_DISCARD, 0, + &mapped))) { + std::memcpy(mapped.pData, shadowPC, sizeof(shadowPC)); + deviceContext->Unmap(m_shadowTransformCB, 0); + std::memcpy(m_shadowWorldCache, srcWorld, + sizeof(m_shadowWorldCache)); + m_shadowCBCacheValid = true; + } } deviceContext->VSSetConstantBuffers(slot, 1, &m_shadowTransformCB); deviceContext->PSSetConstantBuffers(slot, 1, &m_shadowTransformCB); @@ -1194,6 +1224,7 @@ void DirectX11Renderer::CleanupShadowResources() { if (m_shadowCB) { m_shadowCB->Release(); m_shadowCB = nullptr; } m_shadowCBCreated = false; if (m_shadowTransformCB) { m_shadowTransformCB->Release(); m_shadowTransformCB = nullptr; } + m_shadowCBCacheValid = false; if (m_shadowRasterState) { m_shadowRasterState->Release(); m_shadowRasterState = nullptr; } if (m_shadowSampler) { m_shadowSampler->Release(); m_shadowSampler = nullptr; } if (m_shadowSRV) { m_shadowSRV->Release(); m_shadowSRV = nullptr; } From 5db747a474550f3489e598da531bb307db9dcb17 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:17:10 +0300 Subject: [PATCH 24/34] opengl: linearize ssao tap depth instead of reconstructing world pos --- assets/shaders/ssao_gl.frag | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/assets/shaders/ssao_gl.frag b/assets/shaders/ssao_gl.frag index 23b20225..d4e298e3 100644 --- a/assets/shaders/ssao_gl.frag +++ b/assets/shaders/ssao_gl.frag @@ -52,6 +52,14 @@ vec3 ReconstructWorldPos(vec2 uv, float depth) { return world.xyz / world.w; } +// View-space z from depth; equals (View * ReconstructWorldPos(uv, depth)).z +// because the projection's z and w rows depend on view z alone. +float LinearizeViewZ(float depth) { + float ndcZ = depth * 2.0 - 1.0; + return (Projection[3][2] - Projection[3][3] * ndcZ) + / (Projection[2][3] * ndcZ - Projection[2][2]); +} + void main() { if (ssaoEnabled == 0u) { outColor = 1.0; @@ -76,19 +84,18 @@ void main() { vec3 bitangent = cross(worldN, tangent); mat3 TBN = mat3(tangent, bitangent, worldN); - // Reference view-space Z for the current fragment. - // View * worldPos here only needs to be consistent with itself — the - // comparison is self-referential, so even if the view matrix is - // column- vs row-major mis-interpreted, both sides of the compare are - // transformed the same way and the result stays meaningful. - float fragViewZ = (View * vec4(worldPos, 1.0)).z; + // Reference view-space Z for the current fragment; View's z row maps a + // world offset to its view-space z delta. + float fragViewZ = LinearizeViewZ(depth); + vec3 viewZAxis = vec3(View[0][2], View[1][2], View[2][2]); mat4 VP = Projection * View; float occlusion = 0.0; for (uint i = 0u; i < ssaoKernelSize; ++i) { - vec3 sampleWorld = worldPos + (TBN * ssaoKernel[i].xyz) * ssaoRadius; + vec3 sampleOffset = (TBN * ssaoKernel[i].xyz) * ssaoRadius; + vec3 sampleWorld = worldPos + sampleOffset; vec4 clipSample = VP * vec4(sampleWorld, 1.0); if (clipSample.w <= 0.0) continue; @@ -98,9 +105,8 @@ void main() { float sampleDepth = texture(depthTexture, uv).r; if (sampleDepth >= 1.0) continue; // sky — no occluder - vec3 actualWorld = ReconstructWorldPos(uv, sampleDepth); - float actualViewZ = (View * vec4(actualWorld, 1.0)).z; - float sampleViewZ = (View * vec4(sampleWorld, 1.0)).z; + float actualViewZ = LinearizeViewZ(sampleDepth); + float sampleViewZ = fragViewZ + dot(viewZAxis, sampleOffset); float rangeCheck = smoothstep(0.0, 1.0, ssaoRadius / max(abs(fragViewZ - actualViewZ), 1e-4)); From 39f5fa09ac83364bc5e57a07f1b19b5ce9ccaa19 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:17:24 +0300 Subject: [PATCH 25/34] container: exact-size List copies and std::sort --- include/public/Utility/Container/List.hpp | 59 ++++++----------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/include/public/Utility/Container/List.hpp b/include/public/Utility/Container/List.hpp index 49c48cb6..7a48b6ab 100644 --- a/include/public/Utility/Container/List.hpp +++ b/include/public/Utility/Container/List.hpp @@ -4,6 +4,7 @@ #include #include // For std::swap #include +#include namespace Sleak { /** @@ -23,45 +24,20 @@ class List { size_t size = 0; size_t capacity = 0; - /// Recursive quicksort driving sort(). - void quickSort(int low, int high, - std::function compare) { - if (low < high) { - int pivotIndex = partition(low, high, compare); - quickSort(low, pivotIndex - 1, compare); - quickSort(pivotIndex + 1, high, compare); - } - } - - /// Lomuto partition step for quickSort; returns the pivot's final index. - int partition(int low, int high, - std::function compare) { - T pivot = data[high]; - int i = low - 1; - - for (int j = low; j < high; j++) { - if (compare(data[j], pivot)) { - i++; - std::swap(data[i], data[j]); - } - } - - std::swap(data[i + 1], data[high]); - return i + 1; - } - public: List() noexcept = default; /// Adopts an existing buffer directly; List takes ownership. List(T* Data, size_t Size) : data(Data), size(Size), capacity(Size) {} - // Copy constructor - List(const List& other) : size(other.size), capacity(other.capacity) { - data = new T[capacity]; - for (size_t i = 0; i < size; ++i) { - data[i] = other.data[i]; - } + /// Copies other's elements into an exact-size buffer; empty allocates none. + List(const List& other) { + if (other.size == 0) return; + std::unique_ptr copy(new T[other.size]); + std::copy(other.data, other.data + other.size, copy.get()); + data = copy.release(); + size = other.size; + capacity = other.size; } // Move constructor @@ -72,16 +48,11 @@ class List { other.capacity = 0; } - // Copy assignment + /// Replaces contents with a copy of other; strong exception guarantee. List& operator=(const List& other) { if (this != &other) { - delete[] data; - size = other.size; - capacity = other.capacity; - data = new T[capacity]; - for (size_t i = 0; i < size; ++i) { - data[i] = other.data[i]; - } + List copy(other); + swap(copy); } return *this; } @@ -225,8 +196,10 @@ class List { } // Operations - void sort(std::function compare) { - quickSort(0, size - 1, compare); + /// Sorts in place; compare must be a strict weak ordering. + template + void sort(Compare compare) { + std::sort(data, data + size, compare); } void reverse() { From 49345b297441d1100be6c25bb2c7b7142311ccd3 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:18:06 +0300 Subject: [PATCH 26/34] d3d12: memoize shadow caster transforms per world matrix --- .../Graphics/DirectX12/DirectX12Renderer.hpp | 3 + src/Graphics/DirectX12/DirectX12Renderer.cpp | 60 +++++++++++++------ 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp index 9adc6df4..c998a8e3 100644 --- a/include/private/Graphics/DirectX12/DirectX12Renderer.hpp +++ b/include/private/Graphics/DirectX12/DirectX12Renderer.hpp @@ -267,6 +267,9 @@ class ENGINE_API DirectX12Renderer : public Renderer, public RenderContext { float m_lightVP[16] = {}; float m_pendingLightVP[16] = {}; bool m_hasPendingLightVP = false; + // Last shadow transform uploaded this pass and the World it was built from + float m_shadowWorldCache[16] = {}; + D3D12_GPU_VIRTUAL_ADDRESS m_shadowCBAddress = 0; bool m_inShadowPass = false; // Depth-only PSO for shadow pass (no PS, no RTV, CULL_NONE) ID3D12PipelineState* m_shadowPassPSO = nullptr; diff --git a/src/Graphics/DirectX12/DirectX12Renderer.cpp b/src/Graphics/DirectX12/DirectX12Renderer.cpp index 7c18ed92..0befa0e6 100644 --- a/src/Graphics/DirectX12/DirectX12Renderer.cpp +++ b/src/Graphics/DirectX12/DirectX12Renderer.cpp @@ -70,6 +70,28 @@ constexpr const char* SKYBOX_STEM = "skybox"; constexpr const char* DEBUG_LINE_STEM = "debug_line"; #endif +// Transform CB layout: [WVP][World]; shadow CB: [LightVP*World][World] +constexpr int MATRIX_DIM = 4; +constexpr int MATRIX_FLOATS = MATRIX_DIM * MATRIX_DIM; +constexpr int TRANSFORM_CB_FLOATS = 2 * MATRIX_FLOATS; +constexpr size_t TRANSFORM_CB_BYTES = TRANSFORM_CB_FLOATS * sizeof(float); +constexpr size_t TRANSFORM_WORLD_OFFSET = MATRIX_FLOATS * sizeof(float); + +/// Writes World * LightVP (row-major) followed by World into out. +void BuildShadowTransform(const float* world, const float* lightVP, + float* out) { + for (int r = 0; r < MATRIX_DIM; ++r) { + for (int c = 0; c < MATRIX_DIM; ++c) { + float sum = 0.0f; + for (int k = 0; k < MATRIX_DIM; ++k) { + sum += world[r * MATRIX_DIM + k] * lightVP[k * MATRIX_DIM + c]; + } + out[r * MATRIX_DIM + c] = sum; + } + } + memcpy(out + MATRIX_FLOATS, world, MATRIX_FLOATS * sizeof(float)); +} + /// Compiles one fixed-pass stage, logging the compiler output on failure. Microsoft::WRL::ComPtr CompileStage(const char* path, const char* entry, @@ -1215,26 +1237,25 @@ void DirectX12Renderer::BindConstantBuffer(RefPtr buffer, if (!dx12Buf) return; // Shadow pass: use a dedicated shadow CB with LightVP*World for slot 0 - if (m_inShadowPass && slot == 0 && dx12Buf->GetCPUShadowCopySize() >= 128) { + if (m_inShadowPass && slot == 0 && + dx12Buf->GetCPUShadowCopySize() >= TRANSFORM_CB_BYTES) { const float* srcWorld = reinterpret_cast( - static_cast(dx12Buf->GetCPUShadowCopy()) + 64); - - float shadowPC[32]; - for (int r = 0; r < 4; ++r) { - for (int c = 0; c < 4; ++c) { - float sum = 0.0f; - for (int k = 0; k < 4; ++k) { - sum += srcWorld[r * 4 + k] * m_lightVP[k * 4 + c]; - } - shadowPC[r * 4 + c] = sum; - } - } - memcpy(&shadowPC[16], srcWorld, sizeof(float) * 16); + static_cast(dx12Buf->GetCPUShadowCopy()) + + TRANSFORM_WORLD_OFFSET); - DirectX12UploadRing::Allocation alloc; - if (!m_uploadRing.Allocate(sizeof(shadowPC), alloc)) return; - memcpy(alloc.cpu, shadowPC, sizeof(shadowPC)); - commandList->SetGraphicsRootConstantBufferView(slot, alloc.gpu); + // Casters sharing a World reuse this pass's ring copy + if (!m_shadowCBAddress || + memcmp(srcWorld, m_shadowWorldCache, + sizeof(m_shadowWorldCache)) != 0) { + DirectX12UploadRing::Allocation alloc; + if (!m_uploadRing.Allocate(TRANSFORM_CB_BYTES, alloc)) return; + BuildShadowTransform(srcWorld, m_lightVP, + static_cast(alloc.cpu)); + memcpy(m_shadowWorldCache, srcWorld, sizeof(m_shadowWorldCache)); + m_shadowCBAddress = alloc.gpu; + } + commandList->SetGraphicsRootConstantBufferView(slot, + m_shadowCBAddress); return; } @@ -1526,6 +1547,9 @@ void DirectX12Renderer::RenderShadowPass() { } BindLightConstants(); + // Ring memory is per frame and lightVP may have changed since last pass + m_shadowCBAddress = 0; + // Execute shadow draw commands m_inShadowPass = true; if (queue) { From 6426ae6608bc5a4185a13e8b8b4e0234c6b99be7 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:23:10 +0300 Subject: [PATCH 27/34] math: debug-only Matrix bounds check, drop unused zero test --- include/public/Math/Matrix.hpp | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/include/public/Math/Matrix.hpp b/include/public/Math/Matrix.hpp index bdcaf509..8b4d5fd3 100644 --- a/include/public/Math/Matrix.hpp +++ b/include/public/Math/Matrix.hpp @@ -3,6 +3,7 @@ #include #include "Math.hpp" +#include #include #include #include @@ -57,18 +58,15 @@ class Matrix { } } - // Bounds-checked element access + /// Element access; indices are bounds-checked by assert in debug builds. T& operator()(size_t row, size_t col) { - if (row >= Rows || col >= Cols) { - throw std::out_of_range("Matrix indices out of range"); - } + assert(row < Rows && col < Cols && "Matrix indices out of range"); return data[row][col]; } + /// Read-only element access; bounds-checked by assert in debug builds. const T& operator()(size_t row, size_t col) const { - if (row >= Rows || col >= Cols) { - throw std::out_of_range("Matrix indices out of range"); - } + assert(row < Rows && col < Cols && "Matrix indices out of range"); return data[row][col]; } @@ -94,14 +92,12 @@ class Matrix { return result; } - // Optimized matrix multiplication with early exit for zero-like cases + /// Row-by-column matrix product. template Matrix operator*( const Matrix& other) const { Matrix result; - // Quick zero check - bool isZero = true; for (size_t i = 0; i < Rows; ++i) { for (size_t j = 0; j < OtherCols; ++j) { T sum = static_cast(0); @@ -109,11 +105,6 @@ class Matrix { sum += data[i][k] * other(k, j); } result(i, j) = sum; - - // Track if result is non-zero - if (std::abs(sum) > std::numeric_limits::epsilon()) { - isZero = false; - } } } From 05158b81dc08189481d14def0f8bfe7e4d9968f3 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:23:28 +0300 Subject: [PATCH 28/34] culling: vectorize occluder triangle raster with SSE2 --- src/Culling/CullingSystem.cpp | 205 +++++++++++++++++++++++++++++----- 1 file changed, 176 insertions(+), 29 deletions(-) diff --git a/src/Culling/CullingSystem.cpp b/src/Culling/CullingSystem.cpp index dd5cb2e1..8bbf7474 100644 --- a/src/Culling/CullingSystem.cpp +++ b/src/Culling/CullingSystem.cpp @@ -3,13 +3,22 @@ #include #include #include +#include #include +#if defined(__SSE2__) || defined(_M_X64) +#define SLEAK_CULL_SSE2 1 +#include +#else +#define SLEAK_CULL_SSE2 0 +#endif + namespace Sleak { namespace { constexpr float kWEps = 1e-4f; constexpr float kZBias = 1e-4f; +constexpr int kLanes = 4; /// Clip-space vertex (row-vector convention: clip = point * VP). struct ClipVert { @@ -102,6 +111,145 @@ int ClipNear(const ClipVert* in, int n, ClipVert* out) { return m; } +/// Clamped pixel bounds, edge values at the first pixel center and their +/// per-pixel (A) and per-row (B) steps for one screen triangle. +struct TriSetup { + int minX, maxX, minY, maxY; + float w0Row, w1Row, w2Row; + float A0, A1, A2; + float B0, B1, B2; + float z0, z1, z2; + float invArea; + bool positive; +}; + +#if SLEAK_CULL_SSE2 +/// Per-lane inside test for one pixel column; kPositive picks the winding. +template +inline __m128 EdgeMask(__m128 w0, __m128 w1, __m128 w2) { + const __m128 zero = _mm_setzero_ps(); + if constexpr (kPositive) { + return _mm_and_ps( + _mm_and_ps(_mm_cmpge_ps(w0, zero), _mm_cmpge_ps(w1, zero)), + _mm_cmpge_ps(w2, zero)); + } else { + return _mm_and_ps( + _mm_and_ps(_mm_cmple_ps(w0, zero), _mm_cmple_ps(w1, zero)), + _mm_cmple_ps(w2, zero)); + } +} + +/// Min-merges the first n lanes of v into dst with the scalar compare. +inline void MinStorePartial(float* dst, __m128 v, int n) { + alignas(16) float tmp[kLanes]; + _mm_store_ps(tmp, v); + for (int i = 0; i < n; ++i) + if (tmp[i] < dst[i]) dst[i] = tmp[i]; +} + +/// SSE2 fill of 4-row strips; lanes are rows so each lane repeats the scalar +/// x-stepping exactly, and 4x4 tiles are transposed for row-major stores. +template +void RasterTriSse2(const TriSetup& t, float* depth, int W) { + const __m128 a0 = _mm_set1_ps(t.A0); + const __m128 a1 = _mm_set1_ps(t.A1); + const __m128 a2 = _mm_set1_ps(t.A2); + const __m128 zv0 = _mm_set1_ps(t.z0); + const __m128 zv1 = _mm_set1_ps(t.z1); + const __m128 zv2 = _mm_set1_ps(t.z2); + const __m128 inv = _mm_set1_ps(t.invArea); + const __m128 zero = _mm_setzero_ps(); + const __m128 one = _mm_set1_ps(1.0f); + const __m128 untouched = + _mm_set1_ps(std::numeric_limits::infinity()); + + float w0Row = t.w0Row, w1Row = t.w1Row, w2Row = t.w2Row; + for (int y = t.minY; y <= t.maxY; y += kLanes) { + alignas(16) float r0[kLanes], r1[kLanes], r2[kLanes]; + for (int i = 0; i < kLanes; ++i) { + r0[i] = w0Row; + r1[i] = w1Row; + r2[i] = w2Row; + w0Row += t.B0; + w1Row += t.B1; + w2Row += t.B2; + } + const int rows = std::min(kLanes, t.maxY - y + 1); + __m128 e0 = _mm_load_ps(r0); + __m128 e1 = _mm_load_ps(r1); + __m128 e2 = _mm_load_ps(r2); + float* rowBase = depth + y * W; + + for (int x = t.minX; x <= t.maxX; x += kLanes) { + __m128 s0 = e0, s1 = e1, s2 = e2; + __m128 m[kLanes]; + __m128 any = zero; + for (int k = 0; k < kLanes; ++k) { + m[k] = EdgeMask(e0, e1, e2); + any = _mm_or_ps(any, m[k]); + e0 = _mm_add_ps(e0, a0); + e1 = _mm_add_ps(e1, a1); + e2 = _mm_add_ps(e2, a2); + } + if (_mm_movemask_ps(any) == 0) continue; + + __m128 zc[kLanes]; + for (int k = 0; k < kLanes; ++k) { + __m128 z = _mm_add_ps( + _mm_add_ps(_mm_mul_ps(s0, zv0), _mm_mul_ps(s1, zv1)), + _mm_mul_ps(s2, zv2)); + z = _mm_mul_ps(z, inv); + z = _mm_min_ps(one, _mm_max_ps(zero, z)); + zc[k] = _mm_or_ps(_mm_and_ps(m[k], z), + _mm_andnot_ps(m[k], untouched)); + s0 = _mm_add_ps(s0, a0); + s1 = _mm_add_ps(s1, a1); + s2 = _mm_add_ps(s2, a2); + } + _MM_TRANSPOSE4_PS(zc[0], zc[1], zc[2], zc[3]); + + float* p = rowBase + x; + const int cols = std::min(kLanes, t.maxX - x + 1); + if (cols == kLanes) { + for (int r = 0; r < rows; ++r) { + float* q = p + r * W; + _mm_storeu_ps(q, _mm_min_ps(zc[r], _mm_loadu_ps(q))); + } + } else { + for (int r = 0; r < rows; ++r) + MinStorePartial(p + r * W, zc[r], cols); + } + } + } +} +#else +/// Scalar fill of the triangle's bounding box with min-depth writes. +void RasterTriScalar(const TriSetup& t, float* depth, int W) { + float w0Row = t.w0Row, w1Row = t.w1Row, w2Row = t.w2Row; + for (int y = t.minY; y <= t.maxY; ++y) { + float w0 = w0Row, w1 = w1Row, w2 = w2Row; + int row = y * W; + for (int x = t.minX; x <= t.maxX; ++x) { + bool inside = t.positive ? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f) + : (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f); + if (inside) { + float z = (w0 * t.z0 + w1 * t.z1 + w2 * t.z2) * t.invArea; + if (z < 0.0f) z = 0.0f; + if (z > 1.0f) z = 1.0f; + int idx = row + x; + if (z < depth[idx]) depth[idx] = z; + } + w0 += t.A0; + w1 += t.A1; + w2 += t.A2; + } + w0Row += t.B0; + w1Row += t.B1; + w2Row += t.B2; + } +} +#endif + /// Edge-function rasterizer with incremental row stepping, min-depth write. void RasterScreenTri(float x0, float y0, float z0, float x1, float y1, float z1, float x2, float y2, float z2) { @@ -130,35 +278,34 @@ void RasterScreenTri(float x0, float y0, float z0, float x1, float y1, float px0 = minX + 0.5f; float py0 = minY + 0.5f; - float w0Row = (x2 - x1) * (py0 - y1) - (y2 - y1) * (px0 - x1); - float w1Row = (x0 - x2) * (py0 - y2) - (y0 - y2) * (px0 - x2); - float w2Row = (x1 - x0) * (py0 - y0) - (y1 - y0) * (px0 - x0); - - float A0 = y1 - y2, A1 = y2 - y0, A2 = y0 - y1; - float B0 = x2 - x1, B1 = x0 - x2, B2 = x1 - x0; - bool positive = area > 0.0f; - - for (int y = minY; y <= maxY; ++y) { - float w0 = w0Row, w1 = w1Row, w2 = w2Row; - int row = y * W; - for (int x = minX; x <= maxX; ++x) { - bool inside = positive ? (w0 >= 0.0f && w1 >= 0.0f && w2 >= 0.0f) - : (w0 <= 0.0f && w1 <= 0.0f && w2 <= 0.0f); - if (inside) { - float z = (w0 * z0 + w1 * z1 + w2 * z2) * invArea; - if (z < 0.0f) z = 0.0f; - if (z > 1.0f) z = 1.0f; - int idx = row + x; - if (z < s.depth[idx]) s.depth[idx] = z; - } - w0 += A0; - w1 += A1; - w2 += A2; - } - w0Row += B0; - w1Row += B1; - w2Row += B2; - } + TriSetup t; + t.minX = minX; + t.maxX = maxX; + t.minY = minY; + t.maxY = maxY; + t.w0Row = (x2 - x1) * (py0 - y1) - (y2 - y1) * (px0 - x1); + t.w1Row = (x0 - x2) * (py0 - y2) - (y0 - y2) * (px0 - x2); + t.w2Row = (x1 - x0) * (py0 - y0) - (y1 - y0) * (px0 - x0); + t.A0 = y1 - y2; + t.A1 = y2 - y0; + t.A2 = y0 - y1; + t.B0 = x2 - x1; + t.B1 = x0 - x2; + t.B2 = x1 - x0; + t.z0 = z0; + t.z1 = z1; + t.z2 = z2; + t.invArea = invArea; + t.positive = area > 0.0f; + +#if SLEAK_CULL_SSE2 + if (t.positive) + RasterTriSse2(t, s.depth.data(), W); + else + RasterTriSse2(t, s.depth.data(), W); +#else + RasterTriScalar(t, s.depth.data(), W); +#endif } /// Near-clip a clip-space triangle, fan-triangulate, rasterize. From 23fab58e20775dc6faa5e9e0a5ce5aa1b562ece8 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:25:49 +0300 Subject: [PATCH 29/34] culling: vectorize occlusion rect depth scan with SSE2 --- src/Culling/CullingSystem.cpp | 36 +++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/src/Culling/CullingSystem.cpp b/src/Culling/CullingSystem.cpp index 8bbf7474..d1841d3a 100644 --- a/src/Culling/CullingSystem.cpp +++ b/src/Culling/CullingSystem.cpp @@ -359,6 +359,32 @@ void RasterBox(const Math::AABB& box, const Math::Matrix4& vp, if (cam.GetZ() > mxz) quad(c001, c101, c111, c011); } +/// True when any depth in the inclusive pixel rect is >= thresh. +bool AnyDepthAtOrBehind(const float* depth, int W, int minX, int maxX, int minY, + int maxY, float thresh) { +#if SLEAK_CULL_SSE2 + if (maxX - minX + 1 >= kLanes) { + const __m128 t = _mm_set1_ps(thresh); + const int lastX = maxX - kLanes + 1; + for (int y = minY; y <= maxY; ++y) { + const float* row = depth + y * W; + __m128 hit = _mm_cmpge_ps(_mm_loadu_ps(row + lastX), t); + for (int x = minX; x < lastX; x += kLanes) + hit = _mm_or_ps(hit, _mm_cmpge_ps(_mm_loadu_ps(row + x), t)); + if (_mm_movemask_ps(hit) != 0) return true; + } + return false; + } +#endif + for (int y = minY; y <= maxY; ++y) { + const float* row = depth + y * W; + for (int x = minX; x <= maxX; ++x) { + if (row[x] >= thresh) return true; + } + } + return false; +} + /// Conservative depth test: true when the box could be in front of the /// rasterized occluders anywhere in its projected rect. bool DepthRectVisible(const Math::AABB& box) { @@ -391,14 +417,8 @@ bool DepthRectVisible(const Math::AABB& box) { if (maxY > H - 1) maxY = H - 1; if (minX > maxX || minY > maxY) return true; - float thresh = boxMinZ - kZBias; - for (int y = minY; y <= maxY; ++y) { - int row = y * W; - for (int x = minX; x <= maxX; ++x) { - if (s.depth[row + x] >= thresh) return true; - } - } - return false; + return AnyDepthAtOrBehind(s.depth.data(), W, minX, maxX, minY, maxY, + boxMinZ - kZBias); } /// True when the box projects entirely in front but spans < 2x2 px. From c96a8cc9a12728dfe819e73f912f90de8e90acbf Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:26:51 +0300 Subject: [PATCH 30/34] gameobject: cache transform, material and mesh component lookups --- include/public/Core/GameObject.hpp | 34 ++++++++++++++++++++++++------ src/Scene/GameObject.cpp | 19 +++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/include/public/Core/GameObject.hpp b/include/public/Core/GameObject.hpp index bc39fbf3..636b3258 100644 --- a/include/public/Core/GameObject.hpp +++ b/include/public/Core/GameObject.hpp @@ -12,6 +12,9 @@ namespace Sleak { namespace Math {class Vector3D;}; class SceneBase; + class TransformComponent; + class MaterialComponent; + class MeshComponent; /// Scene entity holding components and an optional parent/child transform /// hierarchy. Scene owns the instance; destroy via Scene::DestroyObject(). /// @@ -80,6 +83,7 @@ namespace Sleak { RefPtr newComponent = RefPtr(new T(this, std::forward(args)...)); if (bIsInitialized) InitializeComponent(newComponent.get()); Components.add(std::move(newComponent)); + RefreshComponentCache(); } /// Destroys and detaches the first component of type T, if present. @@ -91,6 +95,7 @@ namespace Sleak { if (dynamic_cast(Components[i].get()) != nullptr) { Components[i]->OnDestroy(); Components.erase(i); + RefreshComponentCache(); break; } } @@ -102,15 +107,23 @@ namespace Sleak { static_assert(std::is_base_of_v, "T must derive from Component!"); - for (size_t i = 0; i < Components.GetSize(); ++i) { - Component* rawPtr = Components[i].get(); - if (!rawPtr) continue; + if constexpr (std::is_same_v) { + return m_cachedTransform; + } else if constexpr (std::is_same_v) { + return m_cachedMaterial; + } else if constexpr (std::is_same_v) { + return m_cachedMesh; + } else { + for (size_t i = 0; i < Components.GetSize(); ++i) { + Component* rawPtr = Components[i].get(); + if (!rawPtr) continue; + + T* component = dynamic_cast(rawPtr); + if (component) return component; + } - T* component = dynamic_cast(rawPtr); - if (component) return component; + return nullptr; } - - return nullptr; } /// True if a component of type T is attached. @@ -171,12 +184,19 @@ namespace Sleak { List> Components; + // First matches in Components, kept in sync by RefreshComponentCache + TransformComponent* m_cachedTransform = nullptr; + MaterialComponent* m_cachedMaterial = nullptr; + MeshComponent* m_cachedMesh = nullptr; + // Hierarchy GameObject* m_parent; List m_children; /// Calls OnDestroy() on and drops every attached component. void DestroyComponents(); + /// Re-resolves the cached transform, material and mesh pointers. + void RefreshComponentCache(); /// Initializes a component once, then enables it if active. void InitializeComponent(Component* component); diff --git a/src/Scene/GameObject.cpp b/src/Scene/GameObject.cpp index 17394171..7ff07082 100644 --- a/src/Scene/GameObject.cpp +++ b/src/Scene/GameObject.cpp @@ -152,6 +152,25 @@ namespace Sleak { Components[i]->OnDestroy(); } Components.clear(); + RefreshComponentCache(); + } + + void GameObject::RefreshComponentCache() { + m_cachedTransform = nullptr; + m_cachedMaterial = nullptr; + m_cachedMesh = nullptr; + + for (size_t i = 0; i < Components.GetSize(); ++i) { + Component* component = Components[i].get(); + if (!component) continue; + + if (!m_cachedTransform) + m_cachedTransform = dynamic_cast(component); + if (!m_cachedMaterial) + m_cachedMaterial = dynamic_cast(component); + if (!m_cachedMesh) + m_cachedMesh = dynamic_cast(component); + } } /// Fallback material for the primitive factories below, using the default shader. From 9a73e713291719d43243100097a671c3aeb2e019 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:28:45 +0300 Subject: [PATCH 31/34] scene: submit transform and material state only for visible meshes --- .../ECS/Components/MaterialComponent.hpp | 7 +++++- .../public/ECS/Components/MeshComponent.hpp | 2 +- .../ECS/Components/TransformComponent.hpp | 6 ++++- src/Scene/MaterialComponent.cpp | 10 ++++++++ src/Scene/MeshComponent.cpp | 25 ++++++++++++------- src/Scene/TransformComponent.cpp | 8 ++++++ 6 files changed, 46 insertions(+), 12 deletions(-) diff --git a/include/public/ECS/Components/MaterialComponent.hpp b/include/public/ECS/Components/MaterialComponent.hpp index 73106988..578fd239 100644 --- a/include/public/ECS/Components/MaterialComponent.hpp +++ b/include/public/ECS/Components/MaterialComponent.hpp @@ -32,7 +32,7 @@ namespace Sleak { /// Initializes the owned material's GPU resources. bool Initialize() override; - /// Submits the material to the render command queue for this frame. + /// Submits the material bind unless a sibling mesh does it. void Update(float DeltaTime) override; void OnEnable() override; void OnDisable() override; @@ -54,6 +54,11 @@ namespace Sleak { void SetRoughness(float roughness); private: + friend class MeshComponent; + + /// Queues the material bind while enabled and a material is set. + void SubmitRenderState(); + RefPtr m_material; bool m_enabled = true; }; diff --git a/include/public/ECS/Components/MeshComponent.hpp b/include/public/ECS/Components/MeshComponent.hpp index 1da11242..b8f62bbf 100644 --- a/include/public/ECS/Components/MeshComponent.hpp +++ b/include/public/ECS/Components/MeshComponent.hpp @@ -32,7 +32,7 @@ namespace Sleak { /// Validates that vertex and index buffers were created successfully. virtual bool Initialize() override; - /// Culls against cull bounds if set, then submits an indexed draw call. + /// Culls, then submits sibling transform and material state and the draw. virtual void Update(float deltaTime) override; void SetVertexBuffer(RefPtr& buffer); diff --git a/include/public/ECS/Components/TransformComponent.hpp b/include/public/ECS/Components/TransformComponent.hpp index c051dfd0..e72e563a 100644 --- a/include/public/ECS/Components/TransformComponent.hpp +++ b/include/public/ECS/Components/TransformComponent.hpp @@ -35,7 +35,7 @@ namespace Sleak { /// Allocates the transform's GPU constant buffer. bool Initialize() override; - /// Refreshes and rebinds the transform constant buffer for this frame. + /// Submits this frame's constant buffer unless a sibling mesh does it. void Update(float DeltaTime) override; /// World matrix (S*R*T), recomputed only after a transform change. @@ -99,10 +99,14 @@ namespace Sleak { Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); private: + friend class MeshComponent; + RefPtr ConstantBuffer; Math::Matrix4 m_world = Math::Matrix4::Identity(); bool m_worldDirty = true; + /// Queues the constant buffer upload followed by its slot 0 bind. + void SubmitRenderState(); /// Pushes the world matrix and main camera matrices to the GPU constant buffer. void UpdateConstantBuffer(); /// Returns the cached world matrix, rebuilding it first if dirty. diff --git a/src/Scene/MaterialComponent.cpp b/src/Scene/MaterialComponent.cpp index 037ca3a8..7c3444a5 100644 --- a/src/Scene/MaterialComponent.cpp +++ b/src/Scene/MaterialComponent.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include namespace Sleak { @@ -41,6 +43,14 @@ namespace Sleak { } void MaterialComponent::Update(float DeltaTime) { + MeshComponent* mesh = + owner ? owner->GetComponent() : nullptr; + if (mesh && mesh->IsInitialized()) return; + + SubmitRenderState(); + } + + void MaterialComponent::SubmitRenderState() { if (!m_enabled || !m_material) return; diff --git a/src/Scene/MeshComponent.cpp b/src/Scene/MeshComponent.cpp index 186e2d37..4452d574 100644 --- a/src/Scene/MeshComponent.cpp +++ b/src/Scene/MeshComponent.cpp @@ -3,6 +3,7 @@ #include "../../include/private/Graphics/Common/ConstantBuffer.hpp" #include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" #include +#include #include #include #include @@ -55,19 +56,20 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje if (!bIsInitialized) return; + TransformComponent* tc = + owner ? owner->GetComponent() : nullptr; + if (m_hasLocalBounds || m_hasCullBounds) { Math::AABB worldBounds = m_cullBounds; bool cullReady = m_hasCullBounds; - if (m_hasLocalBounds && owner) { - if (auto* tc = owner->GetComponent()) { - Math::Matrix4 m = tc->GetTransformMatrix(); - Math::AABB acc = TransformedPoint(m_localBounds.Corner(0), m); - for (int i = 1; i < 8; ++i) - acc.Merge(TransformedPoint(m_localBounds.Corner(i), m)); - worldBounds = acc; - cullReady = true; - } + if (m_hasLocalBounds && tc) { + Math::Matrix4 m = tc->GetTransformMatrix(); + Math::AABB acc = TransformedPoint(m_localBounds.Corner(0), m); + for (int i = 1; i < 8; ++i) + acc.Merge(TransformedPoint(m_localBounds.Corner(i), m)); + worldBounds = acc; + cullReady = true; } if (cullReady && @@ -75,6 +77,11 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje return; } + if (tc) tc->SubmitRenderState(); + if (MaterialComponent* mc = + owner ? owner->GetComponent() : nullptr) + mc->SubmitRenderState(); + RenderEngine::RenderCommandQueue::GetInstance()->SubmitDrawIndexed(VertexBuffer,IndexBuffer,ConstantBuffers,IndexCount); } diff --git a/src/Scene/TransformComponent.cpp b/src/Scene/TransformComponent.cpp index 7c92fb56..c54de7b1 100644 --- a/src/Scene/TransformComponent.cpp +++ b/src/Scene/TransformComponent.cpp @@ -43,6 +43,14 @@ namespace Sleak { } void TransformComponent::Update(float DeltaTime) { + MeshComponent* mesh = + owner ? owner->GetComponent() : nullptr; + if (mesh && mesh->IsInitialized()) return; + + SubmitRenderState(); + } + + void TransformComponent::SubmitRenderState() { UpdateConstantBuffer(); RenderEngine::RenderCommandQueue::GetInstance()->SubmitBindConstantBuffer(ConstantBuffer, 0); From ff401fcd6eb5f585273e24c569a36a1561e766e1 Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:29:36 +0300 Subject: [PATCH 32/34] mesh: transform cull bounds with Arvo's center-extent method --- src/Scene/MeshComponent.cpp | 39 +++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Scene/MeshComponent.cpp b/src/Scene/MeshComponent.cpp index 4452d574..fdc8022f 100644 --- a/src/Scene/MeshComponent.cpp +++ b/src/Scene/MeshComponent.cpp @@ -8,20 +8,32 @@ #include #include #include +#include namespace Sleak { namespace { - // World point (row-vector convention: world = local * M) as a - // degenerate AABB for accumulation. - Math::AABB TransformedPoint(const Math::Vector3D& p, - const Math::Matrix4& m) { - float x = p.GetX(), y = p.GetY(), z = p.GetZ(); - Math::Vector3D w( - x * m(0, 0) + y * m(1, 0) + z * m(2, 0) + m(3, 0), - x * m(0, 1) + y * m(1, 1) + z * m(2, 1) + m(3, 1), - x * m(0, 2) + y * m(1, 2) + z * m(2, 2) + m(3, 2)); - return Math::AABB(w, w); + /// World AABB of a local box under m (world = local * m), via Arvo's method. + Math::AABB TransformBounds(const Math::AABB& box, const Math::Matrix4& m) { + const Math::Vector3D c = box.Center(); + const Math::Vector3D e = box.Extents(); + const float cx = c.GetX(), cy = c.GetY(), cz = c.GetZ(); + const float ex = std::fabs(e.GetX()); + const float ey = std::fabs(e.GetY()); + const float ez = std::fabs(e.GetZ()); + + const Math::Vector3D center( + cx * m(0, 0) + cy * m(1, 0) + cz * m(2, 0) + m(3, 0), + cx * m(0, 1) + cy * m(1, 1) + cz * m(2, 1) + m(3, 1), + cx * m(0, 2) + cy * m(1, 2) + cz * m(2, 2) + m(3, 2)); + const Math::Vector3D extents( + ex * std::fabs(m(0, 0)) + ey * std::fabs(m(1, 0)) + + ez * std::fabs(m(2, 0)), + ex * std::fabs(m(0, 1)) + ey * std::fabs(m(1, 1)) + + ez * std::fabs(m(2, 1)), + ex * std::fabs(m(0, 2)) + ey * std::fabs(m(1, 2)) + + ez * std::fabs(m(2, 2))); + return Math::AABB(center - extents, center + extents); } } // namespace @@ -64,11 +76,8 @@ MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(obje bool cullReady = m_hasCullBounds; if (m_hasLocalBounds && tc) { - Math::Matrix4 m = tc->GetTransformMatrix(); - Math::AABB acc = TransformedPoint(m_localBounds.Corner(0), m); - for (int i = 1; i < 8; ++i) - acc.Merge(TransformedPoint(m_localBounds.Corner(i), m)); - worldBounds = acc; + worldBounds = + TransformBounds(m_localBounds, tc->GetTransformMatrix()); cullReady = true; } From 3e4e7ff5b36b4969ac3f5eb5fc6049d29e1425dd Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 13:32:59 +0300 Subject: [PATCH 33/34] culling: fold raster depth clamp into the coverage mask --- src/Culling/CullingSystem.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Culling/CullingSystem.cpp b/src/Culling/CullingSystem.cpp index d1841d3a..e576e189 100644 --- a/src/Culling/CullingSystem.cpp +++ b/src/Culling/CullingSystem.cpp @@ -198,10 +198,8 @@ void RasterTriSse2(const TriSetup& t, float* depth, int W) { __m128 z = _mm_add_ps( _mm_add_ps(_mm_mul_ps(s0, zv0), _mm_mul_ps(s1, zv1)), _mm_mul_ps(s2, zv2)); - z = _mm_mul_ps(z, inv); - z = _mm_min_ps(one, _mm_max_ps(zero, z)); - zc[k] = _mm_or_ps(_mm_and_ps(m[k], z), - _mm_andnot_ps(m[k], untouched)); + z = _mm_min_ps(one, _mm_mul_ps(z, inv)); + zc[k] = _mm_max_ps(_mm_andnot_ps(m[k], untouched), z); s0 = _mm_add_ps(s0, a0); s1 = _mm_add_ps(s1, a1); s2 = _mm_add_ps(s2, a2); From 83530dc7a136a4727655cfe48b970b065aa7ddca Mon Sep 17 00:00:00 2001 From: Canberk Pitirli Date: Mon, 2 Nov 2026 21:45:09 +0300 Subject: [PATCH 34/34] style: format changed lines --- .../Graphics/Common/RenderCommandQueue.hpp | 114 +++--- .../Graphics/Common/RenderCommands.hpp | 4 +- .../Graphics/DirectX11/DirectX11Renderer.hpp | 20 +- .../Graphics/OpenGL/OpenGLRenderer.hpp | 14 +- .../Graphics/Vulkan/VulkanRenderer.hpp | 3 +- .../ECS/Components/MaterialComponent.hpp | 10 +- .../public/ECS/Components/MeshComponent.hpp | 3 +- .../ECS/Components/TransformComponent.hpp | 47 +-- include/public/Math/Matrix.hpp | 4 +- include/public/Physics/PhysicsWorld.hpp | 10 +- .../public/Utility/Container/HashTable.hpp | 369 ++++++++---------- include/public/Utility/Container/Queue.hpp | 274 +++++++------ src/Animation/AnimatorComponent.cpp | 9 +- src/Assets/ModelLoader.cpp | 6 +- src/Core/Application.cpp | 8 +- src/Culling/CullingSystem.cpp | 1 - src/Graphics/Common/RenderCommandQueue.cpp | 55 +-- src/Graphics/Common/RenderCommands.cpp | 8 +- src/Graphics/DirectX11/DirectX11Renderer.cpp | 21 +- src/Graphics/DirectX12/DirectX12Renderer.cpp | 8 +- src/Graphics/Vulkan/VulkanDescriptors.cpp | 46 +-- src/Physics/CollisionDetection.cpp | 8 +- src/Physics/PhysicsWorld.cpp | 26 +- src/Scene/GameObject.cpp | 3 +- src/Scene/MaterialComponent.cpp | 2 +- src/Scene/MeshComponent.cpp | 59 +-- src/Scene/TransformComponent.cpp | 7 +- 27 files changed, 562 insertions(+), 577 deletions(-) diff --git a/include/private/Graphics/Common/RenderCommandQueue.hpp b/include/private/Graphics/Common/RenderCommandQueue.hpp index 9d7b8b14..848c5a6a 100644 --- a/include/private/Graphics/Common/RenderCommandQueue.hpp +++ b/include/private/Graphics/Common/RenderCommandQueue.hpp @@ -2,7 +2,6 @@ #define _RENDERCOMMANDQUEUE_H #include -#include "RenderCommands.hpp" #include #include #include @@ -10,6 +9,8 @@ #include #include +#include "RenderCommands.hpp" + namespace Sleak { namespace RenderEngine { /// Frame-scoped queue of recorded draw/state commands, replayed into a RenderContext at flush time. @@ -87,63 +88,72 @@ namespace Sleak { /// A draw command paired with the transform/bone buffers it needs to replay in the shadow pass. struct ShadowDrawEntry { RenderCommandBase* command = nullptr; - const RefPtr* transformBuffer = nullptr; // last-bound slot-0 buffer - const RefPtr* boneBuffer = nullptr; // slot-3 bone UBO for skinned draws + const RefPtr* transformBuffer = + nullptr; // last-bound slot-0 buffer + const RefPtr* boneBuffer = + nullptr; // slot-3 bone UBO for skinned draws }; private: - /// Bump allocator owning one frame's commands until the slot is recycled. - class CommandArena { + /// Bump allocator owning one frame's commands until the slot is + /// recycled. + class CommandArena { public: - CommandArena() = default; - CommandArena(const CommandArena&) = delete; - CommandArena& operator=(const CommandArena&) = delete; - ~CommandArena() { Reset(); } - - /// Constructs a command in arena memory and appends it to the frame's list. - template - T* Create(Args&&... args) { - void* memory = Allocate(sizeof(T), alignof(T)); - T* command = new (memory) T(std::forward(args)...); - m_commands.push_back(command); - return command; - } - - /// Destroys every command and rewinds the blocks for reuse. - void Reset(); - - const std::vector& Commands() const { return m_commands; } + CommandArena() = default; + CommandArena(const CommandArena&) = delete; + CommandArena& operator=(const CommandArena&) = delete; + ~CommandArena() { Reset(); } + + /// Constructs a command in arena memory and appends it to the + /// frame's list. + template + T* Create(Args&&... args) { + void* memory = Allocate(sizeof(T), alignof(T)); + T* command = new (memory) T(std::forward(args)...); + m_commands.push_back(command); + return command; + } + + /// Destroys every command and rewinds the blocks for reuse. + void Reset(); + + const std::vector& Commands() const { + return m_commands; + } private: - struct Block { - std::unique_ptr data; - size_t size = 0; - }; - - /// Returns aligned storage from the current block, adding blocks as needed. - void* Allocate(size_t size, size_t alignment); - - static constexpr size_t BLOCK_SIZE = 64 * 1024; - std::vector m_blocks; - size_t m_blockIndex = 0; - size_t m_offset = 0; - std::vector m_commands; - }; - - /// Moves submission to the next arena, destroying the commands it last held. - void AdvanceArena(); - - CommandArena& CurrentArena() { return m_arenas[m_currentArena]; } - - // A frame's commands stay alive while its shadow replay and GPU frames in flight may use them - static constexpr int RETIRE_FRAMES = 3; - static constexpr int ARENA_COUNT = RETIRE_FRAMES + 2; - static RenderCommandQueue* Instance; - CommandArena m_arenas[ARENA_COUNT]; - int m_currentArena = 0; - std::vector cachedShadowDraws; - std::vector m_pendingState; - std::vector m_forwardCmds; + struct Block { + std::unique_ptr data; + size_t size = 0; + }; + + /// Returns aligned storage from the current block, adding blocks + /// as needed. + void* Allocate(size_t size, size_t alignment); + + static constexpr size_t BLOCK_SIZE = 64 * 1024; + std::vector m_blocks; + size_t m_blockIndex = 0; + size_t m_offset = 0; + std::vector m_commands; + }; + + /// Moves submission to the next arena, destroying the commands it last + /// held. + void AdvanceArena(); + + CommandArena& CurrentArena() { return m_arenas[m_currentArena]; } + + // A frame's commands stay alive while its shadow replay and GPU frames + // in flight may use them + static constexpr int RETIRE_FRAMES = 3; + static constexpr int ARENA_COUNT = RETIRE_FRAMES + 2; + static RenderCommandQueue* Instance; + CommandArena m_arenas[ARENA_COUNT]; + int m_currentArena = 0; + std::vector cachedShadowDraws; + std::vector m_pendingState; + std::vector m_forwardCmds; }; } } diff --git a/include/private/Graphics/Common/RenderCommands.hpp b/include/private/Graphics/Common/RenderCommands.hpp index 33661c07..abe33a9b 100644 --- a/include/private/Graphics/Common/RenderCommands.hpp +++ b/include/private/Graphics/Common/RenderCommands.hpp @@ -148,7 +148,9 @@ namespace Sleak { RENDER_COMMAND(BindConstantBuffer) - const RefPtr& GetBuffer() const { return constantBuffer; } + const RefPtr& GetBuffer() const { + return constantBuffer; + } int GetSlot() const { return slot; } private: diff --git a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp index d7b3d81a..5f3ee786 100644 --- a/include/private/Graphics/DirectX11/DirectX11Renderer.hpp +++ b/include/private/Graphics/DirectX11/DirectX11Renderer.hpp @@ -5,21 +5,23 @@ #ifndef _DIRECTX11RENDERER_H #define _DIRECTX11RENDERER_H -#include "Graphics/Common/RenderContext.hpp" -#include "Graphics/Common/Renderer.hpp" -#include "Graphics/Common/ResourceManager.hpp" -#include +#include #include -#include -#include #include -#include -#include +#include + #include +#include +#include #include +#include #include #include +#include "Graphics/Common/RenderContext.hpp" +#include "Graphics/Common/Renderer.hpp" +#include "Graphics/Common/ResourceManager.hpp" + namespace Sleak { namespace RenderEngine { @@ -161,7 +163,7 @@ class ENGINE_API DirectX11Renderer : public Renderer, public RenderContext { ID3D11RenderTargetView* msaaRenderTargetView = nullptr; ID3D11Texture2D* msaaDepthStencilBuffer = nullptr; ID3D11DepthStencilView* msaaDepthStencilView = nullptr; - + ID3D11Query* query = nullptr; D3D11_PRIMITIVE_TOPOLOGY topology; diff --git a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp index 1234c4dd..f1f0f1e0 100644 --- a/include/private/Graphics/OpenGL/OpenGLRenderer.hpp +++ b/include/private/Graphics/OpenGL/OpenGLRenderer.hpp @@ -1,16 +1,18 @@ #ifndef _OPENGLRENDERER_H #define _OPENGLRENDERER_H -#include -#include "../Common/Renderer.hpp" -#include "../Common/RenderContext.hpp" -#include "../../Core/Window.hpp" #include +#include #include +#include + +#include #include #include -#include -#include + +#include "../../Core/Window.hpp" +#include "../Common/RenderContext.hpp" +#include "../Common/Renderer.hpp" namespace Sleak { diff --git a/include/private/Graphics/Vulkan/VulkanRenderer.hpp b/include/private/Graphics/Vulkan/VulkanRenderer.hpp index 73b5c206..2af3b0b7 100644 --- a/include/private/Graphics/Vulkan/VulkanRenderer.hpp +++ b/include/private/Graphics/Vulkan/VulkanRenderer.hpp @@ -737,7 +737,8 @@ class ENGINE_API VulkanRenderer : public Renderer, public RenderContext { VkDeviceSize m_pbrMaterialUBOStride = 0; uint32_t m_pbrMaterialSlot[MAX_FRAMES_IN_FLIGHT] = {}; static constexpr uint32_t PBR_TEXTURE_COUNT = 6; - /// A material's written set contents, reused when it is bound again this frame. + /// A material's written set contents, reused when it is bound again this + /// frame. struct PBRMaterialBinding { PBRMaterialParams params{}; std::array images{}; diff --git a/include/public/ECS/Components/MaterialComponent.hpp b/include/public/ECS/Components/MaterialComponent.hpp index 578fd239..b8288e0e 100644 --- a/include/public/ECS/Components/MaterialComponent.hpp +++ b/include/public/ECS/Components/MaterialComponent.hpp @@ -54,13 +54,13 @@ namespace Sleak { void SetRoughness(float roughness); private: - friend class MeshComponent; + friend class MeshComponent; - /// Queues the material bind while enabled and a material is set. - void SubmitRenderState(); + /// Queues the material bind while enabled and a material is set. + void SubmitRenderState(); - RefPtr m_material; - bool m_enabled = true; + RefPtr m_material; + bool m_enabled = true; }; } diff --git a/include/public/ECS/Components/MeshComponent.hpp b/include/public/ECS/Components/MeshComponent.hpp index b8f62bbf..4d2dfa3d 100644 --- a/include/public/ECS/Components/MeshComponent.hpp +++ b/include/public/ECS/Components/MeshComponent.hpp @@ -32,7 +32,8 @@ namespace Sleak { /// Validates that vertex and index buffers were created successfully. virtual bool Initialize() override; - /// Culls, then submits sibling transform and material state and the draw. + /// Culls, then submits sibling transform and material state and the + /// draw. virtual void Update(float deltaTime) override; void SetVertexBuffer(RefPtr& buffer); diff --git a/include/public/ECS/Components/TransformComponent.hpp b/include/public/ECS/Components/TransformComponent.hpp index e72e563a..a07b1f9f 100644 --- a/include/public/ECS/Components/TransformComponent.hpp +++ b/include/public/ECS/Components/TransformComponent.hpp @@ -91,28 +91,31 @@ namespace Sleak { }; protected: - /// Flags the cached world matrix stale; call after writing fields directly. - void MarkWorldDirty() { m_worldDirty = true; } - - Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); - Quaternion rotation = Quaternion(); - Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); - - private: - friend class MeshComponent; - - RefPtr ConstantBuffer; - Math::Matrix4 m_world = Math::Matrix4::Identity(); - bool m_worldDirty = true; - - /// Queues the constant buffer upload followed by its slot 0 bind. - void SubmitRenderState(); - /// Pushes the world matrix and main camera matrices to the GPU constant buffer. - void UpdateConstantBuffer(); - /// Returns the cached world matrix, rebuilding it first if dirty. - const Math::Matrix4& WorldMatrix(); - /// Combines the transform with the main camera's view and projection matrices. - Math::Matrix4 GetMVP(); + /// Flags the cached world matrix stale; call after writing fields + /// directly. + void MarkWorldDirty() { m_worldDirty = true; } + + Vector3D position = Vector3D(0.0f, 0.0f, 0.0f); + Quaternion rotation = Quaternion(); + Vector3D scale = Vector3D(1.0f, 1.0f, 1.0f); + + private: + friend class MeshComponent; + + RefPtr ConstantBuffer; + Math::Matrix4 m_world = Math::Matrix4::Identity(); + bool m_worldDirty = true; + + /// Queues the constant buffer upload followed by its slot 0 bind. + void SubmitRenderState(); + /// Pushes the world matrix and main camera matrices to the GPU constant + /// buffer. + void UpdateConstantBuffer(); + /// Returns the cached world matrix, rebuilding it first if dirty. + const Math::Matrix4& WorldMatrix(); + /// Combines the transform with the main camera's view and projection + /// matrices. + Math::Matrix4 GetMVP(); }; } // namespace Sleak diff --git a/include/public/Math/Matrix.hpp b/include/public/Math/Matrix.hpp index 8b4d5fd3..f792b138 100644 --- a/include/public/Math/Matrix.hpp +++ b/include/public/Math/Matrix.hpp @@ -2,15 +2,15 @@ #define _MATRIX_H_ #include -#include "Math.hpp" #include #include #include #include #include -#include "Vector.hpp" +#include "Math.hpp" #include "Quaternion.hpp" +#include "Vector.hpp" namespace Sleak { namespace Math { diff --git a/include/public/Physics/PhysicsWorld.hpp b/include/public/Physics/PhysicsWorld.hpp index a6150299..99e0cd07 100644 --- a/include/public/Physics/PhysicsWorld.hpp +++ b/include/public/Physics/PhysicsWorld.hpp @@ -108,12 +108,12 @@ namespace Sleak { float maxDist, uint32_t layerMask = 0xFFFFFFFF) const; private: - /// Refits every proxy, enlarging dynamic ones along their velocity. - void UpdateBroadphase(float dt); - void FindPairsAndResolve(); + /// Refits every proxy, enlarging dynamic ones along their velocity. + void UpdateBroadphase(float dt); + void FindPairsAndResolve(); - DynamicAABBTree m_tree; - std::vector m_colliders; + DynamicAABBTree m_tree; + std::vector m_colliders; }; } // namespace Physics diff --git a/include/public/Utility/Container/HashTable.hpp b/include/public/Utility/Container/HashTable.hpp index dfa19249..13fa482f 100644 --- a/include/public/Utility/Container/HashTable.hpp +++ b/include/public/Utility/Container/HashTable.hpp @@ -8,14 +8,15 @@ namespace Sleak { - /** +/** * @class HashTable - * @brief Implements a hash table (or map) for efficient key-value storage and retrieval. + * @brief Implements a hash table (or map) for efficient key-value storage and + * retrieval. * - * The HashTable class provides fast access to values based on their associated keys. - * It is suitable for scenarios where you need to quickly look up data using unique - * identifiers. Use this class when you need to store and retrieve data based on keys, - * such as symbol tables, dictionaries, or caching systems. + * The HashTable class provides fast access to values based on their associated + * keys. It is suitable for scenarios where you need to quickly look up data + * using unique identifiers. Use this class when you need to store and retrieve + * data based on keys, such as symbol tables, dictionaries, or caching systems. * * Example Use Cases: * - Implementing a symbol table in a compiler. @@ -29,219 +30,185 @@ namespace Sleak * - Provides methods for inserting, deleting, and retrieving key-value pairs. * @ingroup utility */ - template - class HashTable - { - private: - /// One slot: key/value plus occupied and tombstone-deleted flags. - struct Entry - { - Key key; - Value value; - bool occupied; - bool deleted; // To handle tombstones in deletion - - Entry() : occupied(false), deleted(false) {} - }; - - Entry* table; - size_t capacity; - size_t size; - size_t tombstones; - float loadFactor; - - static constexpr size_t DEFAULT_CAPACITY = 16; - static constexpr float DEFAULT_LOAD_FACTOR = 0.7f; - static constexpr float MAX_LOAD_FACTOR = 1.0f; - static constexpr size_t MIN_CAPACITY = 1; - static constexpr size_t GROWTH_FACTOR = 2; - - /// Home slot of key in a table with tableCapacity slots. - static size_t slotFor(const Key& key, size_t tableCapacity) - { - return std::hash{}(key) % tableCapacity; - } - - /// Home slot of key in the current table. - size_t hash(const Key& key) const - { - return slotFor(key, capacity); - } +template +class HashTable { + private: + /// One slot: key/value plus occupied and tombstone-deleted flags. + struct Entry { + Key key; + Value value; + bool occupied; + bool deleted; // To handle tombstones in deletion + + Entry() : occupied(false), deleted(false) {} + }; - /// Slot holding a live entry for key, or capacity if there is none. - size_t findSlot(const Key& key) const - { - size_t index = hash(key); - for (size_t probes = 0; probes < capacity && table[index].occupied; - ++probes) - { - if (!table[index].deleted && table[index].key == key) - { - return index; - } - index = (index + 1) % capacity; + Entry* table; + size_t capacity; + size_t size; + size_t tombstones; + float loadFactor; + + static constexpr size_t DEFAULT_CAPACITY = 16; + static constexpr float DEFAULT_LOAD_FACTOR = 0.7f; + static constexpr float MAX_LOAD_FACTOR = 1.0f; + static constexpr size_t MIN_CAPACITY = 1; + static constexpr size_t GROWTH_FACTOR = 2; + + /// Home slot of key in a table with tableCapacity slots. + static size_t slotFor(const Key& key, size_t tableCapacity) { + return std::hash{}(key) % tableCapacity; + } + + /// Home slot of key in the current table. + size_t hash(const Key& key) const { return slotFor(key, capacity); } + + /// Slot holding a live entry for key, or capacity if there is none. + size_t findSlot(const Key& key) const { + size_t index = hash(key); + for (size_t probes = 0; probes < capacity && table[index].occupied; + ++probes) { + if (!table[index].deleted && table[index].key == key) { + return index; } - return capacity; + index = (index + 1) % capacity; } - - /// Moves live entries into a newCapacity table, dropping tombstones. - void rehash(size_t newCapacity) - { - Entry* newTable = new Entry[newCapacity]; - - for (size_t i = 0; i < capacity; ++i) - { - if (table[i].occupied && !table[i].deleted) - { - size_t index = slotFor(table[i].key, newCapacity); - while (newTable[index].occupied) - { - index = (index + 1) % newCapacity; - } - newTable[index] = std::move(table[i]); + return capacity; + } + + /// Moves live entries into a newCapacity table, dropping tombstones. + void rehash(size_t newCapacity) { + Entry* newTable = new Entry[newCapacity]; + + for (size_t i = 0; i < capacity; ++i) { + if (table[i].occupied && !table[i].deleted) { + size_t index = slotFor(table[i].key, newCapacity); + while (newTable[index].occupied) { + index = (index + 1) % newCapacity; } + newTable[index] = std::move(table[i]); } - - delete[] table; - table = newTable; - capacity = newCapacity; - tombstones = 0; } - /// Grows if live entries fill the table, else purges tombstones only. - void resize() - { - const bool mostlyLive = - size * GROWTH_FACTOR >= capacity * loadFactor; - rehash(mostlyLive ? capacity * GROWTH_FACTOR : capacity); + delete[] table; + table = newTable; + capacity = newCapacity; + tombstones = 0; + } + + /// Grows if live entries fill the table, else purges tombstones only. + void resize() { + const bool mostlyLive = size * GROWTH_FACTOR >= capacity * loadFactor; + rehash(mostlyLive ? capacity * GROWTH_FACTOR : capacity); + } + + public: + HashTable(size_t initCapacity = DEFAULT_CAPACITY, + float loadFactor = DEFAULT_LOAD_FACTOR) + : capacity(initCapacity > MIN_CAPACITY ? initCapacity : MIN_CAPACITY), + size(0), + tombstones(0), + loadFactor(loadFactor > 0.0f && loadFactor <= MAX_LOAD_FACTOR + ? loadFactor + : DEFAULT_LOAD_FACTOR) { + table = new Entry[capacity]; + } + + /// Deep-copies other's slots, including its capacity and load factor. + HashTable(const HashTable& other) + : table(new Entry[other.capacity]), + capacity(other.capacity), + size(other.size), + tombstones(other.tombstones), + loadFactor(other.loadFactor) { + for (size_t i = 0; i < capacity; ++i) { + table[i] = other.table[i]; } - - public: - HashTable(size_t initCapacity = DEFAULT_CAPACITY, float loadFactor = DEFAULT_LOAD_FACTOR) - : capacity(initCapacity > MIN_CAPACITY ? initCapacity - : MIN_CAPACITY), - size(0), - tombstones(0), - loadFactor(loadFactor > 0.0f && loadFactor <= MAX_LOAD_FACTOR - ? loadFactor - : DEFAULT_LOAD_FACTOR) - { - table = new Entry[capacity]; - } - - /// Deep-copies other's slots, including its capacity and load factor. - HashTable(const HashTable& other) - : table(new Entry[other.capacity]), - capacity(other.capacity), - size(other.size), - tombstones(other.tombstones), - loadFactor(other.loadFactor) - { - for (size_t i = 0; i < capacity; ++i) - { - table[i] = other.table[i]; - } - } - - /// Replaces this table with a deep copy of other. - HashTable& operator=(const HashTable& other) - { - if (this != &other) - { - HashTable copy(other); - std::swap(table, copy.table); - std::swap(capacity, copy.capacity); - std::swap(size, copy.size); - std::swap(tombstones, copy.tombstones); - std::swap(loadFactor, copy.loadFactor); - } - return *this; + } + + /// Replaces this table with a deep copy of other. + HashTable& operator=(const HashTable& other) { + if (this != &other) { + HashTable copy(other); + std::swap(table, copy.table); + std::swap(capacity, copy.capacity); + std::swap(size, copy.size); + std::swap(tombstones, copy.tombstones); + std::swap(loadFactor, copy.loadFactor); } - - ~HashTable() - { - delete[] table; + return *this; + } + + ~HashTable() { delete[] table; } + + /// Inserts or updates the value for key, resizing first if over the load + /// factor. + void insert(const Key& key, const Value& value) { + const size_t existing = findSlot(key); + if (existing != capacity) { + table[existing].value = value; + return; } - /// Inserts or updates the value for key, resizing first if over the load factor. - void insert(const Key& key, const Value& value) - { - const size_t existing = findSlot(key); - if (existing != capacity) - { - table[existing].value = value; - return; - } - - if (size + tombstones >= capacity * loadFactor) - { - resize(); - } - - size_t index = hash(key); - while (table[index].occupied && !table[index].deleted) - { - index = (index + 1) % capacity; - } - - if (table[index].deleted) - { - --tombstones; - } - table[index].key = key; - table[index].value = value; - table[index].occupied = true; - table[index].deleted = false; - ++size; + if (size + tombstones >= capacity * loadFactor) { + resize(); } - /// Tombstones the entry for key; returns false if it wasn't present. - bool remove(const Key& key) - { - const size_t index = findSlot(key); - if (index == capacity) - { - return false; - } - table[index].key = Key(); - table[index].value = Value(); - table[index].deleted = true; - --size; - ++tombstones; - return true; + size_t index = hash(key); + while (table[index].occupied && !table[index].deleted) { + index = (index + 1) % capacity; } - /// Looks up key and writes its value into outValue; returns false if not found. - bool get(const Key& key, Value& outValue) const - { - const size_t index = findSlot(key); - if (index == capacity) - { - return false; - } - outValue = table[index].value; - return true; + if (table[index].deleted) { + --tombstones; } - - /// True if key currently has a live entry. - bool contains(const Key& key) const - { - return findSlot(key) != capacity; + table[index].key = key; + table[index].value = value; + table[index].occupied = true; + table[index].deleted = false; + ++size; + } + + /// Tombstones the entry for key; returns false if it wasn't present. + bool remove(const Key& key) { + const size_t index = findSlot(key); + if (index == capacity) { + return false; } - - /// Drops every entry and reallocates the table at its current capacity. - void clear() - { - Entry* emptyTable = new Entry[capacity]; - delete[] table; - table = emptyTable; - size = 0; - tombstones = 0; + table[index].key = Key(); + table[index].value = Value(); + table[index].deleted = true; + --size; + ++tombstones; + return true; + } + + /// Looks up key and writes its value into outValue; returns false if not + /// found. + bool get(const Key& key, Value& outValue) const { + const size_t index = findSlot(key); + if (index == capacity) { + return false; } - - size_t getSize() const { return size; } - size_t getCapacity() const { return capacity; } - }; + outValue = table[index].value; + return true; + } + + /// True if key currently has a live entry. + bool contains(const Key& key) const { return findSlot(key) != capacity; } + + /// Drops every entry and reallocates the table at its current capacity. + void clear() { + Entry* emptyTable = new Entry[capacity]; + delete[] table; + table = emptyTable; + size = 0; + tombstones = 0; + } + + size_t getSize() const { return size; } + size_t getCapacity() const { return capacity; } +}; } #endif // _HASH_TABLE_H_ diff --git a/include/public/Utility/Container/Queue.hpp b/include/public/Utility/Container/Queue.hpp index 962bedd0..a4b6fad6 100644 --- a/include/public/Utility/Container/Queue.hpp +++ b/include/public/Utility/Container/Queue.hpp @@ -1,152 +1,146 @@ #ifndef _QUEUE_H_ #define _QUEUE_H_ -#include "List.hpp" #include #include #include +#include "List.hpp" + namespace Sleak { - - /** - * @class Queue - * @brief Implements a FIFO (First-In, First-Out) queue data structure. - * - * The Queue class maintains a collection of elements in the order they were added. It - * is suitable for scenarios where you need to process elements in a first-come, - * first-served manner. Use this class when you need to implement task queues, - * message queues, or breadth-first search algorithms. - * - * Example Use Cases: - * - Implementing a task queue for background processing. - * - Managing message queues in a network communication system. - * - Performing breadth-first search in graph algorithms. - * - Simulating real-world queues, such as waiting lines. - * - * Implementation Details: - * - Uses an underlying List plus a head index; pop is amortized O(1). - * - Provides methods for enqueueing (adding) and dequeueing (removing) elements. - * - Maintains the FIFO order of elements. - * @ingroup utility - */ - template - class Queue { - public: - Queue() = default; - Queue(const Queue&) = default; - Queue& operator=(const Queue&) = default; - - /// Takes other's elements, leaving it empty. - Queue(Queue&& other) noexcept - : data(std::move(other.data)), head(std::exchange(other.head, 0)) {} - - /// Replaces this queue with other's elements, leaving other empty. - Queue& operator=(Queue&& other) noexcept { - if (this != &other) { - data = std::move(other.data); - head = std::exchange(other.head, 0); - } - return *this; - } - - /// Adds value to the back of the queue. - void push(const T& value) { - data.add(value); - } - - /// Moves value onto the back of the queue. - void push(T&& value) { - data.add(std::move(value)); - } - - /// Removes and returns the front element; throws if empty. - T pop() { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - - T* slots = data.GetData(); - T front = std::move(slots[head]); - slots[head] = T(); - ++head; - compactIfSparse(); - - return front; - } - - /// Front element without removing it; throws if empty. - T& front() { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - return data.GetData()[head]; - } - - const T& front() const { - if (isEmpty()) { - throw Sleak::EmptyContainerException(); - } - return data.GetData()[head]; - } - - /// Reverses the queue's element order in place. - const void reverse() { - std::reverse(begin(), end()); - } - - bool isEmpty() const { - return size() == 0; - } - - size_t size() const { return data.GetSize() - head; } - - void clear() { - data.clear(); - head = 0; - } - - /// Copies out the queued elements, front-to-back. - List GetData() { - List out; - out.resize(size()); - for (const T& value : *this) { - out.add(value); - } - return out; - } - - // Iterators - T* begin() { return data.begin() + head; } - const T* begin() const { return data.begin() + head; } - T* end() { return data.end(); } - const T* end() const { return data.end(); } - - private: - /// Slides live elements to the front once popped slots are the majority. - void compactIfSparse() { - const size_t live = size(); - if (head < live) { - return; - } - if (live == 0) { - clear(); - return; - } - - T* slots = data.GetData(); - for (size_t i = 0; i < live; ++i) { - slots[i] = std::move(slots[head + i]); - } - while (data.GetSize() > live) { - data.erase(data.GetSize() - 1); - } - head = 0; - } - - List data; - size_t head = 0; - }; + +/** + * @class Queue + * @brief Implements a FIFO (First-In, First-Out) queue data structure. + * + * The Queue class maintains a collection of elements in the order they were + * added. It is suitable for scenarios where you need to process elements in a + * first-come, first-served manner. Use this class when you need to implement + * task queues, message queues, or breadth-first search algorithms. + * + * Example Use Cases: + * - Implementing a task queue for background processing. + * - Managing message queues in a network communication system. + * - Performing breadth-first search in graph algorithms. + * - Simulating real-world queues, such as waiting lines. + * + * Implementation Details: + * - Uses an underlying List plus a head index; pop is amortized O(1). + * - Provides methods for enqueueing (adding) and dequeueing (removing) + * elements. + * - Maintains the FIFO order of elements. + * @ingroup utility + */ +template +class Queue { + public: + Queue() = default; + Queue(const Queue&) = default; + Queue& operator=(const Queue&) = default; + + /// Takes other's elements, leaving it empty. + Queue(Queue&& other) noexcept + : data(std::move(other.data)), head(std::exchange(other.head, 0)) {} + + /// Replaces this queue with other's elements, leaving other empty. + Queue& operator=(Queue&& other) noexcept { + if (this != &other) { + data = std::move(other.data); + head = std::exchange(other.head, 0); + } + return *this; + } + + /// Adds value to the back of the queue. + void push(const T& value) { data.add(value); } + + /// Moves value onto the back of the queue. + void push(T&& value) { data.add(std::move(value)); } + + /// Removes and returns the front element; throws if empty. + T pop() { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + + T* slots = data.GetData(); + T front = std::move(slots[head]); + slots[head] = T(); + ++head; + compactIfSparse(); + + return front; + } + + /// Front element without removing it; throws if empty. + T& front() { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + return data.GetData()[head]; + } + + const T& front() const { + if (isEmpty()) { + throw Sleak::EmptyContainerException(); + } + return data.GetData()[head]; + } + + /// Reverses the queue's element order in place. + const void reverse() { std::reverse(begin(), end()); } + + bool isEmpty() const { return size() == 0; } + + size_t size() const { return data.GetSize() - head; } + + void clear() { + data.clear(); + head = 0; + } + + /// Copies out the queued elements, front-to-back. + List GetData() { + List out; + out.resize(size()); + for (const T& value : *this) { + out.add(value); + } + return out; + } + + // Iterators + T* begin() { return data.begin() + head; } + const T* begin() const { return data.begin() + head; } + T* end() { return data.end(); } + const T* end() const { return data.end(); } + + private: + /// Slides live elements to the front once popped slots are the majority. + void compactIfSparse() { + const size_t live = size(); + if (head < live) { + return; + } + if (live == 0) { + clear(); + return; + } + + T* slots = data.GetData(); + for (size_t i = 0; i < live; ++i) { + slots[i] = std::move(slots[head + i]); + } + while (data.GetSize() > live) { + data.erase(data.GetSize() - 1); + } + head = 0; + } + + List data; + size_t head = 0; +}; } #endif // _QUEUE_H_ \ No newline at end of file diff --git a/src/Animation/AnimatorComponent.cpp b/src/Animation/AnimatorComponent.cpp index 5bec552d..374f8add 100644 --- a/src/Animation/AnimatorComponent.cpp +++ b/src/Animation/AnimatorComponent.cpp @@ -1,10 +1,10 @@ +#include +#include #include #include -#include -#include #include +#include #include -#include #include #include @@ -224,8 +224,7 @@ void AnimatorComponent::ProcessNodeHierarchy(int nodeIndex, /// Finds the keyframe pair straddling time and the lerp factor between them. template static std::pair FindKeyframe(const std::vector>& keys, float time) { - if (keys.size() < 2) - return {0, 0.0f}; + if (keys.size() < 2) return {0, 0.0f}; // First key in [1, size - 1) later than time; the last key otherwise auto upper = std::upper_bound( diff --git a/src/Assets/ModelLoader.cpp b/src/Assets/ModelLoader.cpp index c4637ea2..ea014cdc 100644 --- a/src/Assets/ModelLoader.cpp +++ b/src/Assets/ModelLoader.cpp @@ -68,11 +68,11 @@ GameObject* ModelLoader::Load(const std::string& filePath, // Static models also get PreTransformVertices, in the same pipeline order unsigned int postFlags = baseFlags; - if (!hasAnimations) - postFlags |= aiProcess_PreTransformVertices; + if (!hasAnimations) postFlags |= aiProcess_PreTransformVertices; scene = importer.ApplyPostProcessing(postFlags); - if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || !scene->mRootNode) { + if (!scene || (scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE) || + !scene->mRootNode) { SLEAK_ERROR("Assimp: Failed to post-process model '{}': {}", filePath, importer.GetErrorString()); return nullptr; diff --git a/src/Core/Application.cpp b/src/Core/Application.cpp index 27162d97..00fa941c 100644 --- a/src/Core/Application.cpp +++ b/src/Core/Application.cpp @@ -289,10 +289,10 @@ namespace Sleak { if (queue && context) queue->ExecuteCommands(context); - m_cpuFrameMs = std::chrono::duration( - std::chrono::steady_clock::now() - - cpuFrameStart) - .count(); + m_cpuFrameMs = + std::chrono::duration( + std::chrono::steady_clock::now() - cpuFrameStart) + .count(); renderer->EndRender(); if (captureArmed && !renderer->IsFrameCapturePending()) diff --git a/src/Culling/CullingSystem.cpp b/src/Culling/CullingSystem.cpp index e576e189..fec8f9a7 100644 --- a/src/Culling/CullingSystem.cpp +++ b/src/Culling/CullingSystem.cpp @@ -1,5 +1,4 @@ #include - #include #include #include diff --git a/src/Graphics/Common/RenderCommandQueue.cpp b/src/Graphics/Common/RenderCommandQueue.cpp index 1f51bdb5..2696d507 100644 --- a/src/Graphics/Common/RenderCommandQueue.cpp +++ b/src/Graphics/Common/RenderCommandQueue.cpp @@ -1,12 +1,14 @@ #include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" -#include "../../include/private/Graphics/Common/BufferBase.hpp" -#include "../../include/private/Graphics/Common/RenderContext.hpp" -#include "../../include/private/Graphics/Common/RenderCommands.hpp" -#include -#include + #include +#include +#include #include +#include "../../include/private/Graphics/Common/BufferBase.hpp" +#include "../../include/private/Graphics/Common/RenderCommands.hpp" +#include "../../include/private/Graphics/Common/RenderContext.hpp" + namespace Sleak { namespace RenderEngine { RenderCommandQueue* RenderCommandQueue::Instance = nullptr; @@ -34,11 +36,13 @@ namespace Sleak { } void RenderCommandQueue::SubmitBindConstantBuffer(RefPtr buffer, uint8_t slot) { - CurrentArena().Create(std::move(buffer), slot); + CurrentArena().Create(std::move(buffer), + slot); } void RenderCommandQueue::SubmitUpdateConstantBuffer(RefPtr buffer, void* Data, uint16_t Size) { - CurrentArena().Create(std::move(buffer), Data, Size); + CurrentArena().Create( + std::move(buffer), Data, Size); } void RenderCommandQueue::SubmitBindMaterial(::Sleak::Material* material) { @@ -73,15 +77,18 @@ namespace Sleak { for (RenderCommandBase* cmd : commands) { auto type = cmd->GetType(); if (type == CommandType::BindConstantBuffer) { - auto* bindCmd = static_cast(cmd); + auto* bindCmd = + static_cast(cmd); if (bindCmd->GetSlot() == 0) { lastSlot0Buffer = &bindCmd->GetBuffer(); } else if (bindCmd->GetSlot() == 3) { lastSlot3Buffer = &bindCmd->GetBuffer(); } } - if ((type == CommandType::Draw || type == CommandType::DrawIndexed) - && lastSlot0Buffer && *lastSlot0Buffer && cmd->CastsShadow()) { + if ((type == CommandType::Draw || + type == CommandType::DrawIndexed) && + lastSlot0Buffer && *lastSlot0Buffer && + cmd->CastsShadow()) { ShadowDrawEntry entry; entry.command = cmd; entry.transformBuffer = lastSlot0Buffer; @@ -135,7 +142,9 @@ namespace Sleak { if (type == CommandType::CustomCommand) { // Skybox, debug lines — always rendered after the lighting pass. // Flush any pending state to forward group first so state is correct. - forwardCmds.insert(forwardCmds.end(), pendingState.begin(), pendingState.end()); + forwardCmds.insert(forwardCmds.end(), + pendingState.begin(), + pendingState.end()); pendingState.clear(); forwardCmds.push_back(cmd); lastMaterial = nullptr; @@ -155,7 +164,9 @@ namespace Sleak { const bool isSkinned = cmd->IsSkinned(); if (lastMaterialForward || isSkinned) { // Transparent/forward/skinned draw — defer to forward pass - forwardCmds.insert(forwardCmds.end(), pendingState.begin(), pendingState.end()); + forwardCmds.insert(forwardCmds.end(), + pendingState.begin(), + pendingState.end()); forwardCmds.push_back(cmd); } else { // Opaque draw — execute now in geometry pass @@ -174,17 +185,16 @@ namespace Sleak { context->ExecuteDeferredLightingPass(); context->BeginForwardTransparentPass(); - for (RenderCommandBase* cmd : forwardCmds) - cmd->Execute(context); - context->EndForwardTransparentPass(); + for (RenderCommandBase* cmd : forwardCmds) + cmd->Execute(context); + context->EndForwardTransparentPass(); - AdvanceArena(); - return; + AdvanceArena(); + return; } // ---- Forward rendering path (unchanged behavior) ---- - for (RenderCommandBase* cmd : commands) - cmd->Execute(context); + for (RenderCommandBase* cmd : commands) cmd->Execute(context); AdvanceArena(); } @@ -217,16 +227,13 @@ namespace Sleak { // TODO: Implement this method } - void RenderCommandQueue::Clear() { - CurrentArena().Reset(); - } + void RenderCommandQueue::Clear() { CurrentArena().Reset(); } void RenderCommandQueue::ClearAll() { cachedShadowDraws.clear(); m_pendingState.clear(); m_forwardCmds.clear(); - for (CommandArena& arena : m_arenas) - arena.Reset(); + for (CommandArena& arena : m_arenas) arena.Reset(); m_currentArena = 0; } diff --git a/src/Graphics/Common/RenderCommands.cpp b/src/Graphics/Common/RenderCommands.cpp index 1ba2b040..e2fd49e1 100644 --- a/src/Graphics/Common/RenderCommands.cpp +++ b/src/Graphics/Common/RenderCommands.cpp @@ -110,11 +110,9 @@ bool DrawIndexedCommand::IsSkinned() const { } /// Copies the payload into inline storage, falling back to a heap allocation past INLINE_CAPACITY. -UpdateConstantBufferCommand::UpdateConstantBufferCommand(RefPtr buffer, - void* Data, - uint16_t Size) : - constantBuffer(std::move(buffer)), - Size(Size) { +UpdateConstantBufferCommand::UpdateConstantBufferCommand( + RefPtr buffer, void* Data, uint16_t Size) + : constantBuffer(std::move(buffer)), Size(Size) { if (Size <= INLINE_CAPACITY) { // Fast path: no heap allocation for small buffers memcpy(m_inlineData, Data, Size); diff --git a/src/Graphics/DirectX11/DirectX11Renderer.cpp b/src/Graphics/DirectX11/DirectX11Renderer.cpp index 7a41e98d..06a1cd13 100644 --- a/src/Graphics/DirectX11/DirectX11Renderer.cpp +++ b/src/Graphics/DirectX11/DirectX11Renderer.cpp @@ -48,8 +48,8 @@ namespace Sleak { for (int c = 0; c < MATRIX_DIM; ++c) { float sum = 0.0f; for (int k = 0; k < MATRIX_DIM; ++k) { - sum += world[r * MATRIX_DIM + k] * - lightVP[k * MATRIX_DIM + c]; + sum += + world[r * MATRIX_DIM + k] * lightVP[k * MATRIX_DIM + c]; } out[r * MATRIX_DIM + c] = sum; } @@ -654,8 +654,8 @@ void DirectX11Renderer::BindConstantBuffer(RefPtr buffer, desc.Usage = D3D11_USAGE_DYNAMIC; desc.BindFlags = D3D11_BIND_CONSTANT_BUFFER; desc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; - if (FAILED(device->CreateBuffer(&desc, nullptr, - &m_shadowTransformCB))) + if (FAILED( + device->CreateBuffer(&desc, nullptr, &m_shadowTransformCB))) return; m_shadowCBCacheValid = false; } @@ -754,12 +754,11 @@ ID3D11RasterizerState* DirectX11Renderer::GetRasterState( ID3D11DepthStencilState* DirectX11Renderer::GetDepthState( const D3D11_DEPTH_STENCIL_DESC& desc) { - return FindOrCreateState( - m_depthStates, desc, - [this](const D3D11_DEPTH_STENCIL_DESC* d, - ID3D11DepthStencilState** out) { - return device->CreateDepthStencilState(d, out); - }); + return FindOrCreateState(m_depthStates, desc, + [this](const D3D11_DEPTH_STENCIL_DESC* d, + ID3D11DepthStencilState** out) { + return device->CreateDepthStencilState(d, out); + }); } void DirectX11Renderer::SetRenderMode(RenderMode mode) { @@ -862,7 +861,7 @@ void DirectX11Renderer::ApplyDepthState() { D3D11_DEPTH_STENCIL_DESC desc = {}; desc.DepthEnable = TRUE; desc.DepthWriteMask = m_depthWriteEnabled ? D3D11_DEPTH_WRITE_MASK_ALL - : D3D11_DEPTH_WRITE_MASK_ZERO; + : D3D11_DEPTH_WRITE_MASK_ZERO; desc.DepthFunc = m_depthFunc; desc.StencilEnable = FALSE; diff --git a/src/Graphics/DirectX12/DirectX12Renderer.cpp b/src/Graphics/DirectX12/DirectX12Renderer.cpp index 0befa0e6..b5655bba 100644 --- a/src/Graphics/DirectX12/DirectX12Renderer.cpp +++ b/src/Graphics/DirectX12/DirectX12Renderer.cpp @@ -1244,9 +1244,8 @@ void DirectX12Renderer::BindConstantBuffer(RefPtr buffer, TRANSFORM_WORLD_OFFSET); // Casters sharing a World reuse this pass's ring copy - if (!m_shadowCBAddress || - memcmp(srcWorld, m_shadowWorldCache, - sizeof(m_shadowWorldCache)) != 0) { + if (!m_shadowCBAddress || memcmp(srcWorld, m_shadowWorldCache, + sizeof(m_shadowWorldCache)) != 0) { DirectX12UploadRing::Allocation alloc; if (!m_uploadRing.Allocate(TRANSFORM_CB_BYTES, alloc)) return; BuildShadowTransform(srcWorld, m_lightVP, @@ -1254,8 +1253,7 @@ void DirectX12Renderer::BindConstantBuffer(RefPtr buffer, memcpy(m_shadowWorldCache, srcWorld, sizeof(m_shadowWorldCache)); m_shadowCBAddress = alloc.gpu; } - commandList->SetGraphicsRootConstantBufferView(slot, - m_shadowCBAddress); + commandList->SetGraphicsRootConstantBufferView(slot, m_shadowCBAddress); return; } diff --git a/src/Graphics/Vulkan/VulkanDescriptors.cpp b/src/Graphics/Vulkan/VulkanDescriptors.cpp index b493e6a3..e8060e45 100644 --- a/src/Graphics/Vulkan/VulkanDescriptors.cpp +++ b/src/Graphics/Vulkan/VulkanDescriptors.cpp @@ -1,11 +1,11 @@ -#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" -#include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" - #include #include #include #include #include + +#include "../../include/private/Graphics/Vulkan/VulkanRenderer.hpp" +#include "../../include/private/Graphics/Vulkan/VulkanTexture.hpp" #include "Core/Logger.hpp" namespace Sleak { @@ -703,11 +703,14 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { Texture* textures[PBR_TEXTURE_COUNT] = { material->GetDiffuseTexture(), - material->HasNormalTexture() ? material->GetNormalTexture() : nullptr, - material->HasMetallicTexture() ? material->GetMetallicTexture() : nullptr, - material->HasRoughnessTexture() ? material->GetRoughnessTexture() : nullptr, - material->HasAOTexture() ? material->GetAOTexture() : nullptr, - material->HasEmissiveTexture() ? material->GetEmissiveTexture() : nullptr, + material->HasNormalTexture() ? material->GetNormalTexture() : nullptr, + material->HasMetallicTexture() ? material->GetMetallicTexture() + : nullptr, + material->HasRoughnessTexture() ? material->GetRoughnessTexture() + : nullptr, + material->HasAOTexture() ? material->GetAOTexture() : nullptr, + material->HasEmissiveTexture() ? material->GetEmissiveTexture() + : nullptr, }; std::array imageInfos{}; @@ -722,17 +725,15 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { auto cached = bindings.find(material); if (cached != bindings.end() && memcmp(&cached->second.params, ¶ms, sizeof(params)) == 0 && - std::equal(imageInfos.begin(), imageInfos.end(), - cached->second.images.begin(), - [](const VkDescriptorImageInfo& a, - const VkDescriptorImageInfo& b) { - return a.sampler == b.sampler && - a.imageView == b.imageView && - a.imageLayout == b.imageLayout; - })) { + std::equal( + imageInfos.begin(), imageInfos.end(), cached->second.images.begin(), + [](const VkDescriptorImageInfo& a, const VkDescriptorImageInfo& b) { + return a.sampler == b.sampler && a.imageView == b.imageView && + a.imageLayout == b.imageLayout; + })) { vkCmdBindDescriptorSets(command, VK_PIPELINE_BIND_POINT_GRAPHICS, - m_gbufferGeomLayout, 0, 1, - &cached->second.set, 0, nullptr); + m_gbufferGeomLayout, 0, 1, &cached->second.set, + 0, nullptr); return; } @@ -766,13 +767,14 @@ void VulkanRenderer::BindPBRMaterial(Sleak::Material* material) { writes[i].descriptorCount = 1; writes[i].pImageInfo = &imageInfos[i]; } - writes[PBR_TEXTURE_COUNT].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + writes[PBR_TEXTURE_COUNT].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; writes[PBR_TEXTURE_COUNT].dstSet = set; - writes[PBR_TEXTURE_COUNT].dstBinding = PBR_TEXTURE_COUNT; + writes[PBR_TEXTURE_COUNT].dstBinding = PBR_TEXTURE_COUNT; writes[PBR_TEXTURE_COUNT].dstArrayElement = 0; - writes[PBR_TEXTURE_COUNT].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + writes[PBR_TEXTURE_COUNT].descriptorType = + VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; writes[PBR_TEXTURE_COUNT].descriptorCount = 1; - writes[PBR_TEXTURE_COUNT].pBufferInfo = &bufInfo; + writes[PBR_TEXTURE_COUNT].pBufferInfo = &bufInfo; vkUpdateDescriptorSets(device, static_cast(writes.size()), writes.data(), 0, nullptr); diff --git a/src/Physics/CollisionDetection.cpp b/src/Physics/CollisionDetection.cpp index 84b32700..398a8e86 100644 --- a/src/Physics/CollisionDetection.cpp +++ b/src/Physics/CollisionDetection.cpp @@ -384,12 +384,12 @@ CollisionManifold TestCollision(const ColliderShape& shapeA, const Vector3D& pos return TestCapsuleVsCapsule(TransformCapsule(a, posA, scaleA), TransformCapsule(b, posB, scaleB)); } else if constexpr (std::is_same_v && std::is_same_v) { - return TestSphereVsPlacedMesh(TransformSphere(a, posA, scaleA), - b, posB, scaleB); + return TestSphereVsPlacedMesh(TransformSphere(a, posA, scaleA), b, + posB, scaleB); } else if constexpr (std::is_same_v && std::is_same_v) { - auto m = TestSphereVsPlacedMesh(TransformSphere(b, posB, scaleB), - a, posA, scaleA); + auto m = TestSphereVsPlacedMesh(TransformSphere(b, posB, scaleB), a, + posA, scaleA); m.contact.normal = m.contact.normal * -1.0f; return m; } diff --git a/src/Physics/PhysicsWorld.cpp b/src/Physics/PhysicsWorld.cpp index 96552521..0877e06c 100644 --- a/src/Physics/PhysicsWorld.cpp +++ b/src/Physics/PhysicsWorld.cpp @@ -1,10 +1,10 @@ -#include -#include -#include -#include #include -#include +#include #include +#include +#include +#include +#include #include #include @@ -156,8 +156,8 @@ void PhysicsWorld::FindPairsAndResolve() { AABB worldA = colliderA->GetWorldAABB(); m_tree.Query(worldA, [&](int proxyId) -> bool { - auto* colliderB = static_cast( - m_tree.GetUserData(proxyId)); + auto* colliderB = + static_cast(m_tree.GetUserData(proxyId)); if (colliderB == colliderA) return true; // Movable pairs are visited from both sides: keep pointer order @@ -165,8 +165,10 @@ void PhysicsWorld::FindPairsAndResolve() { if (rbB && colliderA > colliderB) return true; // Layer filtering - if ((colliderA->GetLayer() & colliderB->GetMask()) == 0) return true; - if ((colliderB->GetLayer() & colliderA->GetMask()) == 0) return true; + if ((colliderA->GetLayer() & colliderB->GetMask()) == 0) + return true; + if ((colliderB->GetLayer() & colliderA->GetMask()) == 0) + return true; // Skip if both are triggers if (colliderA->IsTrigger() && colliderB->IsTrigger()) return true; @@ -185,9 +187,9 @@ void PhysicsWorld::FindPairsAndResolve() { GetColliderPose(first, posFirst, scaleFirst); GetColliderPose(second, posSecond, scaleSecond); - CollisionManifold manifold = TestCollision( - first->GetShape(), posFirst, scaleFirst, - second->GetShape(), posSecond, scaleSecond); + CollisionManifold manifold = + TestCollision(first->GetShape(), posFirst, scaleFirst, + second->GetShape(), posSecond, scaleSecond); if (!manifold.hasCollision) return true; diff --git a/src/Scene/GameObject.cpp b/src/Scene/GameObject.cpp index 7ff07082..2c382f1e 100644 --- a/src/Scene/GameObject.cpp +++ b/src/Scene/GameObject.cpp @@ -165,7 +165,8 @@ namespace Sleak { if (!component) continue; if (!m_cachedTransform) - m_cachedTransform = dynamic_cast(component); + m_cachedTransform = + dynamic_cast(component); if (!m_cachedMaterial) m_cachedMaterial = dynamic_cast(component); if (!m_cachedMesh) diff --git a/src/Scene/MaterialComponent.cpp b/src/Scene/MaterialComponent.cpp index 7c3444a5..7c2cd6c0 100644 --- a/src/Scene/MaterialComponent.cpp +++ b/src/Scene/MaterialComponent.cpp @@ -1,6 +1,6 @@ +#include #include #include -#include #include namespace Sleak { diff --git a/src/Scene/MeshComponent.cpp b/src/Scene/MeshComponent.cpp index fdc8022f..9cca42b3 100644 --- a/src/Scene/MeshComponent.cpp +++ b/src/Scene/MeshComponent.cpp @@ -1,40 +1,41 @@ -#include "../../include/private/Graphics/Common/ResourceManager.hpp" -#include "../../include/private/Graphics/Common/BufferBase.hpp" -#include "../../include/private/Graphics/Common/ConstantBuffer.hpp" -#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" -#include -#include -#include #include #include +#include +#include +#include #include #include +#include "../../include/private/Graphics/Common/BufferBase.hpp" +#include "../../include/private/Graphics/Common/ConstantBuffer.hpp" +#include "../../include/private/Graphics/Common/RenderCommandQueue.hpp" +#include "../../include/private/Graphics/Common/ResourceManager.hpp" + namespace Sleak { namespace { - /// World AABB of a local box under m (world = local * m), via Arvo's method. - Math::AABB TransformBounds(const Math::AABB& box, const Math::Matrix4& m) { - const Math::Vector3D c = box.Center(); - const Math::Vector3D e = box.Extents(); - const float cx = c.GetX(), cy = c.GetY(), cz = c.GetZ(); - const float ex = std::fabs(e.GetX()); - const float ey = std::fabs(e.GetY()); - const float ez = std::fabs(e.GetZ()); - - const Math::Vector3D center( - cx * m(0, 0) + cy * m(1, 0) + cz * m(2, 0) + m(3, 0), - cx * m(0, 1) + cy * m(1, 1) + cz * m(2, 1) + m(3, 1), - cx * m(0, 2) + cy * m(1, 2) + cz * m(2, 2) + m(3, 2)); - const Math::Vector3D extents( - ex * std::fabs(m(0, 0)) + ey * std::fabs(m(1, 0)) + - ez * std::fabs(m(2, 0)), - ex * std::fabs(m(0, 1)) + ey * std::fabs(m(1, 1)) + - ez * std::fabs(m(2, 1)), - ex * std::fabs(m(0, 2)) + ey * std::fabs(m(1, 2)) + - ez * std::fabs(m(2, 2))); - return Math::AABB(center - extents, center + extents); - } +/// World AABB of a local box under m (world = local * m), via Arvo's method. +Math::AABB TransformBounds(const Math::AABB& box, const Math::Matrix4& m) { + const Math::Vector3D c = box.Center(); + const Math::Vector3D e = box.Extents(); + const float cx = c.GetX(), cy = c.GetY(), cz = c.GetZ(); + const float ex = std::fabs(e.GetX()); + const float ey = std::fabs(e.GetY()); + const float ez = std::fabs(e.GetZ()); + + const Math::Vector3D center( + cx * m(0, 0) + cy * m(1, 0) + cz * m(2, 0) + m(3, 0), + cx * m(0, 1) + cy * m(1, 1) + cz * m(2, 1) + m(3, 1), + cx * m(0, 2) + cy * m(1, 2) + cz * m(2, 2) + m(3, 2)); + const Math::Vector3D extents( + ex * std::fabs(m(0, 0)) + ey * std::fabs(m(1, 0)) + + ez * std::fabs(m(2, 0)), + ex * std::fabs(m(0, 1)) + ey * std::fabs(m(1, 1)) + + ez * std::fabs(m(2, 1)), + ex * std::fabs(m(0, 2)) + ey * std::fabs(m(1, 2)) + + ez * std::fabs(m(2, 2))); + return Math::AABB(center - extents, center + extents); +} } // namespace MeshComponent::MeshComponent(GameObject* object, MeshData data) : Component(object) { diff --git a/src/Scene/TransformComponent.cpp b/src/Scene/TransformComponent.cpp index c54de7b1..ab866ddf 100644 --- a/src/Scene/TransformComponent.cpp +++ b/src/Scene/TransformComponent.cpp @@ -56,9 +56,7 @@ namespace Sleak { RenderEngine::RenderCommandQueue::GetInstance()->SubmitBindConstantBuffer(ConstantBuffer, 0); } - Matrix4 TransformComponent::GetTransformMatrix() { - return WorldMatrix(); - } + Matrix4 TransformComponent::GetTransformMatrix() { return WorldMatrix(); } const Matrix4& TransformComponent::WorldMatrix() { if (m_worldDirty) { @@ -167,8 +165,7 @@ namespace Sleak { Math::Matrix4 TransformComponent::GetMVP() { - auto mat = WorldMatrix() * - Camera::GetMainViewMatrix() * + auto mat = WorldMatrix() * Camera::GetMainViewMatrix() * Camera::GetMainProjectionMatrix(); return mat;