From 1a635f16abf73e076088487f07bc4a289525a709 Mon Sep 17 00:00:00 2001 From: Sascha Willems Date: Wed, 13 Dec 2023 18:31:49 +0100 Subject: [PATCH] Use shader storage buffer to pass lights to shader --- examples/subpasses/subpasses.cpp | 63 +++++++------------- shaders/glsl/subpasses/composition.frag | 29 ++++----- shaders/glsl/subpasses/composition.frag.spv | Bin 3152 -> 3152 bytes shaders/hlsl/subpasses/composition.frag | 56 ++++++----------- shaders/hlsl/subpasses/composition.frag.spv | Bin 3792 -> 2236 bytes 5 files changed, 53 insertions(+), 95 deletions(-) diff --git a/examples/subpasses/subpasses.cpp b/examples/subpasses/subpasses.cpp index 8486c353..61209f37 100644 --- a/examples/subpasses/subpasses.cpp +++ b/examples/subpasses/subpasses.cpp @@ -21,8 +21,6 @@ #define ENABLE_VALIDATION false -#define NUM_LIGHTS 64 - class VulkanExample : public VulkanExampleBase { public: @@ -47,14 +45,12 @@ public: float radius; }; - struct { - Light lights[NUM_LIGHTS]; - } uboLights; + std::array lights; struct { vks::Buffer GBuffer; vks::Buffer lights; - } uniformBuffers; + } buffers; struct { VkPipeline offscreen; @@ -128,8 +124,8 @@ public: clearAttachment(&attachments.albedo); textures.glass.destroy(); - uniformBuffers.GBuffer.destroy(); - uniformBuffers.lights.destroy(); + buffers.GBuffer.destroy(); + buffers.lights.destroy(); } // Enable physical device features required for this example @@ -526,6 +522,7 @@ public: std::vector poolSizes = { vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 4), + vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1), vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 4), vks::initializers::descriptorPoolSize(VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 4), }; @@ -556,7 +553,7 @@ public: VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSets.scene)); writeDescriptorSets = { // Binding 0: Vertex shader uniform buffer - vks::initializers::writeDescriptorSet(descriptorSets.scene, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBuffers.GBuffer.descriptor) + vks::initializers::writeDescriptorSet(descriptorSets.scene, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &buffers.GBuffer.descriptor) }; vkUpdateDescriptorSets(device, static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, NULL); } @@ -617,7 +614,7 @@ public: // Binding 2: Albedo input attachment vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, VK_SHADER_STAGE_FRAGMENT_BIT, 2), // Binding 3: Light positions - vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 3), + vks::initializers::descriptorSetLayoutBinding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_FRAGMENT_BIT, 3), }; VkDescriptorSetLayoutCreateInfo descriptorLayout = @@ -648,7 +645,7 @@ public: // Binding 2: Albedo texture target vks::initializers::writeDescriptorSet(descriptorSets.composition, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 2, &texDescriptorAlbedo), // Binding 4: Fragment shader lights - vks::initializers::writeDescriptorSet(descriptorSets.composition, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 3, &uniformBuffers.lights.descriptor), + vks::initializers::writeDescriptorSet(descriptorSets.composition, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 3, &buffers.lights.descriptor), }; vkUpdateDescriptorSets(device, static_cast(writeDescriptorSets.size()), writeDescriptorSets.data(), 0, NULL); @@ -668,22 +665,6 @@ public: shaderStages[0] = loadShader(getShadersPath() + "subpasses/composition.vert.spv", VK_SHADER_STAGE_VERTEX_BIT); shaderStages[1] = loadShader(getShadersPath() + "subpasses/composition.frag.spv", VK_SHADER_STAGE_FRAGMENT_BIT); - // Use specialization constants to pass number of lights to the shader - VkSpecializationMapEntry specializationEntry{}; - specializationEntry.constantID = 0; - specializationEntry.offset = 0; - specializationEntry.size = sizeof(uint32_t); - - uint32_t specializationData = NUM_LIGHTS; - - VkSpecializationInfo specializationInfo; - specializationInfo.mapEntryCount = 1; - specializationInfo.pMapEntries = &specializationEntry; - specializationInfo.dataSize = sizeof(specializationData); - specializationInfo.pData = &specializationData; - - shaderStages[1].pSpecializationInfo = &specializationInfo; - VkGraphicsPipelineCreateInfo pipelineCI = vks::initializers::pipelineCreateInfo(pipelineLayouts.composition, renderPass, 0); VkPipelineVertexInputStateCreateInfo emptyInputState{}; @@ -727,7 +708,7 @@ public: VK_CHECK_RESULT(vkAllocateDescriptorSets(device, &allocInfo, &descriptorSets.transparent)); writeDescriptorSets = { - vks::initializers::writeDescriptorSet(descriptorSets.transparent, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &uniformBuffers.GBuffer.descriptor), + vks::initializers::writeDescriptorSet(descriptorSets.transparent, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 0, &buffers.GBuffer.descriptor), vks::initializers::writeDescriptorSet(descriptorSets.transparent, VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 1, &texDescriptorPosition), vks::initializers::writeDescriptorSet(descriptorSets.transparent, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 2, &textures.glass.descriptor), }; @@ -755,13 +736,13 @@ public: // Prepare and initialize uniform buffer containing shader uniforms void prepareUniformBuffers() { - // Deferred vertex shader - vulkanDevice->createBuffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &uniformBuffers.GBuffer, sizeof(uboGBuffer)); - VK_CHECK_RESULT(uniformBuffers.GBuffer.map()); + // Matrices + vulkanDevice->createBuffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &buffers.GBuffer, sizeof(uboGBuffer)); + VK_CHECK_RESULT(buffers.GBuffer.map()); - // Deferred fragment shader - vulkanDevice->createBuffer(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &uniformBuffers.lights, sizeof(uboLights)); - VK_CHECK_RESULT(uniformBuffers.lights.map()); + // Lights + vulkanDevice->createBuffer(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, &buffers.lights, lights.size() * sizeof(Light)); + VK_CHECK_RESULT(buffers.lights.map()); // Update updateUniformBufferDeferredMatrices(); @@ -772,7 +753,7 @@ public: uboGBuffer.projection = camera.matrices.perspective; uboGBuffer.view = camera.matrices.view; uboGBuffer.model = glm::mat4(1.0f); - memcpy(uniformBuffers.GBuffer.mapped, &uboGBuffer, sizeof(uboGBuffer)); + memcpy(buffers.GBuffer.mapped, &uboGBuffer, sizeof(uboGBuffer)); } void initLights() @@ -786,18 +767,20 @@ public: glm::vec3(1.0f, 1.0f, 0.0f), }; - std::default_random_engine rndGen(benchmark.active ? 0 : (unsigned)time(nullptr)); + std::random_device rndDevice; + std::default_random_engine rndGen(benchmark.active ? 0 : rndDevice()); std::uniform_real_distribution rndDist(-1.0f, 1.0f); - std::uniform_int_distribution rndCol(0, static_cast(colors.size()-1)); + std::uniform_real_distribution rndCol(0.0f, 0.5f); - for (auto& light : uboLights.lights) + for (auto& light : lights) { light.position = glm::vec4(rndDist(rndGen) * 8.0f, 0.25f + std::abs(rndDist(rndGen)) * 4.0f, rndDist(rndGen) * 8.0f, 1.0f); - light.color = colors[rndCol(rndGen)]; + //light.color = colors[rndCol(rndGen)]; + light.color = glm::vec3(rndCol(rndGen), rndCol(rndGen), rndCol(rndGen)) * 2.0f; light.radius = 1.0f + std::abs(rndDist(rndGen)); } - memcpy(uniformBuffers.lights.mapped, &uboLights, sizeof(uboLights)); + memcpy(buffers.lights.mapped, lights.data(), lights.size() * sizeof(Light)); } void draw() diff --git a/shaders/glsl/subpasses/composition.frag b/shaders/glsl/subpasses/composition.frag index c060fe6f..0134c2a2 100644 --- a/shaders/glsl/subpasses/composition.frag +++ b/shaders/glsl/subpasses/composition.frag @@ -1,50 +1,47 @@ #version 450 -layout (input_attachment_index = 0, binding = 0) uniform subpassInput samplerposition; -layout (input_attachment_index = 1, binding = 1) uniform subpassInput samplerNormal; -layout (input_attachment_index = 2, binding = 2) uniform subpassInput samplerAlbedo; +layout (input_attachment_index = 0, binding = 0) uniform subpassInput inputPosition; +layout (input_attachment_index = 1, binding = 1) uniform subpassInput inputNormal; +layout (input_attachment_index = 2, binding = 2) uniform subpassInput inputAlbedo; layout (location = 0) in vec2 inUV; layout (location = 0) out vec4 outColor; -layout (constant_id = 0) const int NUM_LIGHTS = 64; - struct Light { vec4 position; vec3 color; float radius; }; -layout (binding = 3) uniform UBO +layout (std140, binding = 3) buffer LightsBuffer { - Light lights[NUM_LIGHTS]; -} ubo; - + Light lights[]; +}; void main() { // Read G-Buffer values from previous sub pass - vec3 fragPos = subpassLoad(samplerposition).rgb; - vec3 normal = subpassLoad(samplerNormal).rgb; - vec4 albedo = subpassLoad(samplerAlbedo); + vec3 fragPos = subpassLoad(inputPosition).rgb; + vec3 normal = subpassLoad(inputNormal).rgb; + vec4 albedo = subpassLoad(inputAlbedo); #define ambient 0.05 // Ambient part vec3 fragcolor = albedo.rgb * ambient; - for(int i = 0; i < NUM_LIGHTS; ++i) + for(int i = 0; i < lights.length(); ++i) { - vec3 L = ubo.lights[i].position.xyz - fragPos; + vec3 L = lights[i].position.xyz - fragPos; float dist = length(L); L = normalize(L); - float atten = ubo.lights[i].radius / (pow(dist, 3.0) + 1.0); + float atten = lights[i].radius / (pow(dist, 3.0) + 1.0); vec3 N = normalize(normal); float NdotL = max(0.0, dot(N, L)); - vec3 diff = ubo.lights[i].color * albedo.rgb * NdotL * atten; + vec3 diff = lights[i].color * albedo.rgb * NdotL * atten; fragcolor += diff; } diff --git a/shaders/glsl/subpasses/composition.frag.spv b/shaders/glsl/subpasses/composition.frag.spv index 8d620d0c0a8fac8f8f9d83e43c40411c28875e08..ceeb9732ebb4822938da4783432fc65ae520bdda 100644 GIT binary patch literal 3152 zcmYk7TXR%J5XUFkY(NvaUqmJ3;sp)HTZkwI5(q5H8iD~7@xq49hNGL^m~7sBz_P6J z$tS;vU%_YnI+j{x6~Euv({g63tGoaGpYEBSnX{F_sXZwTq>;2Y?WX=XmFYB6E-cfX?yS>?z2&ti#yuZ%DI)4HyO+|&7teLJ(Mn@o7s*_A~uwk(2ZViz2BQk zTESl~VyoKCZZGEE#cyV<7K*vm{EWK0z01Y+^L!@GI=Ao0sgZvaJ*mS8f1>aH?@yO} z&!WuNmv4WZuP;A*&!x=Qmv8Un>&w@_#tP#MrtlqsnNRFM33HB^In4DsdKei;oELqM zTs3+vRzc)@2IaNsyS9k8vBwaHe0vxEVODJY3E1+rk6}gqhtnn#FlT}vEodfD<=0O9r3I@>oIK4MBlk@pk1q& zwe&p~G57lxdK_^a0i8uV?;Ki=b@VTwqh9poYQZsHD{R*jd$^9B(!xUcALss~i{anO z{U?3Chd+Rr?+di`t*QSl+H;Sd>3g5V?D;m@+Rl;BXCcoe?!NWCZ|0w)hw?lhhdKTR zsiNh2ul%-JJAA*P;d_35FT5gd%VUj?9t%@^KRZ?M{(JUG9LL(VtdCU-x#)-`K)h0#pJvGaYQWEK2c!ej;Qqu`*m+qh@45pe4dAxwrAnn z{Dgdb+*v6wT=dr~UNAyL$efa?KY=12JG>`2Z zaeL#v6bt?Ww%91I$@jE~nDY{{fH=pyD;{%<#T;v1Mx1j6F&1^ionudp#hz=}*O1t= zv8#w&*YF`?Jp7NaUBf6lG+sxn=esm^9r=O%8e2w;`|bI;z;0lh)0k`X?TN*HZen|X z?ScFG1d06^bB^y*Jm%cS77PClwmtc^sQD?j9PwE9XV~WTPTWG|n0pm%%v|>Bb0m7@ zckxRkdS&b$5_P{S;`)cMzedXM=zf8TJFGv5{8ZeJ2DUlwBj&J&Ylv%#xAp*Ao_KJY z*!D3v>)7(d-P2vPSgf;!Ef)R;_9?`3v*!;HF?r56CgvOUz50gK&bLPyVvim%U;mo| zvp3E+CRY9i*!QTviEWLW#d&qG#TZJ^v1IzI@}}J=fs>m9hWG Ha1MD4>Yc>; literal 3152 zcmYk6+j3M@5QcXSlYogp^0TZ-GBe9yH~HhrmJ(TH_JM*zU)xe%-U-p>jcR%^*MB3 z)|30Q<(2YyvsE3xcYB5LOpiv+NHFlt7Do z{lmmIb}O4pjVAG)tRKHw+1jbE@9s33wN|aMo$osdzTMc}s?^QXlbt%?EhISSy$W8b zudP=bZQkGkZ#u#7O4(U(>ppJO8}?kH&o|Ju+%9Do&608ei46f ztwFRqo5ZfxnwHM}8FZ!9TK5vnwSZRSE^AjCt?2t6ezmr-fg)bY@2s)cx{=0^?{OZr z?bW+-X35cy9@SxmKhk#h{qqIiyD9SZ&p+{J1X+^<#!_T_2q}}-MH5^N5?Vy zu{N}i$q_qN;F&|roaX)j-GdAx=JFm~k!v>HP1N=tMsr4czrMY8A?EW93?VLkXBqx+ zPGFu9*z&bcVMYDZ@O?A-#)t6_p~aLlyp|Wx#+BkSm&-kUZ8PUyO}=;NI*ES-J&bf9 z=aYXF+hq^VOO81&FcbX&M2;foCA2;Go}-5M>avGd(Dp1Z;?CS&;Kk>%6}bO zzI7+?)!y?Ia)M6YMGvLBHm6*348rve{RZR@F}Vq}n0Y@$dk!)C(D%;7j9*T+IrTq9 zNA8%-XNhC{X0ojv=a|Q))2Z;k%>8HA!@r&T2eZD9-+`F#0oq=ysqg=fcW*!TtM4}@ z=G=GC){g#vz&4k-eEr>oU1nbLyd#%6{st+b<@#+MMywsa|FhwHhyI_2FW>*o@Vztt zGs8FTe`NU1(*LTWKU47KFBE+F%LU)K|5xsxq+h@N7ZJbZHBRA-jwfsYy~v5=o=CVc zm-BHB*O-9kH6Hm+WBc7kzL&AZiuumqiz}{TKIdgT^1X&Vh(tc$zL@!}?>xoi+y5{k z7QK%oOxzWi-ou*6Q)JKT#%zliiQ(MdE`gV>)tYA-2atd5_SvQ zoW|_Uvx&ud7P0*{oq^|BLgGBe+_RAI*t3E!7XBS(iZC<~L+lU-< z`;Lv7%UOMe#H=1*e~rYfjNM0~?l&o}?;H6RDZY{K5+?4lekbyC`aT|Fn?s&CoZ%zH zp5h%zJZ9zbH6b$-=A;MH>7sI zGulL)(KGhzKS`LgalbLK;`?^)QU7~vYb>Yxs$+|}0^7nCb7p~UC(Icx(YZ5kAnuoM Q+;7hwo}!KYM}{lN|IR1ICIA2c diff --git a/shaders/hlsl/subpasses/composition.frag b/shaders/hlsl/subpasses/composition.frag index 29fd3113..6ded440b 100644 --- a/shaders/hlsl/subpasses/composition.frag +++ b/shaders/hlsl/subpasses/composition.frag @@ -1,11 +1,8 @@ // Copyright 2020 Google LLC -[[vk::input_attachment_index(0)]][[vk::binding(0)]] SubpassInput samplerposition; -[[vk::input_attachment_index(1)]][[vk::binding(1)]] SubpassInput samplerNormal; -[[vk::input_attachment_index(2)]][[vk::binding(2)]] SubpassInput samplerAlbedo; - -#define MAX_NUM_LIGHTS 64 -[[vk::constant_id(0)]] const int NUM_LIGHTS = 64; +[[vk::input_attachment_index(0)]][[vk::binding(0)]] SubpassInput inputPosition; +[[vk::input_attachment_index(1)]][[vk::binding(1)]] SubpassInput inputNormal; +[[vk::input_attachment_index(2)]][[vk::binding(2)]] SubpassInput inputAlbedo; struct Light { float4 position; @@ -13,56 +10,37 @@ struct Light { float radius; }; -struct UBO -{ - float4 viewPos; - Light lights[MAX_NUM_LIGHTS]; -}; - -cbuffer ubo : register(b3) { UBO ubo; } - +RWStructuredBuffer lights: register(u3); float4 main([[vk::location(0)]] float2 inUV : TEXCOORD) : SV_TARGET { // Read G-Buffer values from previous sub pass - float3 fragPos = samplerposition.SubpassLoad().rgb; - float3 normal = samplerNormal.SubpassLoad().rgb; - float4 albedo = samplerAlbedo.SubpassLoad(); + float3 fragPos = inputPosition.SubpassLoad().rgb; + float3 normal = inputNormal.SubpassLoad().rgb; + float4 albedo = inputAlbedo.SubpassLoad(); - #define ambient 0.15 + #define ambient 0.05 // Ambient part float3 fragcolor = albedo.rgb * ambient; - for(int i = 0; i < NUM_LIGHTS; ++i) + uint lightsLength; + uint lightsStride; + lights.GetDimensions(lightsLength, lightsStride); + + for(int i = 0; i < lightsLength; ++i) { - // Vector to light - float3 L = ubo.lights[i].position.xyz - fragPos; - // Distance from light to fragment position + float3 L = lights[i].position.xyz - fragPos; float dist = length(L); - // Viewer to fragment - float3 V = ubo.viewPos.xyz - fragPos; - V = normalize(V); - - // Light to fragment L = normalize(L); - // Attenuation - float atten = ubo.lights[i].radius / (pow(dist, 2.0) + 1.0); - - // Diffuse part + float atten = lights[i].radius / (pow(dist, 3.0) + 1.0); float3 N = normalize(normal); float NdotL = max(0.0, dot(N, L)); - float3 diff = ubo.lights[i].color * albedo.rgb * NdotL * atten; + float3 diff = lights[i].color * albedo.rgb * NdotL * atten; - // Specular part - // Specular map values are stored in alpha of albedo mrt - float3 R = reflect(-L, N); - float NdotR = max(0.0, dot(R, V)); - //float3 spec = ubo.lights[i].color * albedo.a * pow(NdotR, 32.0) * atten; - - fragcolor += diff;// + spec; + fragcolor += diff; } return float4(fragcolor, 1.0); diff --git a/shaders/hlsl/subpasses/composition.frag.spv b/shaders/hlsl/subpasses/composition.frag.spv index 380ebe6dbf698d578c4975cce9bf1335bc279b52..b0f438a995e1cbd1122a9b3efe5cf359367a603a 100644 GIT binary patch literal 2236 zcmZ9NT~AX%5Qdkw6cyw{zC;CE@B;;psDLN}ErLa@fI#%p6k4!Jp-p>gV&VnHn0V!t zKg1uRKS!e`Cce*MS57*~%j~=}J2N}Gr$i$oEygq%W7^G{{vur_5-{jdYu}Y_HSvJY zWH+;3&Ch$2Qxn9unXer~UTFCN4d4=Qzky<98$#WD$-1CKEQzgn*D=Sl%b?tn{|whQ^PxKrkY%f0b( z)34Td{Cc&JU##!$7OGyhxcAx*^2N+aX=le(7aOS@mkqjsXIeYuQn@Nkhj5&&n#&jK zHTsLGvN0v{2X;#^gL2*X-sGy@=8IQb>5a_tmSTtv&nhFE*^CJYc3c{py4ai(n|;{K zcSu0o{^29@znu)(T$NP9~(b?tk`*IX-4=XaUA*KIzt>e;VuaD z-YQK$34wjwAL4RlR0S?7^eVnhIw6pI%-W~5M*3y9OA|w#aplAA5QqVN%0xd2f&Hwz zgzw+7bL2mxbwWT~{%)mJnZk5XHYsRhWEv~ zN8CkeViVHrB^Enr?W1|df;WPnK4#_T9K3DB*_<13Hg65VrZ?UeIj~)C^f#?c^n(wf zzhAOOq~{dNY%U4Z+tzgH@v`N(e`HOkrnf5sK7cd{K9`4Y7_KqTxHxPo~K%~4{=;pYdFs2xM!A&NU!T`yn|JN{lw#Ert|s@ib~`A Is|+*3KR+vpE&u=k literal 3792 zcmZ9NX>3+i5QZ;w1Hl5a2_m+jg26Oua7m0vQBq6_MQDlt$X99bOS+|Bi}*{7G4Yqb z5Cj*NxbL{{`@Zk{g1h3biBS_1pXc@tob-H|&b;r;nKNhR-rK6tbL+BfRF-A+*`NG% zP0B{+jCwwJTvnUome$p+O_jlQO$$#v&ba!lN|pO2WOZ3J_GqN1RPL46D>o5Ko`bNe zvKn+JL=C?&*eg1guWdc6Wm)@bxw>o`+~5WMT}_pt&i+!R(p2s#t?$xTi)2}))YISH zHPGKzDG!$WdUL+^0KcMdpr_OgHr_Lf_~!1;u62DmUy~h*&d(U?lp?Y^=d`72sB;E( z$C9@ERBhLWa@YAQ`zrf6JJ(%af8Jol^QyDi;8roZmY>+QzgTQi_3C^nzix#@F>tu5{srQXQOjJgD>(DZO-wj*%FRNo^)R&cG$8Ja zXCcicH!_#cm@DF1s7B8GQ^|V}x&d+D2?e&Vi;)Ed9*6Ck4jzwokNc0M4)MW=G0HUZ z*P|N{_iIl;wr`uUDDrtv&kH;W+k6p!2sUeR;K@1Mbb8<^InL<8*!(^940~gaSYbC% z!_H{u4gy%<$sd*dACP9n4YFxF*%{stX|cCS6yjF><057;&Kt0uY2>^STW(~|o8a^* zt~94}(H}W)#&&Lz^A>Em$Y~wUPToD<Yd$-Cmb%jkf2yd5#d@0Izs7M!+o z_xtt_x4r}IbEWny-11d9H`KG1z|*^D}xM;`$f72YnYJZ!Yb~bvL%Tn#tw+ zb`Rn^;Q7sT!FTRnMBZOr?}$Y#(VDv5NCLC(T4}I-6QYaea7V? z{vm9+@l3fM{V-zAC5S%vY)8w-9Uj3xaEF@w4v)ggr?o!@mXF#W$CguEBWr&GF0K7Z zuzTdK!}m`vYJUn_&f2~E(}+2jBKl&FeC&A!JMQbAXOY;WFM1^J9-oKjkaUL6gAd%l zE-QmyK;)gF@skku+tU{j{efS?wx{Enzy6mIYnX}X^MB?=%unAdi2ljQr3LpYwmJ1# zuiqiLxZ`WszH`x+*RkV{`rPBUNFzLxj6GJY-8l3?zgecJ0F>X7-Md~ zd-}{}|K35;{=ExM`?nGN9wKl5j8{i3?_=v1+oKPVEl5099~PXv%bNV2T~fRQA7Pur zICI$7j}hyP@5LwB#>q#_r`Yy3Vm`w*PTqU&K+8p)pJUsz3pvAQYcC=f{x7iYvHjEk zC1R~D$X-Ohd3U1q#lEkw9-Qb9hTUj=o@v}Mh@8Ia0{=($Q;`1wWj0eo