From 4b7a8374698a161caa1cb4310c4bb6ef93e145f5 Mon Sep 17 00:00:00 2001 From: Brandon Mak Date: Wed, 26 Oct 2022 01:08:02 +0800 Subject: [PATCH] Fixed some bugs in render graph - Changed the eid buffer for instanced rendering to a vec2 (potentially vec3 or 4), to pass other types of data like light layer index. - Renamed some render graph nodes and subpasses. Added a dummy render pass to transition the scene to read only optimal. - offscreen buffer resource now transitions to eShaderReadOnlyOptimal instead of eGeneral --- .../Graphics/MiddleEnd/Batching/SHBatch.cpp | 40 +++++++++++------- .../src/Graphics/MiddleEnd/Batching/SHBatch.h | 7 +-- .../MiddleEnd/Batching/SHSuperBatch.cpp | 2 +- .../GlobalData/SHGraphicsGlobalData.cpp | 2 +- .../MiddleEnd/Interface/SHGraphicsConstants.h | 2 +- .../MiddleEnd/Interface/SHGraphicsSystem.cpp | 23 +++++----- .../Interface/SHInstancedIntegerData.h | 12 ++++++ .../Interface/SHPostOffscreenRenderSystem.cpp | 2 +- .../src/Graphics/Pipeline/SHPipelineState.cpp | 6 +++ .../Graphics/RenderGraph/SHRenderGraph.cpp | 3 ++ SHADE_Engine/src/Graphics/SHVkUtil.cpp | 12 ++++++ .../VertexDescriptors/SHVertexAttribute.h | 3 ++ TempShaderFolder/TestCubeVs.glsl | 5 ++- TempShaderFolder/TestCubeVs.spv | Bin 2300 -> 2376 bytes 14 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHInstancedIntegerData.h diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.cpp index 40826047..476351c9 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.cpp @@ -110,7 +110,7 @@ namespace SHADE // Clear CPU buffers drawData.clear(); transformData.clear(); - eidData.clear(); + instancedIntegerData.clear(); matPropsData.reset(); matPropsDataSize = 0; @@ -120,7 +120,7 @@ namespace SHADE { drawDataBuffer[i].Free(); transformDataBuffer[i].Free(); - eidBuffer[i].Free(); + instancedIntegerBuffer[i].Free(); matPropsBuffer[i].Free(); } } @@ -208,7 +208,7 @@ namespace SHADE transformDataBuffer[frameIndex]->WriteToMemory(transformData.data(), static_cast(transformData.size() * sizeof(SHMatrix)), 0, 0); } - void SHBatch::UpdateEIDBuffer(uint32_t frameIndex) + void SHBatch::UpdateInstancedIntegerBuffer(uint32_t frameIndex) { if (frameIndex >= SHGraphicsConstants::NUM_FRAME_BUFFERS) { @@ -217,18 +217,23 @@ namespace SHADE } // Reset Transform Data - eidData.clear(); + instancedIntegerData.clear(); // Populate on the CPU for (auto& subBatch : subBatches) for (const SHRenderable* renderable : subBatch.Renderables) { - eidData.emplace_back(renderable->GetEID()); + instancedIntegerData.emplace_back(SHInstancedIntegerData + { + renderable->GetEID(), + renderable->GetLightLayer() + } + ); } // Transfer to GPU - if (eidBuffer[frameIndex]) - eidBuffer[frameIndex]->WriteToMemory(eidData.data(), static_cast(eidData.size() * sizeof(EntityID)), 0, 0); + if (instancedIntegerBuffer[frameIndex]) + instancedIntegerBuffer[frameIndex]->WriteToMemory(instancedIntegerData.data(), static_cast(instancedIntegerData.size() * sizeof(SHInstancedIntegerData)), 0, 0); } @@ -264,8 +269,8 @@ namespace SHADE transformData.reserve(numTotalElements); transformData.clear(); // - EID data - eidData.reserve(numTotalElements); - eidData.clear(); + instancedIntegerData.reserve(numTotalElements); + instancedIntegerData.clear(); // - Material Properties Data @@ -320,8 +325,13 @@ namespace SHADE transformData.emplace_back(transform->GetTRS()); } - eidData.emplace_back(eid); - + instancedIntegerData.emplace_back(SHInstancedIntegerData + { + eid, + renderable->GetLightLayer() + } + ); + // Material Properties if (!EMPTY_MAT_PROPS) { @@ -351,10 +361,10 @@ namespace SHADE device, transformDataBuffer[frameIndex], transformData.data(), TF_DATA_BYTES, BuffUsage::eVertexBuffer ); - const uint32_t EID_DATA_BYTES = static_cast(eidData.size() * sizeof(EntityID)); + const uint32_t EID_DATA_BYTES = static_cast(instancedIntegerData.size() * sizeof(SHInstancedIntegerData)); SHVkUtil::EnsureBufferAndCopyHostVisibleData ( - device, eidBuffer[frameIndex], eidData.data(), EID_DATA_BYTES, + device, instancedIntegerBuffer[frameIndex], instancedIntegerData.data(), EID_DATA_BYTES, BuffUsage::eVertexBuffer ); // - Material Properties Buffer @@ -378,8 +388,8 @@ namespace SHADE // Bind all required objects before drawing static std::array dynamicOffset { 0 }; cmdBuffer->BindPipeline(pipeline); - cmdBuffer->BindVertexBuffer(SHGraphicsConstants::VertexBufferBindings::TRANSFORM, transformDataBuffer[frameIndex], 0); - cmdBuffer->BindVertexBuffer(SHGraphicsConstants::VertexBufferBindings::EID, eidBuffer[frameIndex], 0); + cmdBuffer->BindVertexBuffer(SHGraphicsConstants::VertexBufferBindings::TRANSFORM, transformDataBuffer[frameIndex], 0); + cmdBuffer->BindVertexBuffer(SHGraphicsConstants::VertexBufferBindings::INTEGER_DATA, instancedIntegerBuffer[frameIndex], 0); if (matPropsDescSet[frameIndex]) { cmdBuffer->BindDescriptorSet diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.h b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.h index 193fff0d..a7124e05 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.h +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHBatch.h @@ -23,6 +23,7 @@ of DigiPen Institute of Technology is prohibited. #include "Math/SHMatrix.h" #include "Graphics/MiddleEnd/Interface/SHGraphicsConstants.h" #include "ECS_Base/SHECSMacros.h" +#include "Graphics/MiddleEnd/Interface/SHInstancedIntegerData.h" namespace SHADE { @@ -79,7 +80,7 @@ namespace SHADE void Clear(); void UpdateMaterialBuffer(uint32_t frameIndex, Handle descPool); void UpdateTransformBuffer(uint32_t frameIndex); - void UpdateEIDBuffer(uint32_t frameIndex); + void UpdateInstancedIntegerBuffer(uint32_t frameIndex); void Build(Handle device, Handle descPool, uint32_t frameIndex) ; void Draw(Handle cmdBuffer, uint32_t frameIndex); @@ -111,7 +112,7 @@ namespace SHADE // CPU Buffers std::vector drawData; std::vector transformData; - std::vector eidData; + std::vector instancedIntegerData; std::unique_ptr matPropsData; Byte matPropsDataSize = 0; Byte singleMatPropAlignedSize = 0; @@ -120,7 +121,7 @@ namespace SHADE // GPU Buffers TripleBuffer drawDataBuffer; TripleBuffer transformDataBuffer; - TripleBuffer eidBuffer; + TripleBuffer instancedIntegerBuffer; TripleBuffer matPropsBuffer; TripleDescSet matPropsDescSet; diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHSuperBatch.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHSuperBatch.cpp index 14f2aa76..434e7163 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHSuperBatch.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Batching/SHSuperBatch.cpp @@ -85,7 +85,7 @@ namespace SHADE { batch.UpdateMaterialBuffer(frameIndex, descPool); batch.UpdateTransformBuffer(frameIndex); - batch.UpdateEIDBuffer(frameIndex); + batch.UpdateInstancedIntegerBuffer(frameIndex); } } diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/GlobalData/SHGraphicsGlobalData.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/GlobalData/SHGraphicsGlobalData.cpp index 94d1d2c5..0f1658f3 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/GlobalData/SHGraphicsGlobalData.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/GlobalData/SHGraphicsGlobalData.cpp @@ -115,7 +115,7 @@ namespace SHADE defaultVertexInputState.AddBinding(false, false, { SHVertexAttribute(SHAttribFormat::FLOAT_3D) }); // Normals at binding 2 defaultVertexInputState.AddBinding(false, false, { SHVertexAttribute(SHAttribFormat::FLOAT_3D) }); // Tangents at binding 3 defaultVertexInputState.AddBinding(true, true, { SHVertexAttribute(SHAttribFormat::MAT_4D) }); // Transform at binding 4 - 7 (4 slots) - defaultVertexInputState.AddBinding(true, true, { SHVertexAttribute(SHAttribFormat::UINT32_1D) }); // EID at binding 8 + defaultVertexInputState.AddBinding(true, true, { SHVertexAttribute(SHAttribFormat::UINT32_2D) }); // Instanced integer data at index 8 } void SHGraphicsGlobalData::Init(Handle logicalDevice) noexcept diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsConstants.h b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsConstants.h index f31816ce..4c3ba7f9 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsConstants.h +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsConstants.h @@ -182,7 +182,7 @@ namespace SHADE Vertex buffer bindings for the eid buffer. */ /***************************************************************************/ - static constexpr uint32_t EID = 5; + static constexpr uint32_t INTEGER_DATA = 5; }; diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsSystem.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsSystem.cpp index b7d4bd2f..252d4af6 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsSystem.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHGraphicsSystem.cpp @@ -176,18 +176,21 @@ namespace SHADE worldRenderGraph->AddResource("Scene Pre-Process", { SH_ATT_DESC_TYPE_FLAGS::COLOR, SH_ATT_DESC_TYPE_FLAGS::INPUT, SH_ATT_DESC_TYPE_FLAGS::STORAGE }, windowDims.first, windowDims.second); worldRenderGraph->AddResource("Scene", { SH_ATT_DESC_TYPE_FLAGS::COLOR, SH_ATT_DESC_TYPE_FLAGS::INPUT, SH_ATT_DESC_TYPE_FLAGS::STORAGE }, windowDims.first, windowDims.second); worldRenderGraph->AddResource("Depth Buffer", { SH_ATT_DESC_TYPE_FLAGS::DEPTH_STENCIL }, windowDims.first, windowDims.second, vk::Format::eD32SfloatS8Uint); - worldRenderGraph->AddResource("Entity ID", { SH_ATT_DESC_TYPE_FLAGS::COLOR }, windowDims.first, windowDims.second, vk::Format::eR32Uint, 1, vk::ImageUsageFlagBits::eTransferSrc); + worldRenderGraph->AddResource("Entity ID", { SH_ATT_DESC_TYPE_FLAGS::COLOR }, windowDims.first, windowDims.second, vk::Format::eR32G32Uint, 1, vk::ImageUsageFlagBits::eTransferSrc); - auto node = worldRenderGraph->AddNode("G-Buffer", { "Entity ID", "Depth Buffer", "Scene", "Scene Pre-Process"}, {}); // no predecessors + auto gBufferNode = worldRenderGraph->AddNode("G-Buffer", { "Entity ID", "Depth Buffer", "Scene", "Scene Pre-Process"}, {}); // no predecessors + auto gBufferSubpass = gBufferNode->AddSubpass("G-Buffer Write"); + gBufferSubpass->AddColorOutput("Scene Pre-Process"); + gBufferSubpass->AddColorOutput("Entity ID"); + gBufferSubpass->AddDepthOutput("Depth Buffer", SH_ATT_DESC_TYPE_FLAGS::DEPTH_STENCIL); - //First subpass to write to G-Buffer - auto gBufferWriteSubpass = node->AddSubpass("G-Buffer Write"); - gBufferWriteSubpass->AddColorOutput("Scene Pre-Process"); - gBufferWriteSubpass->AddColorOutput("Entity ID"); - gBufferWriteSubpass->AddDepthOutput("Depth Buffer", SH_ATT_DESC_TYPE_FLAGS::DEPTH_STENCIL); + auto kirschShader = shaderModuleLibrary.GetShaderModule("KirschCs.glsl"); + gBufferNode->AddNodeCompute (kirschShader, {"Scene Pre-Process", "Scene"}); + + auto dummyNode = worldRenderGraph->AddNode("Dummy Pass", { "Scene" }, {"G-Buffer"}); // no predecessors + auto dummySubpass = dummyNode->AddSubpass("Dummy Subpass"); + dummySubpass->AddInput("Scene"); - auto greyscale = shaderModuleLibrary.GetShaderModule("KirschCs.glsl"); - node->AddNodeCompute (greyscale, {"Scene Pre-Process", "Scene"}); // Generate world render graph worldRenderGraph->Generate(); @@ -201,7 +204,7 @@ namespace SHADE auto cubeVS = shaderModuleLibrary.GetShaderModule("TestCubeVs.glsl"); auto cubeFS = shaderModuleLibrary.GetShaderModule("TestCubeFs.glsl"); - defaultMaterial = AddMaterial(cubeVS, cubeFS, gBufferWriteSubpass); + defaultMaterial = AddMaterial(cubeVS, cubeFS, gBufferSubpass); } diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHInstancedIntegerData.h b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHInstancedIntegerData.h new file mode 100644 index 00000000..8ba5adf8 --- /dev/null +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHInstancedIntegerData.h @@ -0,0 +1,12 @@ +#pragma once + +#include "ECS_Base/SHECSMacros.h" + +namespace SHADE +{ + struct SHInstancedIntegerData + { + EntityID eid; + uint32_t lightLayer; + }; +} diff --git a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHPostOffscreenRenderSystem.cpp b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHPostOffscreenRenderSystem.cpp index ebce5c9e..8b41a979 100644 --- a/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHPostOffscreenRenderSystem.cpp +++ b/SHADE_Engine/src/Graphics/MiddleEnd/Interface/SHPostOffscreenRenderSystem.cpp @@ -68,7 +68,7 @@ namespace SHADE { std::vector combinedImageSampler { - std::make_tuple(offscreenRender->GetImageView(), offscreenRenderSampler, vk::ImageLayout::eGeneral), + std::make_tuple(offscreenRender->GetImageView(), offscreenRenderSampler, vk::ImageLayout::eShaderReadOnlyOptimal), }; // Register the image view and sampler with the descriptor set. Now whenever rendering to the offscreen image is done, the descriptor set will see the change diff --git a/SHADE_Engine/src/Graphics/Pipeline/SHPipelineState.cpp b/SHADE_Engine/src/Graphics/Pipeline/SHPipelineState.cpp index f8934bf6..c7ada11f 100644 --- a/SHADE_Engine/src/Graphics/Pipeline/SHPipelineState.cpp +++ b/SHADE_Engine/src/Graphics/Pipeline/SHPipelineState.cpp @@ -142,6 +142,12 @@ namespace SHADE case SHAttribFormat::UINT32_1D: return std::make_tuple(1, 4, vk::Format::eR32Uint); + case SHAttribFormat::UINT32_2D: + return std::make_tuple(1, 8, vk::Format::eR32G32Uint); + case SHAttribFormat::UINT32_3D: + return std::make_tuple(1, 12, vk::Format::eR32G32B32Uint); + case SHAttribFormat::UINT32_4D: + return std::make_tuple(1, 16, vk::Format::eR32G32B32A32Uint); } return std::make_tuple(0, 0, vk::Format::eR32Sfloat); } diff --git a/SHADE_Engine/src/Graphics/RenderGraph/SHRenderGraph.cpp b/SHADE_Engine/src/Graphics/RenderGraph/SHRenderGraph.cpp index 4684419a..93be2413 100644 --- a/SHADE_Engine/src/Graphics/RenderGraph/SHRenderGraph.cpp +++ b/SHADE_Engine/src/Graphics/RenderGraph/SHRenderGraph.cpp @@ -142,6 +142,9 @@ namespace SHADE attDesc.loadOp = vk::AttachmentLoadOp::eLoad; predAttDesc.storeOp = vk::AttachmentStoreOp::eStore; + attDesc.stencilLoadOp = vk::AttachmentLoadOp::eLoad; + attDesc.stencilStoreOp = vk::AttachmentStoreOp::eStore; + // TODO: Stencil load and store // When an image is done being used in a renderpass, the image layout will end up being the finalLayout diff --git a/SHADE_Engine/src/Graphics/SHVkUtil.cpp b/SHADE_Engine/src/Graphics/SHVkUtil.cpp index cf486a7a..8b21e7d1 100644 --- a/SHADE_Engine/src/Graphics/SHVkUtil.cpp +++ b/SHADE_Engine/src/Graphics/SHVkUtil.cpp @@ -51,6 +51,18 @@ namespace SHADE case vk::Format::eR32Uint: case vk::Format::eR32Sfloat: return 4; + case vk::Format::eR32G32Sint: + case vk::Format::eR32G32Uint: + case vk::Format::eR32G32Sfloat: + return 8; + case vk::Format::eR32G32B32Sint: + case vk::Format::eR32G32B32Uint: + case vk::Format::eR32G32B32Sfloat: + return 12; + case vk::Format::eR32G32B32A32Sint: + case vk::Format::eR32G32B32A32Uint: + case vk::Format::eR32G32B32A32Sfloat: + return 16; } return 0; } diff --git a/SHADE_Engine/src/Graphics/VertexDescriptors/SHVertexAttribute.h b/SHADE_Engine/src/Graphics/VertexDescriptors/SHVertexAttribute.h index b216f5f4..b7191c21 100644 --- a/SHADE_Engine/src/Graphics/VertexDescriptors/SHVertexAttribute.h +++ b/SHADE_Engine/src/Graphics/VertexDescriptors/SHVertexAttribute.h @@ -22,6 +22,9 @@ namespace SHADE // integer formats UINT32_1D, + UINT32_2D, + UINT32_3D, + UINT32_4D, }; struct SHVertexAttribute diff --git a/TempShaderFolder/TestCubeVs.glsl b/TempShaderFolder/TestCubeVs.glsl index b7453f13..c6b4071f 100644 --- a/TempShaderFolder/TestCubeVs.glsl +++ b/TempShaderFolder/TestCubeVs.glsl @@ -3,12 +3,13 @@ //#include "ShaderDescriptorDefinitions.glsl" + layout(location = 0) in vec3 aVertexPos; layout(location = 1) in vec2 aUV; layout(location = 2) in vec3 aNormal; layout(location = 3) in vec3 aTangent; layout(location = 4) in mat4 worldTransform; -layout(location = 8) in uint eid; +layout(location = 8) in uvec2 integerData; layout(location = 0) out struct @@ -36,7 +37,7 @@ void main() { Out.uv = aUV; Out2.materialIndex = gl_InstanceIndex; - Out2.eid = eid; + Out2.eid = integerData[0]; gl_Position = cameraData.vpMat * worldTransform * vec4 (aVertexPos, 1.0f); Out.vertColor = vec4 (aVertexPos, 1.0f); } \ No newline at end of file diff --git a/TempShaderFolder/TestCubeVs.spv b/TempShaderFolder/TestCubeVs.spv index 1662cfbd308b1bd28ec466201103f91aaa761084..904062a69b13a76b244a967674fc08ed38bbca58 100644 GIT binary patch literal 2376 zcmZ9N+fEcg5QbZrT@VCC6i^Wt@PLAN;2??!2)K(u!GPRuCc7hgpQk?3nB_=7Q-ledfK%)_~~{Voa|o+P%E9zcbrP>$CF< zPhs?%PD_MySwEI1IrhUCy;lM{B)K8EBY7m5lPpOpl5I(+ejVEXD3PZM$~)Ea?rwQ! zt6FO|tF82V6jY-y4yyGq32JG08o=vrfFWud}#G+(pg2*Wt@KOr$G|>(RN%9zFD7VedG(my5c=uuVDKX#%XXAB%6Nf3n`kZjK3EohpOBjWA6jbQm(y5 zD@?;iY-Qa>*70!qB9>o;SE{TIHP^TKMoS# zmS}4MV>MkN(u+BlT27lE{8T&fv)XHI@!XO5_}WOK`a$BytuKw_IO8%ies*o$5zMnQ z!k8D2X$v^Ppqafx8Qx=Ce9K+DU<{h!A1)KTArWe?}2{F^T&D`SemQ-|#>opvf z!^j85?l5wK(MyM+gE60FdE7hTcfFgGk5v&1jGP5)XB=!eSI%Mjh%H85;@q9+^TauO zTsDh4({t{a)#Tal6Pty`RuH^<_G@`-IUK;`?)*e8gk(?#@3To&5O8H>kZNVNnm? zG-qVECr-u(Cm%L*Lk@TTuyhvZk7(yCa=^!CuH5;zq!Wvue%#j1S@eT=dJ7kvcx>hn zjPu&-g+HGAT)lT?lONmh@8_IR@gGR1|6pU%rnI{^!iSLep?31%rw(ksf3OkR)7pvS zUFq9QZi5s5SUYzDCmx$wboYFc>yFPK#_Wz6AA2sx=+mNf>gIg%zsP;y{sjpfa5(h$rG(yo5{EZf&M|Fg*y|jlpN_YZWAt)Qou~J2 zB*eqV&l~+xUVN((&WFPt*6g0mo$J%OFnF%Ex6<2d+mH>$a&>G<2WziwE5|ytyV~AK YCmue2YIAjzrSrb74s7`Ulyyn+A89?W<^TWy literal 2300 zcmZ9NYflqF6oyCG7QCT&!8;aEyr6gk#0!G9T1cv3P`@^tb|sr^x6O8omrwo${Ym~R zznJ(wGdrYlnv3)p&uiVb;3AheN+ZJCA%P-md(rV$(Cd_*=yOTej}QHD^Z{d>WyZ7d%NCv z-E8%G&3^tZiJD2AM$LAdMXfyEkI;<|>Eg7N4BEqjk`%+Wl(ffT?ZGJP=hypDmRGw; zH?w?TdCoJ~H-;F_usz7_nzMu-KiTyetr=xAX8gJn=1~@h$yVBqzFJ>kedLScw&DWY z3*~!AY)P^7WiM%NrTsiiTagd7d(bZzS2ow2ZYRnDzSn3No+BR|v**YO zM=w3c8ZMC4@;9BC;#L0@mh|&#Q8JP=h^@8j4*>U({s+~ zY6|Rk31;B0X$Jdz^cl=qPK$+~5dyQG(yFYvB0Hye>H?D=OfSJ`r-k_r(ZE-Pk7yFJeYg;{zfu{v561`6I6XZ1 zz`PrB`2Ejm&0zoYn%RpS=)t@zzyBqziFM})%$35&e(1sU*Xt`E#_6{l!lX`gm6c?=k;`>FF=U;C~=P1CNGYF3afSduiz5 zz4E#z1xMd@)MfhoR7N~{{M^S+<#l)9_D91RxSur{Z_M`u{LJ#Mwij9t s*S0PU$MAJ*Xbm@9+slF*uI-gL;x}aYsm<3>)tbBXb%4?TRn}G6KNk$B#sB~S