From 4363185a826852ed8195f799da3719f3bdee4a72 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Mon, 2 Nov 2026 19:29:07 +0530 Subject: [PATCH] SurfaceFlinger renderer: RGBA_8888 scan-out slots under the daemon's layout rule The SurfaceFlinger display renderer hands each X window's CPU swapchain slot to surfaceflinger as a BGRA_8888 AHardwareBuffer. Android 16 on a Skia GLES render engine cannot wrap one and aborts surfaceflinger (RedMagic Astra: "Unable to generate SkImage ... colorType 6" 30-40 ms after the first window maps, whole UI restarts). The gamescope daemon already answers this with an RGBA_8888 pool chosen by a device rule (sdk >= 36 and a GLES render engine, GNDRM_SCANOUT_LAYOUT override); the X11 path had no equivalent. The renderer's slots now follow the same rule, read through the daemon's layout query before the first drawable: RGBA_8888 slots get the X server's B,G,R,A bytes swapped on the CPU copy and go to surfaceflinger as they are, skipping the BGRA-to-RGBA blit (which would swap an RGBA-declared source again). The front buffer the X server writes into stays as it was. BGRA devices are unchanged. libahbimage.so rebuilt with NDK 27.3. --- app/src/main/cpp/asurfacerenderer/ahbimage.c | 33 ++++++++++++++--- .../ui/screen/xserver/XServerScreen.kt | 29 +++++++++++++++ .../java/com/winlator/renderer/AHBImage.java | 35 +++++++++++++++--- .../winlator/renderer/ASurfaceRenderer.java | 7 +++- app/src/main/jniLibs/arm64-v8a/libahbimage.so | Bin 12776 -> 13776 bytes 5 files changed, 92 insertions(+), 12 deletions(-) diff --git a/app/src/main/cpp/asurfacerenderer/ahbimage.c b/app/src/main/cpp/asurfacerenderer/ahbimage.c index 828e745d73..172c48dce6 100644 --- a/app/src/main/cpp/asurfacerenderer/ahbimage.c +++ b/app/src/main/cpp/asurfacerenderer/ahbimage.c @@ -11,6 +11,9 @@ #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM 5 +/* HAL_PIXEL_FORMAT_RGBA_8888: the only 32-bit layout every SurfaceFlinger render engine can + * wrap; AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM in the NDK header. */ +#define AHB_FORMAT_RGBA_8888 1 static void dump_ahb_usage(uint64_t usage) { @@ -170,10 +173,19 @@ Java_com_winlator_renderer_AHBImage_hardwareBufferFromSocket( return (jlong)(uintptr_t)ahb; } +/* The X server writes B,G,R,A bytes; an RGBA_8888 slot wants R,G,B,A. */ +static void copy_row_swap_rb(uint32_t *dst, const uint32_t *src, size_t count) +{ + for (size_t i = 0; i < count; i++) { + uint32_t p = src[i]; + dst[i] = (p & 0xff00ff00u) | ((p & 0x00ff0000u) >> 16) | ((p & 0x000000ffu) << 16); + } +} + JNIEXPORT jint JNICALL Java_com_winlator_renderer_AHBImage_copyHardwareBuffer( JNIEnv *env, jobject obj, jobject srcBuffer, jlong dstPtr, - jshort width, jshort height, jshort srcStride, jint waitFence) + jshort width, jshort height, jshort srcStride, jint waitFence, jboolean swapRB) { uint32_t* srcAddr = (uint32_t*)(*env)->GetDirectBufferAddress(env, srcBuffer); jlong srcCapacity = (*env)->GetDirectBufferCapacity(env, srcBuffer); @@ -206,8 +218,19 @@ Java_com_winlator_renderer_AHBImage_copyHardwareBuffer( return -1; } - if ((uint32_t)width == (uint32_t)srcStride && - (uint32_t)width == dstStride) { + if (swapRB) { + if ((uint32_t)width == (uint32_t)srcStride && + (uint32_t)width == dstStride) { + copy_row_swap_rb(dstAddr, srcAddr, (size_t)width * (size_t)height); + } else { + for (int y = 0; y < height; y++) { + copy_row_swap_rb(dstAddr + (size_t)y * dstStride, + srcAddr + (size_t)y * (uint32_t)srcStride, + (size_t)width); + } + } + } else if ((uint32_t)width == (uint32_t)srcStride && + (uint32_t)width == dstStride) { memcpy(dstAddr, srcAddr, (size_t)width * (size_t)height * 4); } else { for (int y = 0; y < height; y++) { @@ -229,7 +252,7 @@ Java_com_winlator_renderer_AHBImage_copyHardwareBuffer( JNIEXPORT jlong JNICALL Java_com_winlator_renderer_AHBImage_createHardwareBuffer( - JNIEnv *env, jobject obj, jshort width, jshort height) + JNIEnv *env, jobject obj, jshort width, jshort height, jboolean rgba) { if (width <= 0 || height <= 0) { LOGE("nativeCreateHardwareBuffer: invalid dimensions %d x %d", width, height); @@ -241,7 +264,7 @@ Java_com_winlator_renderer_AHBImage_createHardwareBuffer( desc.width = (uint32_t)width; desc.height = (uint32_t)height; desc.layers = 1; - desc.format = AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM; + desc.format = rgba ? AHB_FORMAT_RGBA_8888 : AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM; desc.usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_GPU_COLOR_OUTPUT diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 192513e994..0e2530537b 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -217,6 +217,7 @@ import com.winlator.widget.FrameRating import com.winlator.widget.InputControlsView import com.winlator.widget.TouchpadView import com.winlator.renderer.ASurfaceRenderer +import com.winlator.renderer.AHBImage import com.winlator.renderer.GLRenderer import com.winlator.renderer.VulkanRenderer import com.winlator.widget.XServerRendererView @@ -262,6 +263,8 @@ import kotlinx.coroutines.withTimeoutOrNull import org.json.JSONException import org.json.JSONObject import timber.log.Timber +import android.system.Os +import android.system.ErrnoException import java.io.File import java.io.IOException import java.nio.file.Files @@ -2128,6 +2131,9 @@ fun XServerScreen( // other containers as well. Uncheck the per-container useLegacyRenderer // setting to switch to the Vulkan renderer. val useGLRenderer = isX11Display && (container.graphicsDriver == "virgl" || container.displayRenderer.equals("gl", true)) + // Before the first drawable: the SurfaceFlinger display renderer hands its window buffers to + // surfaceflinger, so they follow the same scan-out layout rule as the gamescope daemon's pool. + if (isX11Display) AHBImage.setCpuScanoutRgba(resolveScanoutLayout(container) == "rgba") val xServerViewInstance: XServerRendererView = if (useGLRenderer) { XServerViewGL(context, xServerToUse) } else { @@ -6077,6 +6083,29 @@ internal fun resolveDisplayX11(container: Container?): Boolean { return container == null || Container.isX11Display(container.containerVariant, container.displayServer) } +/** + * The scan-out layout for buffers this process hands to SurfaceFlinger, "bgra" or "rgba": the container's + * GNDRM_SCANOUT_LAYOUT override, else the gndrm daemon's device rule (RGBA on Android 16 with a GLES render + * engine, which aborts surfaceflinger on a BGRA_8888 layer). The override is put in this process's + * environment first, the way the daemon reads it, and cleared otherwise so an earlier session's does not linger. + */ +internal fun resolveScanoutLayout(container: Container): String { + val override = EnvVars(container.envVars).get("GNDRM_SCANOUT_LAYOUT") + try { + if (override.isNotEmpty()) Os.setenv("GNDRM_SCANOUT_LAYOUT", override, true) else Os.unsetenv("GNDRM_SCANOUT_LAYOUT") + } catch (e: ErrnoException) { + Timber.w(e, "Could not set GNDRM_SCANOUT_LAYOUT") + } + val layout = try { + GameNativeDrm.nativeScanoutLayout() ?: "bgra" + } catch (e: LinkageError) { + Timber.w(e, "Scan-out layout rule unavailable, assuming bgra") + "bgra" + } + Timber.i("X11 scan-out layout %s (%s)", layout, if (override.isNotEmpty()) "container override" else "device rule") + return layout +} + private fun refreshComponentsFiles(context: Context) { val extractionPairs = listOf( "pulseaudio-gamenative-20260919.tzst" to File(context.filesDir, "pulseaudio"), diff --git a/app/src/main/java/com/winlator/renderer/AHBImage.java b/app/src/main/java/com/winlator/renderer/AHBImage.java index 7d9c6fa5e6..c2f490457e 100644 --- a/app/src/main/java/com/winlator/renderer/AHBImage.java +++ b/app/src/main/java/com/winlator/renderer/AHBImage.java @@ -18,13 +18,37 @@ public class AHBImage extends NativeTexture { private int swapchainIndex = 0; private long preparedRendererGeneration = -1; private static boolean supported = false; + /** + * Scan-out layout of the CPU swapchain slots handed to SurfaceFlinger, the same rule the gndrm daemon + * applies to its pool: RGBA_8888 where the render engine cannot wrap a BGRA_8888 buffer (Android 16 on + * Skia GLES aborts surfaceflinger on one), BGRA_8888 elsewhere. The X server keeps writing B,G,R,A bytes + * into the locked front buffer; an RGBA slot gets them swapped on the copy. + */ + private static volatile boolean cpuScanoutRgba = false; + private final boolean scanoutRgba; + + /** Set before a session's first drawable: every AHBImage created afterwards follows it. */ + public static void setCpuScanoutRgba(boolean rgba) { + cpuScanoutRgba = rgba; + } + + public static boolean isCpuScanoutRgba() { + return cpuScanoutRgba; + } + + /** Whether this image's scan-out slots are RGBA_8888 with R,G,B,A bytes (no conversion needed). */ + public boolean isScanoutRgba() { + return scanoutRgba; + } static { System.loadLibrary("ahbimage"); } public AHBImage(short width, short height) { - hardwareBufferPtr = createHardwareBuffer(width, height); + scanoutRgba = cpuScanoutRgba; + // The front buffer is only ever CPU-written and read back; its declared format does not matter. + hardwareBufferPtr = createHardwareBuffer(width, height, false); if (hardwareBufferPtr == 0) { System.err.println("Error: Failed to create hardware buffer"); return; @@ -43,7 +67,7 @@ public AHBImage(short width, short height) { boolean swapchainOk = true; for (int i = 0; i < swapchainAhbs.length; i++) { - swapchainAhbs[i] = createHardwareBuffer(width, height); + swapchainAhbs[i] = createHardwareBuffer(width, height, scanoutRgba); if (swapchainAhbs[i] == 0) swapchainOk = false; } @@ -59,6 +83,7 @@ public AHBImage(short width, short height) { } public AHBImage(int socketFd) { + scanoutRgba = false; hardwareBufferPtr = hardwareBufferFromSocket(socketFd); if (hardwareBufferPtr != 0) { width = nativeGetWidth(hardwareBufferPtr); @@ -122,7 +147,7 @@ public long getScanoutHardwareBufferPtr() { nativeCloseFd(lastAcquireFence); } - lastAcquireFence = copyHardwareBuffer(virtualData, targetAhb, (short)width, (short)height, stride, waitFence); + lastAcquireFence = copyHardwareBuffer(virtualData, targetAhb, (short)width, (short)height, stride, waitFence, scanoutRgba); lastUsedSlot = swapchainIndex; swapchainIndex = (swapchainIndex + 1) % swapchainAhbs.length; @@ -205,7 +230,7 @@ public long getHardwareBufferPtr() { private native long hardwareBufferFromSocket(int fd); - private native long createHardwareBuffer(short width, short height); + private native long createHardwareBuffer(short width, short height, boolean rgba); private native void destroyHardwareBuffer(long hardwareBufferPtr); @@ -213,7 +238,7 @@ public long getHardwareBufferPtr() { private native int unlockHardwareBuffer(long hardwareBufferPtr); - private native int copyHardwareBuffer(ByteBuffer srcBuffer, long dstPtr, short width, short height, short srcStride, int waitFence); + private native int copyHardwareBuffer(ByteBuffer srcBuffer, long dstPtr, short width, short height, short srcStride, int waitFence, boolean swapRB); public static native short nativeGetWidth(long ptr); diff --git a/app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java b/app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java index a17638544a..164dac45a1 100644 --- a/app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java +++ b/app/src/main/java/com/winlator/renderer/ASurfaceRenderer.java @@ -476,8 +476,11 @@ private void pushCpuImageToNative(int windowId, Drawable drawable) { long ahbPtr = g.getScanoutHardwareBufferPtr(); if (ahbPtr != 0) { int acquireFence = g.consumeAcquireFence(); - // Disable swap R/B in cpu path as it is handled with drawable - nativeSetWindowBuffer(windowId, ahbPtr, acquireFence, 0, 0, g, g.getLastUsedSlot(), sfCompatMode); + // Disable swap R/B in cpu path as it is handled with drawable. + // An RGBA_8888 slot already holds R,G,B,A bytes: SurfaceFlinger takes it as is, and the + // BGRA-to-RGBA blit (which swaps an RGBA-declared source) must not touch it. + boolean convert = sfCompatMode && !g.isScanoutRgba(); + nativeSetWindowBuffer(windowId, ahbPtr, acquireFence, 0, 0, g, g.getLastUsedSlot(), convert); if (hudRef != null && skipFPSCount.get() >= 1) { hudRef.update(); skipFPSCount.set(0); diff --git a/app/src/main/jniLibs/arm64-v8a/libahbimage.so b/app/src/main/jniLibs/arm64-v8a/libahbimage.so index fa520f8476ab9682e2372887a2a3c19ccae5cb3d..5d1f9f574e006665a01eb5a8507a9c180944146f 100755 GIT binary patch literal 13776 zcmeHOeOOf2nLjgwG9n@oqDD#P;>Q+!&GWrWsb zpZ#N>r-$dxd*1K&yyx6=&$;)o%~n!mG#HpFCf3D9FvhHs(X&uV5G|KY!tYErolOKe z0+&^?!{LB5rl48M5t;PHD_Wf3D5xBe-UM8Vu2o7o3Dc$uTtU4a#acc`@XHU6hHH!h zKX$u-N{`5-S3gJ4>kp5H-LicOs`gd99duIrZq}bP!^)-JCE1Q}xL3Gm3aT7_0eTeY zr!AyUS1vVl40ueCnS#n6wew+I6nCE28SuHCY!>Q)j2tJxXD;cPIg8SAGO|9a0g8`e zGvQiMx|*#!{*zVP?>IMYJbM)XSj^aQ=rlaWlJ|^54~#?KKMwr|w9!R??4R+)ZOsSgb=3 zRL9eFL-ZDTU$;p6XC-~>7lnK*bvVkc(oU6K;8jw-MAF-<1zp*HK+=C5M(>mKLKzQ@ z15u1j{24Ij4XBU&3d)VYaTtV3zjg}ET1GaT1}3jD79%0uH)bo-*dq~hjA;>Um%$oE zJnGkqhrWF{>6@pPbhbWq_T#SxGp`RtEjsb$V=umw`1~%bR`$*duMTZodbB8p+3mH> z4*NQ{*WqzL;F72#=xTNcfdpK2?#7@iV0Q%9`2y?Ny^f6zd#$g*zRB(NID)n z01|n{`70Y7buN3oBjDWR2)Oc_)~#~|iUPidN?+}ISCDNAxPva1S3HUj1GO8A9bTu$ zMWU;{?q(@&w}VO(gx%w-v-<;XZ}0|Xr>n6x;I47qRA99}|K`!oS*^#{cw(AEBtp@hCCdiV|5{QDUpym?06TM@iS$xao!?qb48r7OsP`=uJ zf5plwo4q2h!dCJ{lB~3qRzk9HWreMv%3hdPm8a8B*VS{QScR*>7j%89P>LLGkIShA zeHv~rEfkE_;0o6JoZ8Alorg?aJ)I*X0hbk3*-E3B$W_4&v*i-a?cIpSgi~|68(iK- zx6j+S5EByJ%|d52pCd<43rjRH$7t(#l*qPoBzNih#-}5bp{%Aa4h1sCG&)u&FswC? zw!VlAQu``wd4;?;-R!1JIOSF+E zs0`pQ^x%zsF@A5;6hFGxC`CmTd8=$HR*|)W)%i9%@t;qFc*RW)qnU7=~^6=hYpe=%kX&_-KnRTLY2=h1UhtMxb<8__3N`?$Qd zE*{#-LR*>LR$5S2s2@^)WrWK?q}{cQCMYyYYih*fWNBLS{Iv9JPqVJk>7(+5Nn|80 z-icwuPtULAcbCRP&)*!Bt2RCb8)lpC-i0~z7F_ofFxC!O0{8-86<`nGTEI(ye!v01 z#{r}9V0sF09$}7zBK|95R3dfIWZ|t(Z%iz1x|7=kT1ybJzNQwi+#rU zbi`I6_1oV-CJomm$X$gT--{K-REP2@v$;0fQZ2G6+CjEw8qr4>Q(34Z1}pN%ZI?$| ztVkWVO>$PqX*VHP54i<@MUKYAF39CVPCq6n1p7SG&n%khXV%Te*e6UZp73sy{pSRS zhKFYmtY+-^WfSXUM$69?uMarsvEkuZg87?_v5!f8!q03J`0yFSYeQQiIzGC8LdVsz zi5){GLz|gd2R||yQ$B=^t0qII`9bR-Gc1Q~cD&wb9VFW?4G&+=&t1@V2Cz2%?l$uy z*1=v=Tqnul{yKgQGD)nZ;!qrWt=BX?We)1?XDqpX6WeQvU{?+qS=FHe!8>KL40u6r zF|jO)E${WASZ@E=qPD5%4x$}8FKG$)v|&==G=rjLrM6*>$!U)Nl5$VwwsphjTG;J1 zSvs$l6`_r`j-f4u9cXhK<>{cwKF~LpwcQVyOEdD4-md4SFXiB3LHA~AZ=Wf?^MdKtPV+U9ql+`FCwc5icZYQEhA#OUD~6#hFLr!5 znGFt^SR3V=xBn;7|I16cjf?P;0vq))*4>s#>`FI=v}Fprl4oF5+*ixpZJ5&HdHqO6 z8`((wkD*wKJ%Q^bOtZAI4P5Uu+Yr*5w{S$SdI8sSoE3U8&^sSt&Dk@dIHz*rnw+>u zf6n>HmYhB26FHTWx^v>9&gPtt>dDzNnRQi0_vXY+>CZVo<#NuRn1P(isV!Y`v5$70 zkKNU^=azk4mD3J(#l^LEosT=#wI}{WSLLnUU2)UTcAcNz)3s;DFS;sc_IAbH*57sh zw#!|6W({=l_+OSe@GRmv_;9jyaA?a*?fpj4&xvOw|8fTBUoaxx>t)$Ie=kX1PY&n3 z30~ZFJLfr0OWxikocB6-m4ll&ueDq9&WgVKDR_GZzQTEjPD$Rg_i(*`Cx0KG;=I!@ zO5U*roOg=+ef$dN_5DclG|`R|ce+dRx>LE$uaLhhUwNKl>3d%CnuWg>@^|?goM%wHOCr~s$=~}=a$e$dDi)Em z0QvjV0nW=iE_qg=x1Rj{>7O{y@h_5>E85^Be}DV|=e7Q`{Q=u719y)Mo>^h3#O z7jfQ4{@y*sd8dy_-ar~}Um5v(_Z80T`+?+b75!IC{{HYP=NT05vdBvw`TPAFoR|20 zsTaSD+gw8a`rqQbyrYum7h^Y*{Pq8i^Bm7g-hy6;`1AQiA=QUGg%EzXODFdbnDTA1!%-<@&^C1Gy2UF*D@M+FZGFb<&g4b)h4RhkG zPV>_R{F!mvau&Bdl5IGLx$4bjEN&a>>rt;Xu;W>{u7hU(PWJk9nA7@~S@+CbZryF( zmbBkuHow~mxh6J{(`&jlV5ccJkB?%K+8{do=^gx@cl4eEc4ixk<|5&x;=L-$8Dk+ks~T z=47(-Dro&C>q(1Y_WHSI#2HET+t#0nWL59Yww^oydM)ZdUdE=i@5o!cJTYf|i{bAd zm~=av)<5IGtRF|RtadZYKD*4i`}c^&ysdq|C6Qf03{{=R_;dDFc4Y_ZliUyP{%SsJ z`yQ_MOp`jTk8C^oIOOhaWo^cYG~ZlY^Air0MpUJh^*$NtO9Z@-6X|;!^^1U)HbwgS za9I-$mewSEx0EFuDb0g?^&`uVu7{1*q2VfvVbZ(ZF|6v&YY(0O!vS-K*)Zu`hjH@G zowz(U>k0D&cI9hdimm**Au8(e+hZ$_8e&qOHAJT@u(1<{;o+(}`J7FBQs&LSkmqeX zawMLImd3nAeAMKfN28*4o-$5;_w(CC+}kr&UH{|f4$Q)uJRUK(y>FPj zGu9Zr6aDh;bAWr%o)+}UYv_~ZbJ&I-&Ob0~17q1&;0tT<_$#IujMZ4gHMLWFc-ztR zIqXVe%l|IV?BV4{8EjCV-$I_rhxW;ERpLhvoliI1a_*qfu(K7{oI>k~^oi_B!P?l$ zLKYR3pAcJF%OX;0m@&mzDDr#``J?f2KiazwIhc-1sKvlGGyon*V1W*-FD}lVkWA~meqLq3-qu%>67N6BM@Eg zC3@suqGx0;5zn717%yCo*4r2O^F`W`>+Sxz)|0=VYCTEz523Hj4^Eo>80;FG?x;+9 zLOg@mC&SO7k9+d}ds!RaInz41Cm;I=xn}Ok$NquFOsjt=wi{y^uMk3W8TOcaU%|am zgJ;#SN#vEDjiPIG)bkq%--`>7 z!?S@O*5ONlzof&L0l%Qb^MMs9 zsVE%#s!@1u7@i-7mxbZ45%zY3}yLgfD%szWAPQhH|g z;`Bv}au#TJ`s;iPw7Y72o*)oUkdOus(Q5G7)T#NrT3}H|Ms9kJGnroJdh4`}`09ht z9a?&BYFcWRmQ)`M`Wu%lT&TVX*7P7D#%Pq^tYUD(QYnS7~|%_Q&e?4j$Z;&^Z{6;^+IgXgn}1?ICWbdm`03a{SSUq1oez((j1QU&Walo7flGaCsQZ(;R^6-B`J0H%#5 zaV!&g7>iyu4&A}&BY9{6PUV?#dotwRl zn;TdvQRQqt3b6+STJLDA7t%pcaFoVDs^b_3S+4_ zI|V5&N`hnqIxH1utlV}-AmG@{?K>LWwQ%VR3Q2!PR7(GMqi5+$jbkI=) zSdTC5q9!1#4Fqd&Jj-iu^rhC~td`GfcY4Hurg_b&>7yui9M*ENdCt@f`pKl9Ec#hQ zKiQ0O7&>hwatL~wp!3#p2`^eJXgKu6yw3G@l}W1eT-&`(4K=O+bEv~FwG6?eF=tbQ z-wvXkf5RlN>GUJRv5T+D_(WQ0w+p7-?rikoEEydPlc&;jhtymRO+iniy~!Ioptjz% z8Diq#np6weHw7GiKSiU?v(Z^MiTH!{`1I*iC)@sZ)@|g>9`Z_`joIJMx``8wl#J15 z7;frV+5dyHZuY`2mgcQmSs-WS&kpdoW3)%XV}qCQbKz>;CfQiCj4rnV)$6PEn1VA$ zi{GH11U~JBlz+89QqU?Ho8?E4K&)zD1EixQIPml@Oa`y$tg(dUD_KdTy?DSM7K})>ZuD z{r`i~56}OjVftzxqF}o+EDfmmmA&0kU+vG;eoOtoNmkgpI^6%hF#XWJI8k`GtQ<)g z8;0W@AkT&BA9cP|{U1qqyy18!^l7iC^ws--HFJccaDU-wvSi!l(X^X3Avh$v$o!P-mLI82i%sY@!!E4TEh)1L)IWhLz%brU*-9)%{my&$ z%I_&Q1a_y>>yghr=YHqhbI<*M?>%H~tkdgsOe6z)nayyFnRtPFl|YcJlBMGRx7gKe zKEx^bnRszB9p=UaP31Te&*S;o$l!8CDyXP{^r%iDA8hA1PWR{)e#at(3Oy2&UWnTb zalD|T+)qJ;pDphPoYcNct&f{w&0No$rTCFd4=Q3NDAhsfR_KwRUpJAyY=gI-CDBvd zprE2YYUlUxBfrg^PQTCNWY;3^FR5^XeB)GHT2Wq9QBw9z4Um7?_!;oqxOpo(cSl3t z@84Ydn&@?GTHp9G+ti*bX8}-m%qCwlhuk}dyl)Qq59W}+IEVbrIpp6$ z17`Dc@f`BvIppkG|9=bsh9N_XoP{NO1r3f1Nxc(0=e$%UxS$n{>#}feKceQ)E z11`VK9@y#g?_xLGciU|aUx)2pPp8)&@cDu0bh`XNnCojAI_&K(o7?Vp-fQ=}YI=6= zbouN2zK$(E$1WF{vnwl!&FgEob@@G=B)iw|3Ak(y_bz6x*J#4DV|P7Nye=}bwbQeQ z8%hQ`UEL19r;WVt4%i*L$eL}Z-Q&F^e;vNAUTw|H;q`U9F7n5VW?Urf>6FDbTZgN| z(bX$AUW&s?Lz5i7 z6h(^9H&APL1-1nI9_OWb*J%%UcDpvZ0=Ijd0rw>}D4|mC^0d1HUqi=2!=uj0yq-2k zLAQ@JHq=<;f40pe!{{a{a(ujV&bqOY=QcK4x9l$AOb4euu|`Z`FP6$GBi7Rnd!YJ;9^<_>8!=o;S0FFmMe8wQe94Cz-PqdG$x!ecDMp=pVQb-tE!WXggO3bXHwmtqMb;UTsuow(lzA%zn%r_>vyHxG(1JZQ3BTy+$FbYcbzq6}~D_vuv%g z+7LHtPF+jW=GH`86ytNdE*~5m@INsM%E%<|f#M<+qs|vss0xr5CmyI$ zM>cJ-w%D3(v$iyv<;lp+VQl1W$-hlK-|{fJdt8{^`J^g_4WAW8V{=PWE9Ni8j0J79 zZf;Fy+L=eYuF>JOcXy*taK>_VI$W}A4Yk%Ln{~6Lsa8FtzG8&)L1esXJuOgZl(DB9 zyH9n|o*RmaS9|xU8eOc36BdyfzvNC#8h+|><-B8*U9ag4@YA8ZWb1MaYw6s51%4^m zY=WTKpmZl)3_1$B4D>u`C1@116*LnYnGKikc7>}eJo&R`hx?ztYGZ&JD>>V;I`>+Tx+>Q-WzRziW7}O>CPz&tG*fe*b z4){W6w@KL=L6C#quLoCyJW z$85_CZ{->dZ<&tiv))Ky*~AYUZ0Cqh$K%%!-GS@#lz~0X^ttCm**JL8WAS(v(V8dp zStDGZ_|r!fK5j9bo!*-{G;J_Q^`mRwO1;_?3l8K)$G**CMEEN3x1 z7s*c8c?5Q#D`U46NR!ZUlvPWz-K${tDy`lAx47NbBH8W_Rd#QK-8|G4?9Q#cP+9YyM^!UKLdz8L2g~u-k58JYh`f8~Ly1{|FqtsXA z=h$*nZ0s@?6Jug%z!pu_rF}HQV>fo0DK@>gaR}o_qA^GM@d1Xhq#K&HFo|?0fg#_> zXZyeKHlN=hEeAG&veX5p$5PYSZ}-eHMlMfuKyIrMXIGy4X#gvGyem}{bIfJGYu}J!Rh{<<3G$E~Z9f*RbY24{YWi)hMUK0_N;;^) zjcM$S{2j-AzE*lvgNtZz{XgZnyh5o@gPYRe%s=6{;c{uW1{c-fbSF7(tVr@|aD6YS z+cEOD9Cv7~)TY7h)8P7_v(MP=o6aa$M#HsYru6sll0_;O*yum(4B zg5!=&XPIrvkJ5VP5w+1(+ z!Hqn@agC+YhZm>Hp z^Ut&1_n%?CA3Vi+|Fa2ag|+;w@Yj~uIQCt6zrk4|A%9#Y$N3_HJvWjAiZe!@L4FO8 z9mz4p&ggmm`Qcvdzu(=YSN7k?GYbE6zGp<98t%P77+u<~l|qIy%7v%;dOv)ouQ&gJ z6^B#P(muLJlr@>cK^Co`c{qmrAs(Mp)^}y(Ar?Nw_nI{DLzdjoB=&McKKzGI6Tqj= z?`s)1q^>xDef>|NJ66cTXhV1(-`^*Zr~PjH&v~5d*T{SPJ-~`O9uoUmKH84+4%w#t zz5|%xz+It8E^MMrLx$}kY8Py`lE2jUA&Vlz`CO6Zn!=*}uX3DR|9)QofnvG-o5@$y zGmiE+RK9E{yQu2|;z)HZHE_EmgKd)ai`Zk&{s`xY57Ac_p6~1ZSdQ8A#=se4`d!*k(R!n{E+L{DfK^N_><_< z1B;>;ET~_HxDITKPFQf(G29W&NByEsjHxTghcTR?<*`70P|D8Y41bW$QaIa&aYiqr zvPtAArsjSg(?gYVOh473zbv+p7*B)9r|>#NU*oEkO&A(may<{3!Z;g+lh0M-Gv})C z=lkhw)r@(WBG1c@P|uv_bj+P`oW&=aSnuQ})*ET!^Fxe}O%z-9-DmmOm_+$`$f!Ld z_wn|;P%5`)1og=0b_S`O@zI(cH%Si?X00i)j;sVyzzO z;B6mUC4GndLD}~}X&g=Otr>z{W!<*uU_S+SIGyGWH;6mPIM$a5>O<-m#3h0@2bpqb zqcIvZ~O5u~RCB{41B|irZHrTv) zypM?So?!}4@2$l>LMOMSNRF2t?MX+x^hxc>iH>6oPAo?|mZLowgS2nBRd>*(PVU>O z)v|AyuqTf*!{R6&%`10MzJ}}3ep=s8SlHQXfm7en#_h75shT5*F~;p@xKjx`+qfMX zw+woNbVn5TW6T42&!fE!VU&1~bo@%!=n{GLW@GTrBbXH(58V~v{{-wEH@qj`6@!Mm zLqoVHraq9CliG9LmI(H~2-cf~Z+~gQ-erqU;6AUuzhgWno(uHf&SSY4eT-N>rSfMO zc^W5l=O=%ZF|$4T)pyUEG5+=Pm^ls`@zXR1=3~rg=fD`ozkFBbW1r45H1EV5h{T_0 ziP%^#-Zf>@opu~^YCpwilPOGc@@LFu>YZ)PPK@EpXbjiEA7u<h%tf#S&?1?<+&V9Ju1m+E`ZG zjgk@1z-WyUKe_B*QTDJ>CiCaOKdIt>4gOUX{~q`W6+Z!9zg)I0>wf_LdKLd6_!<@e z3HUY@|6kxAR`I_B|D=ll4E(Dq{tNIEDqe3z{dqI>&j)|Kicbe$qwtBqBxMjoIo)bd zsNnBR;yp=xcM^~Lv7Chdz9gQ$TcIT0iAtQuGbuTxa{Bvt+(%hqN4)FZhiA@|1g>6T z85c8p&Q2-D^|^U=jUr~F9Ef|IoUk{@N#OQ*6ka@Q7xjvF(8F9&yc-tv(c^MT5uTA> zP=FughCls6Y0n{EPT$~A67S>1bNnnt%%-^iMI8SH=bfC7C&h=WeI+@712(1qYbrhl zH0BkTu3l4IUS6@vxU#F=hyQIpZvey_Ag04hvNn8N;xzg?jsEhIlFH%=XFk2Q>TEae z#@lVY)-@Ja78Dhf8S~tMKv#EFVWIe7qTT0f_qw`$J${F)0N)!G27ErR!)^C;7J9wA zJ8tUo`|fc$0!C+LQBkSAtbDbzxXfPabhT9$m6ti3c6)irnw>k_b{3U5*R++aF3F$G z@Bc^MHF`RCx%^ee#>QG66UGYgQI^MM_xtU=tN9=>dY;GsG_RgLH-T^kSPJ6&kIt6|o*M*t}1q=-J zb|^x(z1uAV{XScl-Rbmnw$IeSU85@6dOTj|P55qXZ~tM%_a)@6-q_j SYT&~w74Cht5P6{|#QzKD-rP|D