From 59ef044ce0b6070257842f25bdb78c93b500a13a Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Mon, 5 Oct 2020 11:04:05 -0700 Subject: [PATCH 01/18] Add support to UiHelper for SurfaceHolder (#3155) --- RELEASE_NOTES.md | 1 + .../android/filament/android/UiHelper.java | 84 +++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a89e1c39cbd..8553f4185c7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,7 @@ A new header is inserted each time a *tag* is created. ## v1.9.4 - New `ShadowOptions` control to render Variance Shadow Maps (VSM) with MSAA (experimental). +- `UiHelper` now supports managing a `SurfaceHolder`. ## v1.9.3 diff --git a/android/filament-android/src/main/java/com/google/android/filament/android/UiHelper.java b/android/filament-android/src/main/java/com/google/android/filament/android/UiHelper.java index a170b12adfa..ae1bdcd7c0b 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/android/UiHelper.java +++ b/android/filament-android/src/main/java/com/google/android/filament/android/UiHelper.java @@ -30,8 +30,8 @@ import com.google.android.filament.SwapChain; /** - * UiHelper is a simple class that can manage either a SurfaceView or a TextureView so it can - * be used to render into with Filament. + * UiHelper is a simple class that can manage either a SurfaceView, TextureView, or a SurfaceHolder + * so it can be used to render into with Filament. * * Here is a simple example with a SurfaceView. The code would be exactly the same with a * TextureView: @@ -191,6 +191,23 @@ public void detach() { } } + private static class SurfaceHolderHandler implements RenderSurface { + private SurfaceHolder mSurfaceHolder; + + SurfaceHolderHandler(SurfaceHolder surface) { + mSurfaceHolder = surface; + } + + @Override + public void resize(int width, int height) { + mSurfaceHolder.setFixedSize(width, height); + } + + @Override + public void detach() { + } + } + private class TextureViewHandler implements RenderSurface { private TextureView mTextureView; private Surface mSurface; @@ -258,8 +275,8 @@ public RendererCallback getRenderCallback() { } /** - * Free resources associated to the native window specified in {@link #attachTo(SurfaceView)} - * or {@link #attachTo(TextureView)}. + * Free resources associated to the native window specified in {@link #attachTo(SurfaceView)}, + * {@link #attachTo(TextureView)}, or {@link #attachTo(SurfaceHolder)}. */ public void detach() { destroySwapChain(); @@ -315,8 +332,8 @@ public boolean isOpaque() { * Controls whether the render target (SurfaceView or TextureView) is opaque or not. * The render target is considered opaque by default. * - * Must be called before calling {@link #attachTo(SurfaceView)} - * or {@link #attachTo(TextureView)}. + * Must be called before calling {@link #attachTo(SurfaceView)}, {@link #attachTo(TextureView)}, + * or {@link #attachTo(SurfaceHolder)}. * * @param opaque Indicates whether the render target should be opaque. True by default. */ @@ -341,6 +358,8 @@ public boolean isMediaOverlay() { * Must be called before calling {@link #attachTo(SurfaceView)} * or {@link #attachTo(TextureView)}. * + * Has no effect when using {@link #attachTo(SurfaceHolder)}. + * * @param overlay Indicates whether the render target should be rendered below the activity's * surface when transparent. */ @@ -403,7 +422,9 @@ public void surfaceDestroyed(SurfaceHolder holder) { SurfaceHolder holder = view.getHolder(); holder.addCallback(callback); - holder.setFixedSize(mDesiredWidth, mDesiredHeight); + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + holder.setFixedSize(mDesiredWidth, mDesiredHeight); + } // in case the SurfaceView's surface already existed final Surface surface = holder.getSurface(); @@ -483,6 +504,55 @@ public void onSurfaceTextureUpdated(SurfaceTexture surface) { } } } + /** + * Associate UiHelper with a SurfaceHolder. + * + * As soon as a Surface is created, we'll create the + * EGL resources needed, and call user callbacks if needed. + */ + public void attachTo(@NonNull SurfaceHolder holder) { + if (attach(holder)) { + int format = isOpaque() ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT; + holder.setFormat(format); + + mRenderSurface = new SurfaceHolderHandler(holder); + + final SurfaceHolder.Callback callback = new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder surfaceHolder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceCreated()"); + createSwapChain(holder.getSurface()); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + // Note: this is always called at least once after surfaceCreated() + if (LOGGING) Log.d(LOG_TAG, "surfaceChanged(" + width + ", " + height + ")"); + mRenderCallback.onResized(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder surfaceHolder) { + if (LOGGING) Log.d(LOG_TAG, "surfaceDestroyed()"); + destroySwapChain(); + } + }; + + holder.addCallback(callback); + if (mDesiredWidth > 0 && mDesiredHeight > 0) { + holder.setFixedSize(mDesiredWidth, mDesiredHeight); + } + + // in case the SurfaceHolder's surface already existed + final Surface surface = holder.getSurface(); + if (surface != null && surface.isValid()) { + callback.surfaceCreated(holder); + callback.surfaceChanged(holder, format, + holder.getSurfaceFrame().width(), holder.getSurfaceFrame().height()); + } + } + } + private boolean attach(@NonNull Object nativeWindow) { if (mNativeWindow != null) { // we are already attached to a native window From 0bec65701a1cbe996f91223d5cbf79bf311b9f0a Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Mon, 5 Oct 2020 16:03:10 -0700 Subject: [PATCH 02/18] VSM: render both shadow casters and receivers (#3147) --- .../android/filament/RenderableManager.java | 8 +++++ .../com/google/android/filament/View.java | 8 +++++ filament/include/filament/RenderableManager.h | 6 ++++ filament/include/filament/View.h | 6 ++++ filament/src/ShadowMapManager.cpp | 6 ++-- filament/src/View.cpp | 33 ++++++++++++------- filament/src/details/View.h | 32 ++++++++++-------- shaders/src/shadowing.fs | 5 +++ 8 files changed, 75 insertions(+), 29 deletions(-) diff --git a/android/filament-android/src/main/java/com/google/android/filament/RenderableManager.java b/android/filament-android/src/main/java/com/google/android/filament/RenderableManager.java index 0f93df775cf..15ef520210c 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/RenderableManager.java +++ b/android/filament-android/src/main/java/com/google/android/filament/RenderableManager.java @@ -268,6 +268,14 @@ public Builder culling(boolean enabled) { /** * Controls if this renderable casts shadows, false by default. + * + * If the View's shadow type is set to {@link View.ShadowType#VSM}, castShadows should only + * be disabled if either is true: + * */ @NonNull public Builder castShadows(boolean enabled) { diff --git a/android/filament-android/src/main/java/com/google/android/filament/View.java b/android/filament-android/src/main/java/com/google/android/filament/View.java index 3c3a83ef4e1..a3cf9719333 100644 --- a/android/filament-android/src/main/java/com/google/android/filament/View.java +++ b/android/filament-android/src/main/java/com/google/android/filament/View.java @@ -1168,6 +1168,14 @@ public void setDynamicLightingOptions(float zLightNear, float zLightFar) { * * The ShadowType affects all the shadows seen within the View. * + *

+ * {@link ShadowType#VSM} imposes a restriction on marking renderables as only shadow receivers + * (but not casters). To ensure correct shadowing with VSM, all shadow participant renderables + * should be marked as both receivers and casters. Objects that are guaranteed to not cast + * shadows on themselves or other objects (such as flat ground planes) can be set to not cast + * shadows, which might improve shadow quality. + *

+ * * Warning: This API is still experimental and subject to change. */ public void setShadowType(ShadowType type) { diff --git a/filament/include/filament/RenderableManager.h b/filament/include/filament/RenderableManager.h index b042408f949..fffd7733d1e 100644 --- a/filament/include/filament/RenderableManager.h +++ b/filament/include/filament/RenderableManager.h @@ -218,6 +218,12 @@ class UTILS_PUBLIC RenderableManager : public FilamentAPI { /** * Controls if this renderable casts shadows, false by default. + * + * If the View's shadow type is set to ShadowType::VSM, castShadows should only be disabled + * if either is true: + * - receiveShadows is also disabled + * - the object is guaranteed to not cast shadows on itself or other objects (for example, + * a ground plane) */ Builder& castShadows(bool enable) noexcept; diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index 2331766dccf..4e649a0b397 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -692,6 +692,12 @@ class UTILS_PUBLIC View : public FilamentAPI { * * The ShadowType affects all the shadows seen within the View. * + * ShadowType::VSM imposes a restriction on marking renderables as only shadow receivers (but + * not casters). To ensure correct shadowing with VSM, all shadow participant renderables should + * be marked as both receivers and casters. Objects that are guaranteed to not cast shadows on + * themselves or other objects (such as flat ground planes) can be set to not cast shadows, + * which might improve shadow quality. + * * @warning This API is still experimental and subject to change. */ void setShadowType(ShadowType shadow) noexcept; diff --git a/filament/src/ShadowMapManager.cpp b/filament/src/ShadowMapManager.cpp index 7b10396d7c9..77514bf059b 100644 --- a/filament/src/ShadowMapManager.cpp +++ b/filament/src/ShadowMapManager.cpp @@ -111,7 +111,7 @@ void ShadowMapManager::render(FrameGraph& fg, FEngine& engine, FView& view, continue; } - pass.setVisibilityMask(VISIBLE_SPOT_SHADOW_CASTER_N(i)); + pass.setVisibilityMask(VISIBLE_SPOT_SHADOW_RENDERABLE_N(i)); map.getShadowMap()->render(driver, view.getVisibleSpotShadowCasters(), pass, view); pass.clearVisibilityMask(); @@ -288,7 +288,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateCascadeShadowMaps( layout, cascadeParams); Frustum const& frustum = map.getCamera().getFrustum(); FView::cullRenderables(engine.getJobSystem(), renderableData, frustum, - VISIBLE_DIR_SHADOW_CASTER_BIT); + VISIBLE_DIR_SHADOW_RENDERABLE_BIT); // Set shadowBias, using the first directional cascade. const float texelSizeWorldSpace = map.getTexelSizeWorldSpace(); @@ -447,7 +447,7 @@ ShadowMapManager::ShadowTechnique ShadowMapManager::updateSpotShadowMaps( UniformBuffer& u = shadowUb; Frustum const& frustum = shadowMap.getCamera().getFrustum(); FView::cullRenderables(engine.getJobSystem(), renderableData, frustum, - VISIBLE_SPOT_SHADOW_CASTER_N_BIT(i)); + VISIBLE_SPOT_SHADOW_RENDERABLE_N_BIT(i)); mat4f const& lightFromWorldMatrix = view.hasVsm() ? shadowMap.getLightSpaceMatrixVsm() : shadowMap.getLightSpaceMatrix(); diff --git a/filament/src/View.cpp b/filament/src/View.cpp index d703c006a23..c121affbd2a 100644 --- a/filament/src/View.cpp +++ b/filament/src/View.cpp @@ -463,17 +463,17 @@ void FView::prepare(FEngine& engine, backend::DriverApi& driver, ArenaScope& are uint8_t const* layers = renderableData.data(); auto const* visibility = renderableData.data(); computeVisibilityMasks(getVisibleLayers(), layers, visibility, cullingMask.begin(), - renderableData.size()); + renderableData.size(), hasVsm()); auto const beginRenderables = renderableData.begin(); auto beginCasters = partition(beginRenderables, renderableData.end(), VISIBLE_RENDERABLE); auto beginCastersOnly = partition(beginCasters, renderableData.end(), - VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_CASTER); + VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE); auto beginSpotLightCastersOnly = partition(beginCastersOnly, renderableData.end(), - VISIBLE_DIR_SHADOW_CASTER); + VISIBLE_DIR_SHADOW_RENDERABLE); auto endSpotLightCastersOnly = std::partition(beginSpotLightCastersOnly, renderableData.end(), [](auto it) { - return (it.template get() & VISIBLE_SPOT_SHADOW_CASTER); + return (it.template get() & VISIBLE_SPOT_SHADOW_RENDERABLE); }); // convert to indices @@ -555,7 +555,7 @@ void FView::computeVisibilityMasks( uint8_t visibleLayers, uint8_t const* UTILS_RESTRICT layers, FRenderableManager::Visibility const* UTILS_RESTRICT visibility, - uint8_t* UTILS_RESTRICT visibleMask, size_t count) { + uint8_t* UTILS_RESTRICT visibleMask, size_t count, bool hasVsm) { // __restrict__ seems to only be taken into account as function parameters. This is very // important here, otherwise, this loop doesn't get vectorized. // This is vectorized 16x. @@ -573,18 +573,27 @@ void FView::computeVisibilityMasks( // else: // set all bits in visibleMask to 0 // if !v.castShadows: - // set shadow visibility bits in visibleMask to 0 + // if !vsm or !v.receivesShadows: // with vsm, we also render shadow receivers + // set shadow visibility bits in visibleMask to 0 // // It is written without if statements to avoid branches, which allows it to be vectorized 16x. - bool visRenderables = (!v.culling || (mask & VISIBLE_RENDERABLE)) && inVisibleLayer; - bool visShadowCasters = (!v.culling || (mask & VISIBLE_DIR_SHADOW_CASTER)) && inVisibleLayer && v.castShadows; + const bool visRenderables = (!v.culling || (mask & VISIBLE_RENDERABLE)) && inVisibleLayer; + const bool vvsmRenderShadow = hasVsm && v.receiveShadows; + const bool visShadowParticipant = v.castShadows || vvsmRenderShadow; + const bool visShadowRenderable = + (!v.culling || (mask & VISIBLE_DIR_SHADOW_RENDERABLE)) && inVisibleLayer && visShadowParticipant; visibleMask[i] = Culler::result_type(visRenderables) | - Culler::result_type(visShadowCasters << 1u); + Culler::result_type(visShadowRenderable << 1u); // this loop gets fully unrolled for (size_t j = 0; j < CONFIG_MAX_SHADOW_CASTING_SPOTS; ++j) { - bool vIsSpotShadowCaster = (!v.culling || (mask & VISIBLE_SPOT_SHADOW_CASTER_N(j))) && inVisibleLayer && v.castShadows; - visibleMask[i] |= Culler::result_type(vIsSpotShadowCaster << (j + 2)); + const bool vvsmSpotRenderShadow = hasVsm && v.receiveShadows; + const bool visSpotShadowParticipant = v.castShadows || vvsmSpotRenderShadow; + const bool visSpotShadowRenderable = + (!v.culling || (mask & VISIBLE_SPOT_SHADOW_RENDERABLE_N(j))) && + inVisibleLayer && visSpotShadowParticipant; + visibleMask[i] |= + Culler::result_type(visSpotShadowRenderable << VISIBLE_SPOT_SHADOW_RENDERABLE_N_BIT(j)); } } } @@ -598,7 +607,7 @@ UTILS_NOINLINE // Mask VISIBLE_MASK to ignore higher bits related to spot shadows. We only partition based // on renderable and directional shadow visibility. return (it.template get() & - (VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_CASTER)) == mask; + (VISIBLE_RENDERABLE | VISIBLE_DIR_SHADOW_RENDERABLE)) == mask; }); } diff --git a/filament/src/details/View.h b/filament/src/details/View.h index c9b3b98cc04..8652956b953 100644 --- a/filament/src/details/View.h +++ b/filament/src/details/View.h @@ -71,26 +71,30 @@ class FScene; // The value of the 'VISIBLE_MASK' after culling. Each bit represents visibility in a frustum // (either camera or light). // -// bits 7 6 5 4 3 2 1 0 -// +-------------------------------------------+ -// VISIBLE_RENDERABLE X -// VISIBLE_DIR_SHADOW_CASTER X -// VISIBLE_SPOT_SHADOW_CASTER_0 X -// VISIBLE_SPOT_SHADOW_CASTER_1 X +// bits 7 6 5 4 3 2 1 0 +// +------------------------------------------------+ +// VISIBLE_RENDERABLE X +// VISIBLE_DIR_SHADOW_RENDERABLE X +// VISIBLE_SPOT_SHADOW_RENDERABLE_0 X +// VISIBLE_SPOT_SHADOW_RENDERABLE_1 X // ... +// A "shadow renderable" is a renderable rendered to the shadow map during a shadow pass: +// PCF shadows: only shadow casters +// VSM shadows: both shadow casters and shadow receivers + static constexpr size_t VISIBLE_RENDERABLE_BIT = 0u; -static constexpr size_t VISIBLE_DIR_SHADOW_CASTER_BIT = 1u; -static constexpr size_t VISIBLE_SPOT_SHADOW_CASTER_N_BIT(size_t n) { return n + 2; } +static constexpr size_t VISIBLE_DIR_SHADOW_RENDERABLE_BIT = 1u; +static constexpr size_t VISIBLE_SPOT_SHADOW_RENDERABLE_N_BIT(size_t n) { return n + 2; } static constexpr uint8_t VISIBLE_RENDERABLE = 1u << VISIBLE_RENDERABLE_BIT; -static constexpr uint8_t VISIBLE_DIR_SHADOW_CASTER = 1u << VISIBLE_DIR_SHADOW_CASTER_BIT; -static constexpr uint8_t VISIBLE_SPOT_SHADOW_CASTER_N(size_t n) { - return 1u << VISIBLE_SPOT_SHADOW_CASTER_N_BIT(n); +static constexpr uint8_t VISIBLE_DIR_SHADOW_RENDERABLE = 1u << VISIBLE_DIR_SHADOW_RENDERABLE_BIT; +static constexpr uint8_t VISIBLE_SPOT_SHADOW_RENDERABLE_N(size_t n) { + return 1u << VISIBLE_SPOT_SHADOW_RENDERABLE_N_BIT(n); } -// ORing of all the VISIBLE_SPOT_SHADOW_CASTER bits -static constexpr uint8_t VISIBLE_SPOT_SHADOW_CASTER = +// ORing of all the VISIBLE_SPOT_SHADOW_RENDERABLE bits +static constexpr uint8_t VISIBLE_SPOT_SHADOW_RENDERABLE = (0xFFu >> (sizeof(uint8_t) * 8u - CONFIG_MAX_SHADOW_CASTING_SPOTS)) << 2u; // Because we're using a uint8_t for the visibility mask, we're limited to 6 spot light shadows. @@ -423,7 +427,7 @@ class FView : public View { static void computeVisibilityMasks( uint8_t visibleLayers, uint8_t const* layers, FRenderableManager::Visibility const* visibility, uint8_t* visibleMask, - size_t count); + size_t count, bool hasVsm); void bindPerViewUniformsAndSamplers(FEngine::DriverApi& driver) const noexcept { driver.bindUniformBuffer(BindingPoints::PER_VIEW, mPerViewUbh); diff --git a/shaders/src/shadowing.fs b/shaders/src/shadowing.fs index 093bf4e2048..b42939b1c71 100644 --- a/shaders/src/shadowing.fs +++ b/shaders/src/shadowing.fs @@ -360,6 +360,11 @@ highp float shadowVsm(const highp sampler2DArray shadowMap, const uint layer, highp vec2 moments = texture(shadowMap, vec3(shadowPosition.xy, layer)).xy; highp float depth = shadowPosition.z; + // depth must be clamped to support floating-point depth formats. This is to avoid comparing a + // value from the depth texture (which is never greater than 1.0) with a greater-than-one + // comparison value (which is possible with floating-point formats). + depth = min(depth, 1.0f); + // TODO: bias and lightBleedReduction should be uniforms const float bias = 0.01; const float lightBleedReduction = 0.2; From f31eb09ca03781131644c0be078171f41df376e5 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 6 Oct 2020 11:03:55 -0600 Subject: [PATCH 03/18] Add Live Wallpaper Android sample (#3161) --- RELEASE_NOTES.md | 2 + android/samples/README.md | 6 + .../samples/sample-live-wallpaper/.gitignore | 12 + .../sample-live-wallpaper/build.gradle | 19 ++ .../src/main/AndroidManifest.xml | 25 ++ .../filament/livewallpaper/Wallpaper.kt | 224 ++++++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 34 +++ .../res/drawable/ic_launcher_background.xml | 171 +++++++++++++ .../src/main/res/drawable/wallpaper_thumb.png | Bin 0 -> 14879 bytes .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../src/main/res/values/colors.xml | 6 + .../src/main/res/values/strings.xml | 4 + .../src/main/res/values/styles.xml | 8 + .../src/main/res/xml/wallpaper.xml | 5 + android/settings.gradle | 1 + .../images/samples/example_live_wallpaper.jpg | Bin 0 -> 24803 bytes 27 files changed, 527 insertions(+) create mode 100644 android/samples/sample-live-wallpaper/.gitignore create mode 100644 android/samples/sample-live-wallpaper/build.gradle create mode 100644 android/samples/sample-live-wallpaper/src/main/AndroidManifest.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/java/com/google/android/filament/livewallpaper/Wallpaper.kt create mode 100644 android/samples/sample-live-wallpaper/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/drawable/wallpaper_thumb.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 android/samples/sample-live-wallpaper/src/main/res/values/colors.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/values/strings.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/values/styles.xml create mode 100644 android/samples/sample-live-wallpaper/src/main/res/xml/wallpaper.xml create mode 100644 docs/images/samples/example_live_wallpaper.jpg diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 8553f4185c7..135e62afe6e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,6 +5,8 @@ A new header is inserted each time a *tag* is created. ## Next release (v1.9.5) +- Added a new Live Wallpaper Android sample + ## v1.9.4 - New `ShadowOptions` control to render Variance Shadow Maps (VSM) with MSAA (experimental). diff --git a/android/samples/README.md b/android/samples/README.md index 6b0856b44c2..12a6d43e167 100644 --- a/android/samples/README.md +++ b/android/samples/README.md @@ -15,6 +15,12 @@ Demonstrates how to create a light and a mesh with the attributes required for l ![Lit Cube](../../docs/images/samples/sample_lit_cube.jpg) +### `live-wallpaper` + +Demonstrates how to use Filament as renderer for an Android Live Wallpaper. + +![Live Wallpaper](../../docs/images/samples/example_live_wallpaper.jpg) + ### `image-based-lighting` Demonstrates how to create image-based lights and load complex meshes: diff --git a/android/samples/sample-live-wallpaper/.gitignore b/android/samples/sample-live-wallpaper/.gitignore new file mode 100644 index 00000000000..71e173b003e --- /dev/null +++ b/android/samples/sample-live-wallpaper/.gitignore @@ -0,0 +1,12 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +/.idea/caches +/.idea/gradle.xml +.DS_Store +/build +/captures +/src/main/assets +.externalNativeBuild diff --git a/android/samples/sample-live-wallpaper/build.gradle b/android/samples/sample-live-wallpaper/build.gradle new file mode 100644 index 00000000000..ecb1988dc7b --- /dev/null +++ b/android/samples/sample-live-wallpaper/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +project.ext.isSample = true + +android { + compileSdkVersion versions.compileSdk + defaultConfig { + applicationId "com.google.android.filament.livewallpaper" + minSdkVersion versions.minSdk + targetSdkVersion versions.targetSdk + } +} + +dependencies { + implementation deps.kotlin + implementation project(':filament-android') +} diff --git a/android/samples/sample-live-wallpaper/src/main/AndroidManifest.xml b/android/samples/sample-live-wallpaper/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..20848404574 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/android/samples/sample-live-wallpaper/src/main/java/com/google/android/filament/livewallpaper/Wallpaper.kt b/android/samples/sample-live-wallpaper/src/main/java/com/google/android/filament/livewallpaper/Wallpaper.kt new file mode 100644 index 00000000000..8e21649fa72 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/java/com/google/android/filament/livewallpaper/Wallpaper.kt @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.filament.livewallpaper + +import android.animation.ValueAnimator +import android.app.Service +import android.graphics.Color +import android.graphics.PixelFormat +import android.service.wallpaper.WallpaperService +import android.view.Choreographer +import android.view.Surface +import android.view.SurfaceHolder +import android.view.WindowManager +import android.view.animation.LinearInterpolator +import com.google.android.filament.* +import com.google.android.filament.android.DisplayHelper +import com.google.android.filament.android.UiHelper + + +class FilamentLiveWallpaper : WallpaperService() { + // Make sure to initialize Filament first + // This loads the JNI library needed by most API calls + companion object { + init { + Filament.init() + } + } + + override fun onCreateEngine(): Engine { + return FilamentWallpaperEngine() + } + + private inner class FilamentWallpaperEngine : Engine() { + + // UiHelper is provided by Filament to manage SurfaceHolder + private lateinit var uiHelper: UiHelper + // DisplayHelper is provided by Filament to manage the display + private lateinit var displayHelper: DisplayHelper + // Choreographer is used to schedule new frames + private lateinit var choreographer: Choreographer + + // Engine creates and destroys Filament resources + // Each engine must be accessed from a single thread of your choosing + // Resources cannot be shared across engines + private lateinit var engine: com.google.android.filament.Engine + // A renderer instance is tied to a single surface (SurfaceView, TextureView, etc.) + private lateinit var renderer: Renderer + // A scene holds all the renderable, lights, etc. to be drawn + private lateinit var scene: Scene + // A view defines a viewport, a scene and a camera for rendering + private lateinit var view: View + // Should be pretty obvious :) + private lateinit var camera: Camera + + // A swap chain is Filament's representation of a surface + private var swapChain: SwapChain? = null + + // Performs the rendering and schedules new frames + private val frameScheduler = FrameCallback() + + // We'll use this ValueAnimator to smoothly cycle the background between hues. + private val animator = ValueAnimator.ofFloat(0.0f, 360.0f) + + override fun onCreate(surfaceHolder: SurfaceHolder) { + super.onCreate(surfaceHolder) + surfaceHolder.setSizeFromLayout() + surfaceHolder.setFormat(PixelFormat.RGBA_8888) + + choreographer = Choreographer.getInstance() + + displayHelper = DisplayHelper(this@FilamentLiveWallpaper) + + setupUiHelper() + setupFilament() + setupView() + setupScene() + } + + private fun setupUiHelper() { + uiHelper = UiHelper(UiHelper.ContextErrorPolicy.DONT_CHECK) + uiHelper.renderCallback = SurfaceCallback() + uiHelper.attachTo(surfaceHolder) + } + + private fun setupFilament() { + engine = com.google.android.filament.Engine.create() + renderer = engine.createRenderer() + scene = engine.createScene() + view = engine.createView() + camera = engine.createCamera() + } + + private fun setupView() { + scene.skybox = Skybox.Builder().build(engine) + + // NOTE: Try to disable post-processing (tone-mapping, etc.) to see the difference + // view.isPostProcessingEnabled = false + + // Tell the view which camera we want to use + view.camera = camera + + // Tell the view which scene we want to render + view.scene = scene + } + + private fun setupScene() { + // Set the exposure on the camera, this exposure follows the sunny f/16 rule + camera.setExposure(16.0f, 1.0f / 125.0f, 100.0f) + + startAnimation() + } + + private fun startAnimation() { + // Animate the color of the Skybox. + animator.interpolator = LinearInterpolator() + animator.duration = 10000 + animator.repeatMode = ValueAnimator.RESTART + animator.repeatCount = ValueAnimator.INFINITE + animator.addUpdateListener { a -> + val hue = a.animatedValue as Float + val color = Color.HSVToColor(floatArrayOf(hue, 1.0f, 1.0f)) + scene.skybox?.setColor(floatArrayOf( + Color.red(color) / 255.0f, + Color.green(color) / 255.0f, + Color.blue(color) / 255.0f, + 1.0f)) + } + animator.start() + } + + override fun onVisibilityChanged(visible: Boolean) { + super.onVisibilityChanged(visible) + if (visible) { + choreographer.postFrameCallback(frameScheduler) + animator.start() + } else { + choreographer.removeFrameCallback(frameScheduler) + animator.cancel() + } + } + + override fun onDestroy() { + super.onDestroy() + + // Stop the animation and any pending frame + choreographer.removeFrameCallback(frameScheduler) + animator.cancel() + + // Always detach the surface before destroying the engine + uiHelper.detach() + + // Cleanup all resources + engine.destroyRenderer(renderer) + engine.destroyView(view) + engine.destroyScene(scene) + engine.destroyCamera(camera) + + // Destroying the engine will free up any resource you may have forgotten + // to destroy, but it's recommended to do the cleanup properly + engine.destroy() + } + + inner class FrameCallback : Choreographer.FrameCallback { + override fun doFrame(frameTimeNanos: Long) { + // Schedule the next frame + choreographer.postFrameCallback(this) + + // This check guarantees that we have a swap chain + if (uiHelper.isReadyToRender) { + // If beginFrame() returns false you should skip the frame + // This means you are sending frames too quickly to the GPU + if (renderer.beginFrame(swapChain!!, frameTimeNanos)) { + renderer.render(view) + renderer.endFrame() + } + } + } + } + + inner class SurfaceCallback : UiHelper.RendererCallback { + override fun onNativeWindowChanged(surface: Surface) { + swapChain?.let { engine.destroySwapChain(it) } + swapChain = engine.createSwapChain(surface) + val display = + (application.getSystemService(Service.WINDOW_SERVICE) as WindowManager) + .defaultDisplay + displayHelper.attach(renderer, display) + } + + override fun onDetachedFromSurface() { + displayHelper.detach() + swapChain?.let { + engine.destroySwapChain(it) + // Required to ensure we don't return before Filament is done executing the + // destroySwapChain command, otherwise Android might destroy the Surface + // too early + engine.flushAndWait() + swapChain = null + } + } + + override fun onResized(width: Int, height: Int) { + val aspect = width.toDouble() / height.toDouble() + camera.setProjection(45.0, aspect, 0.1, 20.0, Camera.Fov.VERTICAL) + + view.viewport = Viewport(0, 0, width, height) + } + } + } +} \ No newline at end of file diff --git a/android/samples/sample-live-wallpaper/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/samples/sample-live-wallpaper/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000000..b1517edf496 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/android/samples/sample-live-wallpaper/src/main/res/drawable/ic_launcher_background.xml b/android/samples/sample-live-wallpaper/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000000..88e31872fef --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/samples/sample-live-wallpaper/src/main/res/drawable/wallpaper_thumb.png b/android/samples/sample-live-wallpaper/src/main/res/drawable/wallpaper_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..f2772f51f143694603d404e7ca6fe12d55e73ab4 GIT binary patch literal 14879 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 + + + + \ No newline at end of file diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000000..6d5e5d094cb --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 zcma)BcR1WZxBl%e)~?{d=GL+&^aKnR?F5^S)H60AiZ4#Zw z<{%@_?XtN*4^Ysr4x}4T^65=zoh0oG>c$Zd1_pX6`i0v}uO|-eB%Q>N^ZQB&#m?tGlYwAcTcjWKhWpN*8Y^z}bpUe!vvcHEUBJgNGK%eQ7S zhw2AoGgwo(_hfBFVRxjN`6%=xzloqs)mKWPrm-faQ&#&tk^eX$WPcm-MNC>-{;_L% z0Jg#L7aw?C*LB0?_s+&330gN5n#G}+dQKW6E7x7oah`krn8p`}BEYImc@?)2KR>sX{@J2`9_`;EMqVM;E7 zM^Nq2M2@Ar`m389gX&t}L90)~SGI8us3tMfYX5};G>SN0A%5fOQLG#PPFJYkJHb1AEB+-$fL!Bd}q*2UB9O6tebS&4I)AHoUFS6a0* zc!_!c#7&?E>%TorPH_y|o9nwb*llir-x$3!^g6R>>Q>K7ACvf%;U5oX>e#-@UpPw1ttpskGPCiy-8# z9;&H8tgeknVpz>p*#TzNZQ1iL9rQenM3(5?rr(4U^UU z#ZlsmgBM9j5@V-B83P3|EhsyhgQ77EsG%NO5A6iB2H; zZ1qN35-DS^?&>n1IF?bU|LVIJ-)a3%TDI*m*gMi7SbayJG$BfYU*G+{~waS#I(h-%@?Js8EohlFK)L6r2&g ztcc$v%L)dK+Xr=`-?FuvAc@{QvVYC$Y>1$RA%NKFcE$38WkS6#MRtHdCdDG)L5@99 zmOB8Tk&uN4!2SZ@A&K>I#Y$pW5tKSmDDM|=;^itso2AsMUGb8M-UB;=iAQLVffx9~ z>9>|ibz#eT>CNXD*NxH55}uwlew*<*!HbMj&m@)MJpB3+`0S~CS*}j%xv0#&!t?KV zvzMowAuAt0aiRnsJX@ELz=6evG5`vT22QVgQ8`R8ZRMFz4b*L1Iea$C{}L-`I@ADV z>6E7u@2*aes?Tbya7q(2B@(_EQ`i{|e`sX<`|EStW0J4wXXu{=AL)Yc~qrWr;0$Pv5 zv>|&Z)9;X%pA)*;27gocc66voVg~qDgTjj+(U9|$GL0^^aT_|nB9A30Cit)kb|vD4 zf)DnEpLD$vFe;2q6HeCdJHy;zdy!J*G$c>?H)mhj)nUnqVZgsd$B3_otq0SLKK#6~ zYesV8{6fs%g73iiThOV6vBCG|%N@T5`sPyJC=Khz2BFm;>TDQsy`9-F*ndRcrY(oR zi`Yl&RS)~S{(6bu*x$_R`!T^Rb*kz$y74i|w!v9dWZch7*u=!*tHWu{H)+?o_5R?j zC3fh6nh%xP1o2@)nCKrOt45=`RDWzlx4E4Vyt~xJp=x(& z&nexdTA1T z8wlsklpvKX6UmIAoqD2{y!U7sJ1pb*!$$7-$WqT`P85GQnY<9f-V#A{D0qB4s( zM}v7W^xaEsAKOKHwfqZjhp--BnCdoIWKR-`Fzd|6nA|kgToLF%fZtoODEB96Wo9H1 z0Sdw%@}akuaT$>wLSecayqMj-91_>92B%+(=`^b?eO-^^iU_rUI1HudU9|kEC)+4kO$7RH+ld1twCmYZY9TvW^5l;Z}B8= z896yWiZZB`qqS&OG0XwC_$cobL16lrJ*2c3&fKbrp9 z%tlJvW_MO`=d4M{%mK#3Z4&l;9YJ1vr(ouTCy`gN^l^_A9NgpWRb8LrAX%Q#*Cmp5 zIwyGcPL%eUjz^{sVkq*vzFy#ta>EToiootr5A5XFi*hI$n2k0Y^t86pm2&3+F0p%mt`GZnV`T}#q!8*EbdK85^V zKmz&wU&?nse8nxapPCARIu14E@L92H30#omJIM-srk(t?deU6h*}Dy7Er~G6)^t#c>Md`*iRFxBLNTD%xZ?*ZX(Eyk@A7-?9%^6Mz+0mZ94+f?$Bjyu# z13t~Gc4k*z$MR-EkcUxB z&qf)13zOI)&aC{oO!Rc0f=E+Fz%3Dh2 zV#s?W#u7wIkKwpC1JpsDx>w@|$yx6)8IuolPXc&F`pg23fo3ut{Vi&9S5ax7tA`Jt zwy+x6 zmAjv170vr2Nqvw^f>!9m2c`;ERAPyYv%geDGY^+1Hu9_Ds%%_dgo`-0nQe|jj?3cV zBs&>A3u~RhH@@aaaJYOi^)d;Q9|^Bvl4*H#aNHs#`I7&5osKp$o#b8(AHEYaGGd5R zbl*pMVCA?^kz#h)fPX{it?;>NPXZ%jYUL7&`7ct>ud@Fafg?^dudINo z(V}0Pzk*<5wlI*`V}S9|VcGUJ>E(Z~SJK!qm!rRVg_iEo}kx(ZP@xbA^ zv5C}~Frbyc79Gf|LEN9bkut~oE_ts|A0;FoQd}xjkal?FrynlE$0~+WvV3FqT7hl& zCex`(-&TN>>hn=Z-GiZcT6`@s4Q={XbGonu=`?IO(DL;a7q4GJT*LFu=i-0%HoxX6 zcE6uWDcb4U{c-Lv)sS5Laat=&7<4^Nx-dI0yhCBphb{EUIOPF!x-K*8?4mhe)ql&=>t&BpmQ+Cro zU}jKu9ZVtI-zmH~&_GitE94R}uPo|TH7Avb>6`bfsw(H5#6i@1eAjnbJ6Jp2`sUyA zT6=~iK`oPTyOJ@B7;4>Mu_)Y5CU8VBR&hfdao**flRo6k_^jd9DVW1T%H662;=ha4 z|GqT_1efxomD2pViCVn>W{AJnZU z@(<&n5>30Xt6qP&C^{bC7HPAF@InDSS1jw5!M7p#vbz_0rOjeBFXm4vp#JW99$+91 zK~k`ZV)&&?=i!OIUJn61H*6??S4i2(>@e9c&~OD1RmDDRjY>mIh*T2~R)d#BYSQSV z<518JITbPK5V-O@m<{jeB0FU^j)M2SbBZhP~{vU%3pN+$M zPFjBIaP?dZdrsD*W5MU`i(Z*;vz&KFc$t|S+`C4<^rOY}L-{km@JPgFI%(Qv?H70{ zP9(GR?QE@2xF!jYE#Jrg{OFtw-!-QSAzzixxGASD;*4GzC9BVbY?)PI#oTH5pQvQJ z4(F%a)-AZ0-&-nz;u$aI*h?4q{mtLHo|Jr5*Lkb{dq_w7;*k-zS^tB-&6zy)_}3%5 z#YH742K~EFB(D`Owc*G|eAtF8K$%DHPrG6svzwbQ@<*;KKD^7`bN~5l%&9~Cbi+P| zQXpl;B@D$-in1g8#<%8;7>E4^pKZ8HRr5AdFu%WEWS)2{ojl|(sLh*GTQywaP()C+ zROOx}G2gr+d;pnbYrt(o>mKCgTM;v)c&`#B0IRr8zUJ*L*P}3@{DzfGART_iQo86R zHn{{%AN^=k;uXF7W4>PgVJM5fpitM`f*h9HOPKY2bTw;d_LcTZZU`(pS?h-dbYI%) zn5N|ig{SC0=wK-w(;;O~Bvz+ik;qp}m8&Qd3L?DdCPqZjy*Dme{|~nQ@oE+@SHf-` zDitu;{#0o+xpG%1N-X}T*Bu)Qg_#35Qtg69;bL(Rfw*LuJ7D5YzR7+LKM(f02I`7C zf?egH(4|Ze+r{VKB|xI%+fGVO?Lj(9psR4H0+jOcad-z!HvLVn2`Hu~b(*nIL+m9I zyUu|_)!0IKHTa4$J7h7LOV!SAp~5}f5M;S@2NAbfSnnITK3_mZ*(^b(;k-_z9a0&^ zD9wz~H~yQr==~xFtiM8@xM$))wCt^b{h%59^VMn|7>SqD3FSPPD;X>Z*TpI-)>p}4 zl9J3_o=A{D4@0OSL{z}-3t}KIP9aZAfIKBMxM9@w>5I+pAQ-f%v=?5 z&Xyg1ftNTz9SDl#6_T1x4b)vosG(9 ze*G{-J=_M#B!k3^sHOas?)yh=l79yE>hAtVo}h~T)f&PmUwfHd^GIgA$#c{9M_K@c zWbZ@sJ{%JeF!chy?#Y6l_884Q)}?y|vx&R~qZDlG#Q$pU2W+U4AQ+gt-ViZ@8*)W| zN}wXeW~TTA#eqe)(vdbZm(Pm3j;>#thsjkQ;WH#a1e>C?-z7B%5go0khC;qQfrA-~ z$^9-bBZi+WMhAW0%y*4FlNC%SvM%a(`BE ze-4>w7)wg(sKN@T-nTl^G~+e{lyeTG(dfoz3U!LKf{rmR=<}+ih`q1*(OB8oS#B&> z;Mf*_o&W5*=YXfgFP}B@p)|WJA7X^OhD8)dnP)jzA@E=&=Ci7QzO`+_Vzsr zPWpZ3Z1>W?dNv6)H}>_%l*Di^aMXFax2)v1ZCxi4OJKTI<)yK_R>n#>Sv$LTRI8cB ziL<^H!Q&(ny#h19ximj|=3WygbFQ9j_4d8yE5}Rvb>DpH^e#I;g6}sM7nZnLmyB3# z!UenLG)cb%%--*pozd3}aX#-Nmu5ptKcp>-zcwRx9se(_2ZQsmWHU!Rgj3QRPn3UF z_sqgJ&Eb=kv+m0$9uW~j-aZ0Hq#b_2f^rS*bL}stW91HXNt0JDK~q-%62AW}++%IT zk!ZO&)BjYf)_bpTye9UB=w_-2M{YgE#ii%`l+(PHe_QjW@$o^e)A&KoW2)+!I9Ohw zDB1e=ELr`L3zwGjsfma_2>Th#A0!7;_??{~*jzt2*T6O%e3V)-7*TMGh!k050cAi2C?f}r2CHy&b8kPa2#6aI1wtOBBfiCCj?OjhctJT zF|t;&c+_-i=lhK}pNiu>8*ZFrt0rJp={`H182b$`Zb>SI(z!@Hq@<+#JSpVAzA3oc z@yEcV|MbQ+i)`%|)klTCzCj&qoC0c7g6FFgsUhcaDowSG{A=DV19LHK*M7TK?HV;a zAAvOV<(8UlC>jP4XE>(OS{6DfL B0*L?s literal 0 HcmV?d00001 diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 zcmZu&byQSev_3Py&@gnDfPjP`DLFJqiULXtibx~fLnvK>bPOP+(%nO&(%r2fA>H-( zz4z~1>*iYL?tRWZ_k8=?-?=ADTT_`3j}{LAK&YyspmTRd|F`47?v6Thw%7njTB|C^ zKKGc}$-p)u@1g1$=G5ziQhGf`pecnFHQK@{)H)R`NQF;K%92o17K-93yUfN21$b29 zQwz1oFs@r6GO|&!sP_4*_5J}y@1EmX38MLHp9O5Oe0Nc6{^^wzO4l(d z;mtZ_YZu`gPyE@_DZic*_^gGkxh<(}XliiFNpj1&`$dYO3scX$PHr^OPt}D-`w9aR z4}a$o1nmaz>bV)|i2j5($CXJ<=V0%{^_5JXJ2~-Q=5u(R41}kRaj^33P50Hg*ot1f z?w;RDqu}t{QQ%88FhO3t>0-Sy@ck7!K1c53XC+HJeY@B0BH+W}BTA1!ueRG49Clr? z+R!2Jlc`n)zZ?XWaZO0BnqvRN#k{$*;dYA4UO&o_-b>h3>@8fgSjOUsv0wVwlxy0h z{E1|}P_3K!kMbGZt_qQIF~jd+Km4P8D0dwO{+jQ1;}@_Weti;`V}a_?BkaNJA?PXD zNGH$uRwng<4o9{nk4gW z3E-`-*MB=(J%0*&SA1UclA>pLfP4H?eSsQV$G$t!uXTEio7TY9E35&?0M-ERfX4he z{_Hb&AE`T%j8hIZEp@yBVycpvW2!bHrfxbuu6>_i<^9@?ak)9gHU*#bS~}$sGY*Fi z=%P&i3aH%N`b;I~s8{&6uGo$>-`ukQ<8ri(6aH6p_F`Fhdi6HuacwfQn10HVL7Om1 z4aZpjatkbgjp$L5Mceab#G#C)Hr{^W|TJX~?B3@2buj0;kfuNTf4c3*Au~O^aj=W2$j^4okeCxh#lwexN@eam-u4dNz zN2NIuIM4566{T&^k%4ftShcPk#=im-zXm>QWqH^0>A@?MqlDZCZ@8Wi*@tvhn5p<} zRwFm@gz|WZp91S5Z{}tB^e9|FBg(~Ik+?&_53J6ye_QQOSJ*846~H%s#LD}|O9v9H z1fLrrgoPo_&bs}eqEr}2en3iqAcP^>YsKiez$5-6m6(#3ZZ$@M5Ck=_Vv`QA>1A*v z3w-nJ_;5Nc(0_%`kG91#sotIlhO!*5#|yg+Gx{V;0ty`*=Y9=jCh$l*=fE(~t}%R# zc}iNpO)OZX`P=leQY^?^DF1w%FJh>Dkp}-o5Ig|2!6^E>|W|zc~W7gF;MtxX7 zV~UjQNsUC$EYXpN?~o{83D2c*0~7;Tm~%FRTAnnt3ln{?DcLZ=NsBY|JxwUA-6K3V zP&#|9t#a}Q4{Sg{6v-OmjJBkCh>m)8vLNm4lStMUT$)FZeJG05A)px&o3H)5oAl9= z31@?HyCriHcCDnt628BFN+T;U69Wl#itfvqIDBydMvOJO0Zl?go$cfG5>TK75CMj3 zakLaH3=&J0e}Xmqlav$S0>E@_Yo_V~3SiiXrw)$&!XhrHCDQ%P1BHPusuKr0LthAB zg)mDrLy>2*yevMMOQe6fZ|)%PEb!lC^*9yaX9UMy7-v!fSICssTR|wML0Ic2BhKAq z3I1X~ z7^_!M&;6Z9?br3#HU_&kfJ~%botXQkC1v<}ZZxN5q-T)|Sb2cW3WYUBbDZ`TH{!*^ zrmAeRM+(QI>D+?}guZ+dH*X)@^!O|oL69&Avbtw2^M3HP(+2kV{O$^3BN1RLfrC8nwz7=VhBR%>!;7WR<~;34B_j3A{>^@e@H+Q! zL=UNr1(JvKAQLKT0b}EMn|QUWtY>!>8-t@fVj_&`~gGd{_aPy5W>0u5L$zrsU^rBO=i$`#Xd*>kh)lPf}A znNXSEl`+HlhXtylgS9(#N02A=zVV?#OF?)Gr>(HszVa+1*2VG@qYttJuXaBlzP`Pb zX)ueu?s&}R>xI#^*r4gR?tMFi!_eeKlIM5g)Nk)Y^h=ZCR**xY>$E5knctRrq!zw? zX{2|hwR9LXTY1)pTlKg7U4_ej{dcj2{!+1sZ6<@9^?mn)=37V)DIAvS(}S`IgFO!6 zn({?nYw`Z-@jvt@!q|5z?TI3(dx^1szSn%azAwp>N#fk^kt|=MejKtacAs@Rdku#zT>9$s z=m7ek)`=O7hO2n+2Uj$QUs&2EIqycF{(L9Y#^IyxXA%R@ z&j`VAprIV~d!pH-7~zA+bjwVn3kOB3;rlg{nr&wHV12N}g^i>Upls~=z`VX>9HQ#= zTu&luVb@_Lkz63&&^_M!6(-2^0?GCAX9XKp{O={pd|AlIMGriX6s_Jy8_q9|{5jLc zxd1aj_ucE7Vcti#$r!s~w~W=XpaLQ}#mX`apR7^n9-d3?O+adJYr*L;{c)x@REewM@vZN0njS3iE$88KHPWAkWt((OUMherUnPm?i&8@!9E@ zUW^$%CpdruZR0ohzUq-XQ$KEIB8Sjgs1+wKSUH&Y;=ee%E&O$X18{&979d~K2uJW` zd*8awHCXb;Q>4z$B|sPNv+Zd__f6&@KmS+L`z3H1x+x|Xs7-N-iw|1C=QiJdU)f~z z{vO4hpP`0MyqmwIHN=l?jSq>OKG6CEC#O`*blP`?>)CUWj5j1cB>%6N7;`kfZ1iQV zam~SDB?{uyp^=vF_u|=8xn3S)L;wF8ZRZV{bezM-EH;MC91JQZ{KcZZ$IWJUy?SJGeGUWm6PeuO8-K2|hD~p;Ls~9Y-4lE+?|bF)XaNKUNX(K7 zBQk0Z{n>hrH-CA`bTr$6z0n@Cn9EL$XZ3=X7NopjcI=;z<(X7-oEmK}BId=PxX*!b7Q6oL@ufd%eEPc`_la(}WkT zKe?-YJWn^6b$^{dhdJZ)I!Kn6c}iw%o5mLDyvM7qJZbkGG?zLU;M|W;Wis|A;SuY3{_X53`+>9g^B%O4b{;^t$^;{oKHbo*CY%u91 zp#2d8Pg=I0&UX{qwr=y=o_^BLdk=KYH$=Z8+k|p8V5`ph~3b^{^NnL4m_+4zx( zeoTt@f<$DmsB1}o%R1Hx`ToPuBl+P6cb-?uF{1!z-2WvdR4+vJ*SYTic5@gwnzu%e zD!HF^X=$ha^#1hi*@~^nDL!HQ;MC&e+6=onaJgm-J-+|>PpmU=SIe?EQE5vJiqziw z*K=Z%bWZz_we!qiFqE`I?#$yozNxIE7Ei;csv>++r*?)0bozFpF&oLh94u z-2c2L`5BarP7l>87|f)vxaT*9(!Q`2xBMZ&^JVj-|1)Tg!6OW=lk=w zLwVlr!*<(l*L$a?ox3+%!~UIj3Ej@KD;W>1E_c)1szDi93BC;0K?drOQ>@$yi|DtT zSir}!Yx>znf&b0KS;Lk7VKPDF@e>(qQr0%SNcGQd(p9StjqJ`QSW&c{ggF?5{d22w zlkX%JTUq`;(3WSH+)WHl%qlF)iNG_?}K?ZM3cS7#u5v zZ!apx4Apv=PWsn}eD%MI#=KA)OlNy0)l@~D^1;NC5k@|OPW3wt>WNYDN+8~+gM%E! z$ z`Olr0;eytiK&~O*ps%KV?2vq+DhuRh*!6Ilzu>A;iMe9 zI?zug9nT9CI_o)O}KF_I_U z_Cswu{)3pCYgw{eOt#E?UCqBwkAugSl>5 zX?G=Ci(Lo+r3suuJezyQyDvw*<1b{rx*&ZaY2HlJ>k{Qc%IZeU43pQXw4mh!4I5>l zZ@4$uxaPY#!*IhL4Hctn#!n#S+SiPcZP_PTd5fXf1exhFi5zf3kl`UcW2RUk)F2oF z_ogN`{03PiseQR;fa#{Uy;jeNlJ0Sle`~;ZYhLjkuy>a^!Z_nR~`$&F?NVuIE3HX;i zD82snwlwPb`7yE)ZA_Ndmq5zuSO1{{1}(d9u4#!Fl_|eOuxKBwOfQ*tG`VjCV$-WF zxi0c&+w}Z)rqz{%f46@`ADPdGm#x)+zpT+gyfDi;_P zR{#Ta`Mzd=putKO@5lQJO*aNy(i?}Ltwy^Z;69f|eqi#UCI1$vL!+(#mi?dK`OL$! z3jQnx$_$+Li2<__CL@Wuk4^J7-!n3j2I4N8e#=qpir+iEQcrn3`B4yNOd1BBLEni<(tdRWE>m0I^ zt(^*Td+S3}$5rOzXy=MW>%#MN_qy%5St!>HrGZ~Fq1WKw-&kv@2TrCcPCPzY%2aO- zN?7@+$4?&qA|uv{QHuV)O9haZpG7Jx2f%D)7J@oWTxJ#E_YSq_6qT1tomOD?02(1otT{Hk8{?g(944>h4f% zOJ8tzjecV{x2uWde&6oAP)*({ zFkW0Q%gdI*9@W)oKO65DgP<3F_BIKvRXLAR?Z61&0g2TR6mEZ7OZK?dP7zukdg?s_tNZeuOsh^e1Tmdlz5rIg?LcK|%aQ1FsSDv#W0EnHd z9M)p;gAL_R~Z5cojTdwy+qDsd6R01Vtxmq&FhfPz{wxmB$${zW~z@{Ro_ zK#y5^KqIp!#@or>GD`c+aZ(PV1=`Eo1?a55p6a*WepFgxvmp!^2518YEU-;{F}fLr zD~)=S0m=+px3TUN8-El}Xb}{2ET*_i3-|WlY@V7vr6#&cOr*+oS9?GF?@)K6op>>o z4af0@%KwaLr`{3P&)474<3rDMsd!IM-bepWfhfuMmJt}#0%PgDSx*q(s0m%ZFgWTj zwwvH%2!(i9{RHX~FVUB5qHvF{+ZF}+(bZVPG1)a*Ph>KV;cYNK^aB@R#dS~&`^60V zn2Z24Y{{djzK33}t@q%!v5k)u7jAXB_H{#4Ut2 z1}0j5$RXcTyfazqL9=^Qe%GL`G)=!lirv7AgVRf^=XyEM&kiOe_%JD!O?sXK&hrDo zF}m9B68im!oGshuZluy2H#T$`XPZQu@zf;(nBCZB-cjQ&w*p@Tm_$pe^MTN3EauI) zJG&G^H-4S|1OCd#@A6jO+IcAXG#5M-d9E!^YNmV7Z(=F^?8bfrYf&mLMnRd_22&Q} z2*msbLsrI!XPeOK@|V?n>`kNC`8eSFmekELLr|!-wQRltxZnuRedup<7VflowJ+gC z)F}P6lUSsh^B41?=~0*68YA6z63lKG`W$@{GV!cC2FCl0s<7yz6!3JWoBbUDTgpg% z4VNUk%xblMy7PjLF2We*3XY7K*N(*9Yx!_M zjU$&JXLiNxaTzoa&k@NSbzbLJTn$6bu6SPWYx)Zc1Li~Lqj($GuWsA#;zg85eH{yx zz3IIOea3A4QFGmJCfn7N_d$8a77j+T^W}Sr%0XdVLFf&zJ$s^D5Vrc!iV&GXyb5*A z6mG8d*6EDN7a;=dgVjYI--~4@Fe{{fcJ4B|;_Qg~&%6#?I(?X_$S4rDw{=>=8iZS=M^I#EF!m zXn%K_xXWwmm7R40LKXPo6ZzNZfN1-$S6RuVU=JlC|3#Xjo-%ebJvvC4n%IM)Q8NDh zGXd)L;ay_JMozc^mU*Uifnp=#+if>LD*O9MV#@wB1l``z|tlu(7PJqS6rm)0@ zJzP50{0Vpa`_?92oB;*i(?i225a6tZgT+9Dg?vTh)N4OKA~(c8{$8-ZKz=mb@$4IT9g8>;k11WIT+Y=%Z})`y#OJ zK-~rlEy!T%0h!Qo+jjPF2RQz2Z^B;dbvYg2JS`+@D~OWH{2-EEs^BdnuJskh>CKeT z1b;%8dU6QU%i@z?^6Q-{XESe^qRiw`ka+k!d-{c%&lXM}vCX^T=|?|;t6r?N*h-W4 z?o4Hy%BWqW+5=+md#5^8|49zjM zon_Do@rhzZ4XAb}-m|bMH$Vg<;^Bo6A8cfhUQ>|wFk~j(`>1NgD3sTg)He1pWrUj9WZ8R(Wn5Rr zhc&dXvv_m%HrwwHo9l_))NgdVUff%d&@4^$Pc=MDZdZ^xHL$KX^ z7W1{3UJ%>9v$W{Y3>vBvflE-soDj8{`>#F|8Z$EF%lN$NylORTn5JsI4mTMHWd*%- z2sD(RO(H-&i8&Ge)5i12slI5VekYCZ)s8rv&_)194;vKY2m8DIC2{4<&xTM3HHxwT zd(42n)gCJ$O4I|8sJq07#0U7Yk7PjPK&bMdy-5b)OdhSsBo^|IB_H43@&F@tpdJR0 z#~)=UJdP|=)O{0(rVZnjbTtwHV^}&kfLJQP@R6rda;K;O>9J9bnW$BgbzOZ8aO{D8 zPuJ%=Nqg~rdzk-IW0ZC5I%cc;ek5~=lDXl4?gMOQQ!KE5Aq$9qeGFM6jFP;Xy6)%N zjg{q(E6fnF02P3L*tutbHRR-gyYK3g^y9H?GMtIs;ojG zY~3*C>qD)(8jz}89w|xfb7L`^d>AG#%D-uq=qz}(o9kzzrx0LSBX90ykr*5oM+YmoTRWe+Cj6aq^xnWRymLmE>krCpoC9K%2LT0aK0Y< zt@kUUrrj1WL9rmBB8B;WXqg-BztOiUZX-!`*a&-75+!WZ!R0OPiZz?w`Of4q#+(;m z`${Ea6GnTCY3`V2R8w*}knf)*`RA@(8k{Lp4VP;<+ z9O_z0_{3=HcVi z5)&QGEB_&$)mu@)(Z8zuw#>Gc6C>^O-FUZEo;TO1@$>-xu%`v`tMS3V-8R1pb5w&zP%&rAP2*5h z$k{jqReFXCJhJ?-{x(2j5gH_zQ>;#Ec*@bUqF0u}XB09+U-K}+jQd>)k#AOkr6M8x zHyhrfJ`99@Vzr_B@*p@`DxeJ#`jimavZ9ZV%v{mO0!%9$TY(f%_}BU~3R%QxmSdD1 z2Bp45R0C=8qtx-~+oULrzCMHMof!&H<~~>BhOu9t%ti7ERzy&MfeFI`yIK^$C)AW3 zNQRoy0G}{Z0U#b~iYF^Jc^xOlG#4#C=;O>}m0(@{S^B2chkhuBA^ur)c`E;iGC9@z z7%fqif|WXh26-3;GTi8YpXUOSVWuR&C%jb}s5V4o;X~?V>XaR)8gBIQvmh3-xs)|E z8CExUnh>Ngjb^6YLgG<K?>j`V4Zp4G4%h8vUG^ouv)P!AnMkAWurg1zX2{E)hFp5ex ziBTDWLl+>ihx>1Um{+p<{v-zS?fx&Ioeu#9;aON_P4|J-J)gPF2-0?yt=+nHsn^1G z2bM#YbR1hHRbR9Or49U3T&x=1c0%dKX4HI!55MQv`3gt5ENVMAhhgEp@kG2k+qT|<5K~u`9G7x z?eB%b2B#mq)&K}m$lwDv|MU~=Y(D2jO{j*Box$GUn=$90z6O^7F?7pn=P;{r4C8qa zv1n*5N7uIvTn`8$>}(74>Oqk=E7){#pHUFd5XRJ5ObMhqODTa}=V0;+a(7JZR-4<3 zBTvsqRwLh?*ZF)JWsWOkEq7*XMQ!G3Rmkdh7ZbM#v1~?jt((e2y}u}Ky>1qa&Y7m@ zveIzH@?5Gexr79*?sbZGkVS;s1U<7D(%~7HjAmzj$aDYv_FGl5JX@LW8>w=HCDl6W z%?rsr0)bErYJ5G1v&zjr{8=lW)ZYcstgZAuL}!0~8HAcgOm@nJ9cvOOtL@)Fpl2Dr z8876Lt<|1eF88Jx#C*XyGI)C5z_o!Os!t=Xy0$Kj^4fG1pb@16%g z+<)zJ1n1QO78g#$3yHj+(Smv`HW5y_-PP{h2A1UXMG-c%hMvHLbF6t}G>KA)H# z`AWL~>8JUT(iq7;zJr!Aj)AS+n{mRbA3aM+Gj}b#PhHdTM_NkwQm330EC9waM$=slPfxR1vmr!vf~t_M?a%`@`&tdE}ipY-p#Q#zhLK zd9eFC;PjIEAKLkRkO94{rTuNFqKbNUGtaNZRRbax9;|%2WbnGu!44#64RriY5u0O} z05G^e&JB?Wb*8^g)aM`yt|}~QJkKCipFNeyex~P~SFPVEafD(73rncKmm)m~&`O*YUyY9z7tO%ec7z@wWcoOr-ebP z1k+|y?d{>1jLC=s4B2tEhiTtu->WVJno&%%6bG46KuU9D`GEN!C!9chM>zd=cl0+- z^k>4rpkq7_iWGHtBvy$Q`dja2;1ZdYmF6cANU6{v>l1=fSKRpsTRonp@alC%p{bhU z>g+(%-)&_nDQ~#bq5;xo^06RggA&uH4RMVb6wt;oQI+`m_zt>SiI5hXkfEnn6@ZNk zh9KUr1jtt6lBg$O#TAoTRvwUtWeMP3EjnGoRPQppiNF(sX%|Q4@kIjas|WZWXSENO zfF#2yOb;%XO*LeOoAwlf{u7_39$x(w3xT~)2BNJ2l5u4n3a0NkNLT4yT);7fA?1Vt zCz*`hbw-doYa09E!05zcfOT0EOORY``E@D z5{v%@F~&|UfNt@>vrj66W5f>jy+G_8&VB9D0*>N!7_Nr=-x6N?A)M8>1~q(X34sXp zpA%@w&c};L7u*G3;(Qe=LFL}NbTF$|aX#A%P(h`-N=ZRxCvlG$>Klv}jo0MS|UR8qKq-1FokBJmrbTJjQ!k#Is0tY+0c)m4Gp80YzYD zEGXd~ihaihk;?xUknXNH?rssjzaF+l6?HnDQjVP$i=q}{lp_WbOTKKg}HPKW)2sW`L#NvgmaY0^b2Ldk|t{P6{L{>ym;Xgao1PrudBgEMRFb^ zkPJ6v0h^tJ>K@;maHk_|6Z>yFzq@YvDOeO6Ob_?P4Ey>kHiJv`Wlh_MX4fBY36f%^ zV#2t;$Rg&}!Kwifm z;TVZXMxw3~$--{&A8-6vnUZ#s4`Z-zQ#+y7UI8#Hgsc|ompLUc zqlAG!Ti>t{JzYF^5pM925*PUWUvDuYDGKhC4FMx45c`L#V7%V+88@|khLj|V=J9Un zJEcP5qVCzR6p{FK!nIY~TXo)tJ!{>CG;~&u;EPlnNrwJ=5)ke@hJosN!siM$8b2mM zmc&weo-rY{n1+%c`c<{AT3i zjF{p253Ul-)s5A+!8Dp7?viXAdH1+qlY%mK5pp?{pS1t!3qmmDOq2TnoV`F3<>(XK z1=gfH39N_~8O+~({MZX~+QHyB>vtgwK0@uqGkX^eaf$UFHiO#>LB*7@=c0o6`0muj zmH00_F#p)s3E*$A-zP+p2bvXARTg3)Lxh`tf~9X>7!Z^kHV`uE%V9+BiBG=mxj*)M zr%3rn=)>GR`{#zmwD)$3ToLMx++uqsCx(+50Uk*5QJp2c6msxLD&P-y{c|XK6zZl3 z_Fgu8kp|gKVWv`GS!c56FWPO)ZrCCtYh#*yp-ssus)ot>_~UB zyGfjTjz#fXod{^KEQK1~@jN|;SZw5OgH#0wK78Oe4#vV3*|&XPQU z$r~5u8ziT0<#ICrX^<1){mvtaqT9OqlW?wiSu4X#rOC(0uL{Ownb%i1F_G&d>=l51 zx!FEO4_LK+)W^N6UF+fAccyyp{t)TE`;vF@1irbNjcXF8b?yFh zl5UEB>@;wO`~gMF!QB;h<``+f(lxAb_8B$;&vT7)(bXG(7x_5f%AZ5;h#3WjHisX{ zLTSguapAADXMwWZ&jsD0+K!+8#*6z7-(T+QUk>(~!Q|0&!d)PgEw8F6RK;LkB;!HXg79$+l*KU&-fRF|$o+kR4mJ36k9p&>*uS~RhCV+*Y$3U-k%~M)jxCFW zl9;bQ-fx4HPy)*(bhrKL!81M6*@6p5W?z*W`jb;@JKMFwmic{gQPv*) z?I{Fh)y)}(-6uh^I52xKo!LRZV0c*1X)Z(g+GVFN{2n%vD*@&IkVI{R_0;M28M z8vu?M+xVF-&<{l@1g{PA#hnyAq(gudz4WKSFL5YOr3q!|qrxa7z~F~rEJ29VQKgNe z1*L^m9&acg2p7&`u&V%oY|AKF(Xpv=)wf&j#n|;2UYEaUIHLJuTQw$SbrNn+)38PlfV^0<6s>)|hT#IAAS*T)_^_q@I} z0S%tV-HrXOjzkvW!YSbDjdH=g;=4A@whsDB zI8^aX6n=|ab(?!Ay!)CxH(wC(iX~Q@%FEx>C{Hmp98f2ku$Bsw%lk6v50(U@; zu68Z9U&za}O#-Mv^+!V=eyj6S)5oS{My`1MVs)nlnYl_$xU^QId1_jMf7&K8ij)jQ zJ|+~@l)xpV%~Y{P()$`+nBihkjE|3t3t8PoKU3wZ_Eg%0P<>%(A@oW#*8i$X!nfG& z;&&2ZIKlD~*Gff+p3A7QB!}Ei>RGhUUz^UoEpeJ{`2ov>wH!O@1$VW>A#D#{i2z9l z{d)FK9OYxRY#(6NUMO=q^5Ve7R|72%f}ZDlsm0BN&LzyaSHurXV4p5HGf7|Z)}8)g z5J#S6h{-+_U0m$k#+|N{6_8MYactWzWb+1~ea8wX3zX<@O0>pU*q($J{=R&7)P&jg z6Kb)o=HAnC_MP;cIeBq}{gG^0CZzOUJZ|7C-VjE}!?*UtKTcwwF33v^BYC&}Rq)C* zpAJ07-!{`flYX1@n;ZK-=x4)!o(%(1UqulVmes(D z^`_HNfM#umEYy~=zh$9&+?8$4!l(4rr?d#8hS4iks@9w%E4l`BKmhUtvsm1X-mKC3 z>4(u4yS45OgZIOQ;EQ6s`sjNelo!~mLe7gS69TW2WnFwEKcAwioq2mLXV<9CIa#(0`sQpl>vwW`A$D?!2%nt*HEb;Ga=o?92 zHAOICmXHEQ%Cc{m2>dLjPU1J}^w7zilFIxy9nG(OZbYPtW?3KJyv@A7|1A*NiD_v! zTLC}%E4kI*d?$lQBRL==MPsD#FyN0ZSr`;aeQ4C6a2INH9klU~_gCH;G2%8R4EuHb z44Ej^6301>?c06FP3X~xyP{77p`-3td;HKAGf4mZw1qRd6Z^^L#?qaiAKv~px)*jAV^re~beps9m{kJzb6n(oS8uCt#Lnjofg;Rl z=apY)JsV;^dVkzCW)jDrii_WTT`3iKri(xmCC1^AO}Vqt-1B*wwIlBAmE1AmdRtMc zD!fB@mtwHPHyV-^VIVU??*~*{olz-Ub)NCX941BDj_CKZ+QYQ?+``tyhy_7WFXF}_ z?~CVO#LsDYD!&}cph22{PZ*TK?$K^u`E7%{^na89Rm%!jSZs7vI-D zL1POD!1cu56G)*p1gui3-i^JZPX3tI*_Fq&JRwbz*#8LUSiMRWjuu`zD|uk;+X&d@ zuxF5C2{Zp#O?GtOB+R2~tF>MDI(}%p-W=M>1tEY}8E=b_l*WbOO zY9tCPgL3vMEqz)_eWeqmN{qobq_4)XdXJSe6Hj;Eie0??2ZZ?p;*_K8@(&v~1evu- zxQCA2YYvv@qhzamqdi`?{Z{c*7$arCdz4-4G(`O5It%y&8>d{#Y9Vax^FZ99ZK zUdIPpkNhp8uP3T+W4lhvUIYaoY##y6KtxBFoj3&5^@Q(^{677%C#3YJh$p-Ee2M6F ztJAoQv1N0L!|N8XBD(eAYcB#gRaIX7T8U5xXbx~cJSon~YnC zaJYE%zOj9y?E==_B$*9NiAm{~)2Z}t1$$l?qOYct5Ep5HvqFKvuSE7A5YF$K@2>UE zbQOdTNzjD#zS(L>wa2$K-WK!Pc%pY^8To58;^JaXZ}F30wuYl;WWs~rCoo&vrEtUh zTBLMU??yx1#;-weCPZyOJ%Yeb?14z+OXW0L_E+<)(q=;xz74U-Q~R~n*oC;MxyrJo(74r$y2t;x`D~{nhUw`N{Bbc zo`l5kb`Yy;L=&@MTQ~Ml_%V%){mCIj4WC}5q=A_ACx2^by!4w1rVX6H0ifayJsw;; z=+}5kjC?RG*q)^FA;udd?fK$7vU1x>y0w;A-)YbE%l$J%nRRjAIlrItFPgQvJ7Ytb z%HSFnjF2||X&L_g-Q>1{(mholW_-EJmSzsO%*VVVB4)#OAv<(kOIx2H!f)I9#e_Nyjdb$&*1KN^gM}yFIhi%%BWB}7Ke0M{0WY>CxJQUuL<9GW$I>S z8~;QmE{^wS?I`=DyV^l+MozMPWLoFz=uSLu99tiVHdCN>7jRs~vd13`&Gey!!7_+< z6o@25%!eN~+Eki#7iq@#{Hxl7pF0^`N;~p~#tc6HXJP0g5xvK|AuLSwNHVI2_Y-!& z4hemc%vOM5!ySDypyEGe=lAeFbIp`w8FIUcTqUwens>sTIV-jDhrcKGX7XHFXyazb z^DO8=ZgefY6R6&+)c1_i*WoenjtR5@_JU#Ph;4M8fpmznxE9R`=r@-#_y zkD?Muq|*gg7f*BQeI|Np#}Q|NXLJHM6GE{;SJn8ce`V1Gehym~{8c+M<2~=HcCRuk z-v&$8dc8YG+tK}NYVhwdm1iZ&A#r+T<>Ez88)Eq9j+G5h5D(_u{WQdUTOs+QbA(=? z{F6n6UV8D2*lvb)0vDrca$729KG$xO2aH$jWoWl0drlmefYsTswh)`GjMtmR=vEkJ zN$aTp_@@KL%KQ-VDB2ppbZK@X`6cJA5n`g>sbCTvU_xdid!{9gWA|>Mfs6rtHx6s` z_wMt*FgUTBZ@I2C62&zbs?pPvK9TpatkXzqDqe4YTr^nnQg8gWxjKt*s&eOMEp!Qc zG~PT`>xg76Xqh^dKI-Eu#K*VnvEf9qT{L0yNpVj)eVD#kQzGgVRbTB!5nWY=?t!cggiEGBAcWM2xNtW&9 zZB_6RZ}|a87CuEYRYCRJ`Sg+_gBK$_J@*zoWcJJw>eBw?G9WY(Jw~qN|A3MBR^~jm?>k5oGv7z+0jWOox(co@%nya|* zE-2peyX)#@svgwwDMPJ89dT=iO>}@wtNR@NUQ|cJZ};sX(w2uWP4AE5)@A ziJgy_TIZ+T&vG&xPh@Jmt!OJ|zA6C0ZxfF2 z7>aIZqecbmM$lyvDMwg2?Ipo9b)-WL6K_7(X_rmJgdd$-Qc^ywEw4SThChz6*_yu= z{v~a4V|RJtH-GThc2C0Z|JHPl{II-!?B~7cWnRz&dgP*UqoY!iCo&i-xeM}kl?ID* zKTX`w+;z0+MCdGcl{N?xb|tYb%Id=k++k_@(V%bTS&n09`0{S0)|>IH_F;V@_zrxS-dKDDc7+i`nHN8J z;38w69lzAS*WWa+dnVvk(0-KD3%*)TerLH zSCc}Tjc-mR5|1HAL$C1}oue|Qp&M!hmyDUcg)Cz>GXPEyeYf}+s48kIl*pL{{treP BIP(Ai literal 0 HcmV?d00001 diff --git a/android/samples/sample-live-wallpaper/src/main/res/values/colors.xml b/android/samples/sample-live-wallpaper/src/main/res/values/colors.xml new file mode 100644 index 00000000000..5a077b3a78f --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3F51B5 + #303F9F + #FF4081 + diff --git a/android/samples/sample-live-wallpaper/src/main/res/values/strings.xml b/android/samples/sample-live-wallpaper/src/main/res/values/strings.xml new file mode 100644 index 00000000000..8aa3a79ecd8 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + Filament Sample Live Wallpaper + Google + diff --git a/android/samples/sample-live-wallpaper/src/main/res/values/styles.xml b/android/samples/sample-live-wallpaper/src/main/res/values/styles.xml new file mode 100644 index 00000000000..a7a06158ff1 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/android/samples/sample-live-wallpaper/src/main/res/xml/wallpaper.xml b/android/samples/sample-live-wallpaper/src/main/res/xml/wallpaper.xml new file mode 100644 index 00000000000..680d4bc1e37 --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/xml/wallpaper.xml @@ -0,0 +1,5 @@ + + + diff --git a/android/settings.gradle b/android/settings.gradle index 18c19e1283e..8741c2a3b43 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -10,6 +10,7 @@ include ':samples:sample-hello-camera' include ':samples:sample-hello-triangle' include ':samples:sample-image-based-lighting' include ':samples:sample-lit-cube' +include ':samples:sample-live-wallpaper' include ':samples:sample-material-builder' include ':samples:sample-multi-view' include ':samples:sample-page-curl' diff --git a/docs/images/samples/example_live_wallpaper.jpg b/docs/images/samples/example_live_wallpaper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..74ad33f1aebc0da6232008258e0d4118e1a6eff2 GIT binary patch literal 24803 zcmbTd2|U!>`#Am{gF%+cRA?-vm`TYpm9dW7Wz5Ka<0ffQ8A=QiV^7*Z*xh=e(cuZ09-8^PK0H(^1Akf49}|7Tm&y23luyyJhq`@fFJ9zPM{1E79@pU_}H94O*A zXd`|9Y=?{6$Np?5irc}zxb6ioEYL>#{;PfLU+r_DL7@PqMhx#06fACk25s|z(`UuB z#E)3p@+Z#hcLRTa1wSY#1agH4&`*#S^a~UW9fi(-k(1B~(AI*^L4HsuLKvvLf!>BGV>2Vfm^!1x)jO8DK z!`J71n!fp}i{OM$2YD|wpJrJ|jmqVI_+PJ%JZEUxY+ zqTo;(7@1!q#6}*SD7ZEc6S7Bl@WG(d@0@vAA-i)`N}rfQOnJ24<9VegY%$q0sE2(^ zVzO_dtyUO70|e|&gr3RR<18m01Hio8`Iyn-8hvHXMUar1e(sY~4xjMJi5q5h)G{Ud zN@h!BPyqneBpnuu0PvszZyKL*j%I+m^%Kj~_UOwUjL|J&e_?&b-6=+K=<=te-?@Kt zO9u>!nSAtK%+wQ&H%Vvo-6>^&0{F?|hG`zPyp;5K{%;Prpr86_@p4D}v1qjDk8i%3 zB4z%Zi};tvQIYnLpH135cIVa9^gzI<7`8q7D&WDgCGv<^uj_$kbeqM*EL*aUivD73 zMFJ<@OC>-DU?LR%7kvW-{xx4riwJN$svgxX;fjU*gga1m70$N@FMwE1KrFGkFJr0} zF94Pf{Y%vX@WRS}_;s~_(2n57tP>=8qg_cT>-)-5UO*z5Qi%^h9IeZ?)Kp^F4pX1Co(nX5$d%{hY~r;RHHuB-OS z+wGU{PrqL*^Qf$M_hm|u^4mQf=dPIJJE||LO<>!;pNJ;>aZ0@+(Q97~ZOes;CfWVg zQooHIx}H;K{n2UvH-6j4&T*>0eOGnuZ`pOaY2R$LXFs@WB^RiwsB^TSF|(34{e$$v z0)9xx2hp^1tz=WWxtnd1-Q`EBC_PN|F{zk@nUG&v&dzsVw$v%Tc%JV!j2quKvuXXcQ{(5}xQ%p5-SDS; zH|l%8HiZH^Jsxkk&-b++zx%S4jMLlL4N?1e=H#Q`l*ys7C#O~ChX(RUiecxh$iG5luBMrZ0M>U*w1}SH@Qi=R<5M+&M@AkEPR|#kv#b<| z$7YdYo4mYYr~iQzk482L&VTsZq=BVZSquS6oe0dk#iUNKnPS8?ef&?G{s(a9itQ(n z{w6OrelehxkGg?hOqGQK*lU1yFr*1M)4RX>SkjXulEvfT^xq4i^`{|eqB$gW_L-H! zt3cnoS!)l?M7muj{%TY4`UcPD+3I4M>ov9#t3yi0=~NF+ln$eU>U(WRzWF&d%kjfh4?_@8&^jQ7&8&cC-q_mk~z;^a<`KT0;o^=t zwx&_MJMGHhw6VgM>iUykV-nk5oDe8r4xZvgI&z)J@cHS^E{`T&x9Zu%gtdxlL31r(c&UI6NcioBS z^z2tD$DfCtnmYAVa5lS+`b5Wnqztx%o(X)@IP>1~$JEE-H1zgS595P(&)a|gNqS~= zzA4!`?F{=D^eMT7m3 zY75L$h9!#*R9x#?71MVayGPp&>$H90Hf3ONdg)o8O3O%xpTwqnxQQ%HU857JY7-Vu z@@_9T@n2UHK8>*F^;|@*db52UnHBZX^rR?r%1TxaEmtEY3nv5hLX496XTnQdakH{RTCaq7Z6hCQ0+=~DHP)5+blQ53dtp*M3t z^0B>n5_XhF)ZTC|_`vO3{RM_wZV+pAZ%|ueeI6x^<`hY{w0gJr;(&{&KBQWNjZSG2 z;0G;%>SpJv8)^dO(&p~4uI`Ct0c-T{6u|6Y|b&uKVeo>TppOpOE=`0Sx z%fI7BXm^l%>Bt|J+TrWkW=@D${JmGOT-a2fIyaaKzh8IVQs z$B#xc8KWOdMbyIuAi0vLDWBh^+g|!0c2rpA%kGd$ju#!}*J+;28!Jd69WJ0*uvclr zNAD&TM7mEW+xPnxX*68~FlW&7JBtDrGm{ELl^TuP6P3gaM7l3S$soHK0%VTdIm#!r z1GF;&qA1p1j0Q2<)e?P2{Xt}nhz-60tpt(3CE%~y7YwjF44#0#Q-Ozdf23=47RWO3 zPYq(~2#CewXHnyfJOC zHCDtku&g7B0{w?98yZNK^+1~AvCGYKcSc*HnZ=g%elhi@W_{DAmF#DHf4p{D?OE4b z>e!TDII{c1r8l@WTV7{aeY<09^pFCd{!Qx{-mukaZPc!roZjeT(Wji7?$U|H_p9Ya zwMdt$-5j!H!Z({9Uq;J(NSE^X*6K^ynO)&FON^M;hZZkn1&8vJ7uz??{OZ#v($;&s z-gCQJVeQw>-_8_H7JZGl&Y`Isuj-jPZiP8>t@ZWQk#MDZKcOD+=WB^NUB%RjF9&TN zWw<|!@bs!%h7RAZAAX^*^xM4#uK)Gf4elS-+fC~Uwn$hW-|8e8x+@DT-KTl0qMhw^ zQ47O1wIza)Wk_LG?N;|m_GnRoD1^{H98q31zeBhE)Tz;dqynY|OGW(dV}Xcj*+4J> z9nSoYfavrl04wf+S4E=OJl*!8LHI_o%`uB(>dEsKsGehs%rx;lfU^$O;}gwHDsXRT zuhaC!hQwb3w)eEPt$k=7$4-|&T;RHRr=!@R^zV0;bqB3{u~~mX9v0906Ed*C6%csc zKxkjQCyM(Q1wh@hs&hkY*;JS2qDm1z3wid?8Ko;!q5IsmQNtaLa><|HU17gVio0K~ z^rqy&uNTfa+bwLf-)Jb5>tW2wiIUW2)=s~T(QBFdV(iw$E~)R)4}ZBs;CH*0yQO>TN#%n6$6dLvH<1m!{Pz^tw*72G4AEmNZF7_> zxK3#C$hs=|ye+0dC(I%CTd|3mcXSS-O}ezXB1ZDnk8YZR2QqeZ zvwa*A-`{gAcxmH%Q|>o??HvQe^!wWw^pfnE%WwBAX|+At712eJd=~3ZZzXow_0A?p zImfsp`fMWbUPM}5G42dcIQ;Q-gLzn&R?tLsK=6^0i`T!~(vRdM*WkPiOm;PSD5ttV zoMpeuKb41_W`@}>=#@U2{z!7OpPRtljzP}c*f?S@5x}&6KbezDR87{b-LOh~clVT& zQvE}tAQ`PSw9qUiotF+KpYBAG4-%J}UY0Vyb}4NqHO-bxOzSK_PwpL~4(5d#MD2ZD zl;#z#m0I;cZtF|0&qZ&GpKb|Ix-($%`GZdw;(L6iiAoM%bM;A{&(lIWZ~wDrP0hqx z6~RX{E(BwL+wQt;Nsv-(U@hs3WlhAK3NbUA=G7g{Ik>xvUf-ECeObR-ZsBMB8%=5* z?l&e{o78ZssW&H#x4R`2#(YcBpp>k(HF>DuThVbd;gotk`&-VPh;t{OoVUOE^vDfM z1L-$!rEs;v{JlxY~b}c(S%=U1qfWQ!VW;$p%SOx*u{Hi4MoO=!fj52X=#LzansUx1R!F$x# z<0odlTOtlUaH{1_MBf`+rMpG z=qFbuR{EV@K(}YMgcuk{0Zs7Q1=iM?DdHZ@oC$iicHgZRZOpg&%_Z3UlT z4BsPOg$INqLN@rm9e<~vH@k}sh(8r$PZqN!4LG63tRQ6Y!GJY_yAi=feUR_ooTO7& zEVo;UguA5**4C-W?=C`HPI@NA_cY zc||7j#3}ptqQ6;a0s>a>|K{&6UHvH_%{(d351>4)kp);#T5erbGh_y)d0Ot*y=m$( zK5ZzJQrbS(RIn@dy@)$;nTm^Rm>8NE_WlmB!IChB1* z`;SS50*TNcHTq} zz^#0_8yyZ$T-=!=?i`iAPVki2L;xL$q%=P9@yAJ(N(~Rd`|po=DV9@_@6^!~i}wa3 z=nb9@fPYsNF`bDaSx=4qU|RtW%wIJ<7Csm-0NNEJd~Y$-UPq3I=^mWqtnefgV%tC} z1A;$}5X+kK`q!xkOYT&P{WncdT-E##E09L0}FBZ2Wtw7?t}czujsDPt$y7D^WE# zQm2yVZtDJjp>mfachU-;idlJ{kdj|~E&Ji<6x%m{B1g<+@wHbgz^B=_5tv_g=)Wr{ zXq$CjOFxd{1{4PLJ`|fMw|$9Xe+x%?sYE(dtq~;xTn%%|X7A*9p83d;m z^928ru#;H$kCMd5frgGgFhg*4-r=qXr;1zy#IR_9S}bCT=boRMe%gckjhXF#i1pSDf04u58n$wwV z|BPN3VxT9m6y9<8wbNdV?Z^dcqcigf?U49|B=)pr>X9A#9k^TTiZ!XbnFY_7a!nt~ zA3Wv>cKHTuBo7>M|3v;2JzU>SG6XypH-B>Zw&-8~x#<~C%-Ln!K$_neGYk1r3<(xE za^PWrz*fx9r)c!2=xtT#juqy>dKML56O%k`DSYWv>J*3WT#?zf;myOoR)z$?7M;W0 zXp!{Ioni~v(Ohm?bOBb$a0}p6#;Ru75ege&qFkVnxXl%u4t#FdoU4E(Q_M8*-{%6x zIzmX+>vTE}6~fjx3y6g+oenx(LfHCN5zvqq0NB*w7sg~jf6m#bcIOPb6TezufeFj< zPXlsF?8v^B&FR2|HA)_I*yvzi%s4}q(8|7cVJf$n7a#w{v9-u=@-PTjOS{U=M_cDK?MT7q*meI=kXF zZ}SVgzRfFywp&b4zu7f_?Z;AD<}G@5+O5wb9pA+|Ca07rzG?z#X`GHLmV?Z(4v^ zsV>$_vXi!(H#LH9pz6n+!ErkM5ay}Lbo0(3CK5WIY)N)FE%4rFZ2BHENY6b9tD;1w!$ zEXz_YliplqSuHm&dTn7EQF(Mv2K{c?)dtRn++Id{lYn%Pj)u%9Fqm+4jM*d}VUcUC zT04R_VP%Hk6}8m%_4AJK#_1N|?vi)hN8(y{stK$$ZewTWZH{^bx3jub7oP74Ar4br zDhA?AIMV{WV!fl8Tf~Tcy1X+5;DAQz)J;P<|=dgen#)(M{l8pQCx>vu`*+>P{AZZP$2!c!iPHly!9|Wg=eA(GL?jX^SWu#DjEe@emHh`##0dW*Qk*#%{>6)5US& zy7UxSRrHYLngqN;POMTNk?BH%oCfU~EpC3FYU&_;N6f3O+aRl zVRjWF)>NJP4?iVO$dT$+Qms+QI)}Y&bq&I(^Gkugt-t#j*E>McY4ny7ZSGDJmUPe5 zn4MvwnoGuK-nlvmF68p3Sky)Xf@L9;10B15maSW)8^wgYnAUvz+JQthXdg37(a?I9 zfJ|W2L5N1z>$2I4< z3=2zlVPsLAjqRw7u*T!A8f0$0ty!bQ4;*JOKwS}oKwzQcBKGgSDH0KEdd5AN$(Z#N zySoSpxpeEKBzc+&S@2vosyDI6*$OPgENcSC<~wSmXiy-sWXmLegxE}j!esb7rlX@@ z_!vpa9|{btO4H4uI)38nVxf$PF-K^P!$3kC7Al>zFNx&Ii@3A*;2`1gl1$=a%#Sph z%sI!BmUdcRH?Ujqv(x3(GU4W~)MRU@Iean#9{DeN48&gU6b2{p|btHV_A@zPFvtyTJ+ zO$Oq?_HQ^!aCAV?5uPxXWWOsTb-~Z9$2If00FT&Pxv9a81Z}<%w?UO}s{r*k55Ykm zb7-VaC%n?^SsgJmSa0Ul9KJu@C1Iwi6M4J5oCP;Qh2Etb3+!u>w3oh2w8^aa-*p0h+BGJ}t=sndPTg4CQT)l?_{FkBt9 zCr`6JE$NTBre;2V)%Eb~=J;NQZYd<0S(H+9pLJl!qsL7LPw9P)Ox zcVWg4G$b0c(YI4!>wqP>!z!=DCT8D`H8C1f*#4(l(`12rfD4Se`8$ zEY66h_#QgFHhFyml^1zKecUu8rdtAo(xEL1c`QTA?}JTD)C%y3XQ!9u{b z5!B~+aUo2DST)Eog)>NVOXT@zTMWSvp5Cy&JUF8DdQ2hojX%TFEF~t@(?jS&$S2)} z{$;4zgljT_m#wBM8YYj}Gp+G*;iq{EEnSx~8ouHcmO{g-Uz2OaZvXa}b#F$sQx%hf zM`$%a^^+A4f#Ypj^wzZ^?JZ}`iCFRJa^kJzs&pnLJw8*I`6bs{eXbFfK2F$6!>OZt zG{`VR`y`UdG)r+HZZ%cSG>Gb&S;E%6$HybykJ(b{w(18N;$_pP*q@s~5wMgDi@vA= zXAq}y1`}zJZ(AOkpKKf1)Y9)P5t2U5m7Ed~+VXS^@v9nnN{%f#Q17)@jgYjHI=Xt$ zHj)?rxe*@C#|x}SDzzFwr1Nw;Pqv8Q+XA0%AiOv_TpD9@GT43*&t6@MW^6|K9EF( zS>#qfBEz0zOUmmI>VAkkNOjO+h;O4_&=rR~TZaloh90g;Og|O}s~Ct+3>hJuiS`RJ zb(T23g!NTN*YdmQOF}4PhTV~JgV0BqV8QS8aiJc{h^-^JwjK>bb%$CxtK}l5-Fy+`Q2kuTXEJs!emIv?hXCsv1I#=bU;!Yg{EH>67D$(78S}kP?#u z!I(0m=%sB4xcU%e32Lu3oM|qU#f5Yz<=oE{yJ$4GbdyrLg;=e-<3!*EGe)>y{XQKyiFsS zEzprrZXfajtPTao%L-ZJycDXMtV;P53wB8gVL=wWNLdX6^m!X|;lM5H>LLto&KYvz z>bj8>G=Lcf`$edmI`0W%=l116x@V}a{k=DmxR8F3pV_2+l@Z2>$dYQ{ZUxM+waY*h zGHbGx;R0qLpIp{biu^L9L~^{$gbe)bw3&M|FQsInVZk9ZBg48qBP7GlJZMvup?ymu zrH$zr*(j0l$=TEu?DKQeEO?RGKtmDsEEl0kf~*sdG-dYXVN^KcfE<98&$QWDx02R) z^-tPYTa$qz=WP#+Spq*SUDRMm(2 zx~^r%Dfz=B7#f9~^l`jw8-p;(9w9iUKslLygi;+NlPR`zobnb5K|S{^gejdML(^P~ zaJC9IJq)o8WBj=ndj3Z zdgGLDgjf&q36j1{*dTCOHe1mn1QKL;iCb@iOwaTSQo=%?8II~EoE|lCT-qqGFKkMt zlLu8;2p>4@OrIUjglmBV`3SsJ$F4V_0h!Pmr%4ZS*pev)g<-)Wk5Xv6nXuk8EKHo! z^h}V446R2dT`7rTbBWIk=T_&FNZvpxTEMa*IQ1s#ew6M~NcV7_Zd;x@mB&bq$0MlH zh&)6#dZaZ@-Z$C0hHY_C7uaY(-9UwKh_3VyTHpk`3}mxlf90!_2hFEAA|w>T zOEWPM*jIOP%E9jLN(rg<#~X$7ZGkC*_%EYU!-j-x1}|>iMFl?I{5Vg;0YcHq@KRmg zc?f7TGZ^D&U8f}p(s6TViR90&Av4X7jhSSNm(H+s`E#5;RjyYbEvP31FGK0NLo#s0 zgd?HLNH&9sm&I`eGmWrjngu_+%0U-o(3yofJjQJEB_OL#kO`ogZb%DMSpe(D#`R;x;tZ#PZy%rTQrjCZXceb@%IYt96Ml}y4t+?7jSc=nbxE8gl(68xSNoH^)8!0o zAkKh+ zHE|pdt0Gs~`dQpF2O-5@RwvCWb>nk!k}{qcLW0#h?72a~%Wc3O_d$kYgQOK8U8g*g zggPCQ>ORszUfk*Tu5q1=iwYpYaf<27Ae1J%orQ;US;W3X(k-i&f&dUPLNwz02B)3I(;z?eoJ%(iG9vI&#%nmWBP5XLY~5Lk=1&rad5-A`zQ{nWF zJ2t2wZ}-ttAe5pM*tC^q3L|sCfIz~dm0YJ2O;$x9<0xXsqX?hi=$gAGuCa?4ECRl~ z+Fu&aqPqRIIZiDC>+_gvL6(l&kj!I%b6%VhwrinLBL6TIVeWdaoc#9(SaIC3%-Fe03HJxw^FUrlES{bNNDdRSF%-dEBLZYLhw4q86)jVI(p~%2#G5FP>eU}&YuFlR3~+=r=b;kft#KSl z8swHKMD;O$BChyWodkMeH2T{`v3uUBC*%$m!J^49MWTq?m#5>Pi(|p9GLWyo+*7Fo z7Vc)B4lWbin2@R4EycoH7B5^uN&*{5ifK7l3C@BR{D?pm5Gk)rbA^!5EsIES`Ua`XR>*|G42)vB21n=001Y8a3-IM5trtP?04Lg}1Y$SS z+X5V(K&@twt5WUhtQbOE@wywfsNu|c!3z4e!Dy%gr0!@qHg+AP3-+Wkk71Re(E_ph zN>PJ4iE8*DD>aZzc!dZ+y6glp5Q3-EAfYvF2MyZG^o|VW^p&cPqM;l(z3{>~?WYht zA_k2cBfPi(5;l`xHCZjS3CFEL zIYV}vuPY}i7G4%|Yx#INV&$Iv)+DMpz$&moYAqRzF{#Ev=Xz5ztMd?fC|{kp)z0WD zsvE5l9_gAyvKBZ(gE}BfA<2vpdB8bxc+q~JYU{9(jquVsNIufdNtb4#PKJ53GU(Z9 z<}R7w3?&_n2}!Qg-CL(4%aSVB-pnXNE<-Oc8Dwaz&J1k&?-&mdnZSvvH_q7Bi3F|E zXgSXFZRYAbK(QdF@fxW^FqqCpI5xP^0s&i|Y1Z#j1&%wwQ_ES`+vuK&ChO8j5XFS= z&=4bxlr!@M%d4HsS^6cineG7X$vmGC2NEBIMxo*>kVtJ2Wsft%EB9D{UFPjzGF69$ z)Xr?ogVdi8tD~BlM{FQCP1=mt&oCa>7wP3^s z&vV*&kd{TR*|Bulx;-GLdKCsv0<{RH=>!>O%Jk0JoLVsB>H|W)DCbPJ5bG?_pFBpS z8=(>DP2C-F$dKBc0OSWoB@zbm8Se^hw#?IU zftTDBr_8EDvZq*+4gQc3g|HqE0V(*m8i>`5}BI@{u~P);6-L33PcVFR7%gPLGV zZz5%;F_;WoNaJy5(;nAg1qiKc!jh*cp_&-L(^!yeEA^%J7KJ#vzV*4~pDB^Ar ziqmIJYmolWHZ2UnBI+GFZ+RxhkMGP*3Sfx7*A>ohTzYE;PcXxD^~o#H~ zX5XCS3Bhm!iIYsveV|qY`7&%34ae}P8FT4BCj*^ptG?wTF-~ zYkqZ}dIWM!{7j<+DvT@1JDGBw3^QiBoHroPzI3S>EQNN?V=uTQv@-HR5f|I{vPes{ zuSn7dq?UsTt=C?+D1UZgtQ2bwRhtNi3%U3j5YR`&Ssh|8&4A#kWPvfjv*0;jNlxZX zwLOi7Oh`r?llC@PH-=0S{IgJAo%y3N0;}Vn4%zcp_zaH_ixc5dp3sAe3u*5?2?T>_O>=m!`{w=NEy^ z1?|+1C)V;E8}dnkAP9ca&LJrpkCDKp;+(@mltxfJb?s4eTyT~U;;@(1@0qKCq5-U# zNpiJj9mxbyv!O_TLsvZCu>E2;l&51%#_)2KM4GjIbFzH}7P?g-;?#~2cxhn65KJmDRem^BBluWK=}ve8 z5x**#cU_TUjST@;0KPdS**@f2YNIr8qFE#(CuF3YxAj1rgaO8^r>Q=K1pKh1WF&Bb z87&k$TTqlS=Oq)s&BY#oAlOkscLIdph!NlsC|z^{h?!OwbsyhN1Ep+qofZlZoFrSX0AUsVW~Q<4 zfEGXob2km~jAZiXB}xh3zGa-480=SV1;Pk*pQ2ESmjX6LW=oER103S@fS8@2%QOn9 zg!&SQV|Yb%%fp_IIY@X{YAgvl6HRql0QP_b!I8p?cdV#V*d3>s<#`U9G2W5LsDyM| zlR|6|Q{WWJ-NH0Zpg|^ekk*JjonjY(jok);Qo<`Cr@GD&L4y=!D|JS<;|${W{gzFfq#$vL10lU>ou5 zEnqg}po92opU_;SH4q0EIGTqzsE2cFEvU{Av2GI%$r`6Qitj7NZPf4m5ebe$AZLM; ziKCbxD$R)MAudjVk{Y$CimVFK8?b1`36(o7B6%T=XF<-79;<`%(sIGE0+hWOGcNXR zG>Bv|1WF9)s748SACP^5T7YSKbL7c5&HCuv3{%%Z?aY+p$)F(1N#vmtn6hV=$i8l~M zs(+Sa1Wd2YjG@^lwuUP{MKg?n>SaiV*e~3Js+@{+H)qg$nIXUjs?Isv4VyN~ATwrJ z@b03Wj#3D9DM_Dva2t^BDjGN+Uc&(eV^T=h`4DivF$UF;#3WB=qGd}HUHIJt9EJ)9 zJFYJkzM8^-|HPz-QL+Hb z*+I`?nlAd4gJdB%CM8oCYYMJgAQ;11RTY_0Ku|XmU%aHuPvr#RveT9Ea=|vBj^uQv zXhpg1X3{0|*2)Xs*`@Be+VR9uDQfH<+&XRV!>_u~YMT3E|&{!QxBL$RZ@#)F*E69T!*Avkb=y7|WlGYFxsu*c8B zx(?ceI!SNo&1;0)NmCPM#kVghKBX$Uaf)ayF)OR7&6jLcd@2yg0Igu9Z+Hc6AIkX` zdR(KenfUD>l1(R9R0RQ2U6&_VP&!YDYWxLEOf>9=;}r&T#r2;f!is7h1CbAit3M%0 zM*x{GPvoFYhJYyKEmNR~q|c)!Fbzj3ps+ew2ldgMb4swcL;xNGQF-TT*#kn8IoH1D zKwXlm0^YT1)-VH^!@nrr~3Go1`8gk9YpgJkJkI#uo2R%ww7zgtt&tD zxi0dYh2W13%mF{&)Fk&CzWRgXCv_gQ^{L3S6)tje-^jks%R9h-$7Vbq%KGula$9U; z-=k%yC#tNEu&|?nRwgtVDVx#UIn$QWbg0aoy>RvW&y&VYR$tF7{IU%FUVUg8DxZz5 zN?Te#R(W_1b+&s!DQ)3Y{XO<@OKfs;dbV4%h0NGz4?oZrejNp_ea(;tMj`GihSy zbE$SQzcJfH3{!J6$joG#g{i7J~b2geQny~)xmw0af5>KhJftIu6`@b@wyRf61 zmi4zv#Z3H5*jCpXm1tL^tdO%l3Ty-*^xI7mo7H?j}8OS+bed8W3yjzpc{$)Y#bHDhs2oDB=Ig zgm@*EA%OV5FU9NsvJ_|k8{Sf@uZI89*}qKkZ=K<8|7ke?T2TA{VGYhd3jb}s|JK8Q zH~oLi_x$Mpwrc+i2mjlqh>BH-+5Yp$mKdWpIHpEt&Q_CgT=T6*x(o3uc^TR~fBjI_ z@0tZ|g1wJUQ5Un~mbTWX1^dfyYMu(XihLcRxqZ(j$aWGZtF>zK=&z`R!_k3Pm#Edt zP&92ps_;4U-S<_UCo#JUn;MiAA8gTASL%7EAYO-M=!&Lr{hi5OlfN#l{iAo=yl3Fe z#r0J`9`OpmUGc0p4W0k40gt{*3mb;nAf2=Da(W#Ly7RcGkny(50&|bAN(k>^rp?^T zgNG_9C)7yVVb>lPcvbZ&a)Q}8s;E}0D@(Fb`+v|=d$0Mv?NZNjEc$UGuV|p;L}oja zx2m*Cw-ePca7NBidpmw4q4MtZ|&gTTynEnuZj#`!(krq`s%XAdiz@;eD>rGyxZMU zb|v^wOMbNyZ0793f!;CcZvNc9Z5q9UABs!XzYBt81PrG3dfWc`=;P96y{eyZk{3uDW6l@6_yV~c@}QY3^>#~TGjwT%wV@x|9(uSn@5@(S%&rp&Zb#*!?q~dQ zx#ROhk--n8t9Q!Femt$ptp4M0N9?C%=**;cb*EiJ+PCkM(a!=pGmZDx>J4sYllMaF$|N@4RlUl}!b zyNYd`g%50cQPU9EdrOi8n{z+)so0(j z?2+I05oeM5W)NW$ded_CxFxrzbL>i(S`;HZ(BZ|iCofwP-afGj3Dq{JF5;{$FiDm9 zlqmi6hTa{9A=|F!qVuomVQ{N=+4XHlkRBzSG4*B0M+*m7Gddds_RM>{eXYT$R14lU zzPkAKhP3A#M(t)?Y4=e2o;hIGGzj3fQPji+vfd+YL zr_OXHFgj5!V#|9dZKp-;jeBU@qlk!dJF=S=WUIXi+d~|xe&*4AcN*Z8>$;w{K5gf>nI;^nIbNc$ zah2~+@g9TkkFSMLy|0eFugF9^U(6~a*PU~EVX14mF~PxM)#2x~Q<1-I4*krFFnU5> z3OgY;|KyprYW|k-J(U3-l~(fR8-F|+6-@AWXQhq#sS%CSPVdeHCrOT+ec;;00p`!R#v^Q_GmAwK=T5?j` z0+`B|f9V?iP-46Ps8>GCQhC9wCDiGH3yW zoLw1J(t7I~_wo+2qT&zQL54o zJ@wKFO{>%YxIa@`_JytUD}a+DPG&^qV7|j!<=@%~@Tw>jAnzp*OB(;E<*G-DrzJ&T= zb>X4VVt3Ds&)fChn>J`{Drwo?ymgmegv8pdC(N$bYKLhVN=4iWR9aB}Au9W!{bo;= zT&Vh?rL8lu4p|;8JGr+{UOE`BcKc`?Ch@azjpqS-y`%dH_hz3}#cB$>)jCI**9PB= zNp{S*7Fw&lL zV9PQje>!0M>CFee8Fq2m+Io(pGuLjhY8=Xa&r{t$KDp%`<4-wc*}!~#b0~j$R@Tk|1*pZ<`fZfCtG)f*^WYCe zt94OIV04J(8*1^vZ@4<#@{}kL-Ej=~evjhxboyY{Ohi&Oauqt>@rZKFk$;YevzCXUQpa7=3i`vo1)g|T z8af{kU|zJ%@bkQreUo6t@QhkuC5 zy>O!a*lEj;n+?&fQm3Le%y{&uJK4|p{SH1)UVSldH0sbXhgNQ{fO2&FInJH0Q?+qqy8s>cdyulUj z+Pxc%BHn#Ib5eG4H8uCxeskd z^HrRgZucK$ZzIbTKB=EPaMb-N-R9!@u!~+bwC5qJhw^n~t5v21zHiDP;o-!?@~>8% z`jNKx`O}%)9M9oByw6`89T}nr=g`d1P5UU&_?h5rBl|O`M32fCote%DGk40pF*A`m zNxr^5z4`0j_qGqvdJk-U_98X4L*@^b$JL;kuuadaOnpwdE#8f?xgWj^eVu-bD?BLq za^~#OQ#tJiw@=&(T-&t8CgN81Q2T|;o8Ykc)&g!K8<~1Dz09|6GE{K1q&U(hKOshC zVb_dV-~U(1mB&NbcK=aHQK@W^C=t;ZibxnsXiSow8D>ZxhHP1@VMHjhZx3FSHEWSA z%S`qyWY82bjHS>pW1Sc?zuWUZ@9%lu_w)O_pU>})Gc)&nU)Ob?`J8i|>zwa7*EM)* zG5Y`!>s}h@NwFJ@s3udNJH;5KWhCe)Rcnu#$DG2q)<0BekIf?m3o?)&whgGfwBb%N zudg3%GmS1Z9otx;8FxB0qLU*2bHnecSVDb0>L5SABu70d*@Z0BqzFtD@{GH42TGjw zJqn_(OF!#sB_=rPd|l*p+kqG*q;^*Dz%Q4%Ll19>-bn*(Z6=!!vTBiF%Qr|?&J!@~+otE! z98T=TLgpcwN?akWD2(Y;#u(7`^H;}|u|ZDbUlvR%wNhmqlQjmu3EhW$d}!Zjj&0nz z!BA<{+{Y-V@E&7uZ^{8hNY7q1y$zJ-B5|!3=<-Dr zn|11SnEP#``94^!VC@|ly%%tV%=!X_t+8Av_ah3Fmm1stC`YTKeU{xCQ4g zXnA>aJZN%0dt@f6cLa)QmTpHFMEVj=W6#q9qNHe@NylOwAD0+;OH#%JwDi>z`98`B zC!f50eaec1!W$!`Mrw0g3HKKhA+8%8TWgPOuk3P^e2RFlm2)rZ>YYLm*QBJ1>X@8s z&FdW-(u+5NKgzbM>fpX7Z}*5BVBrkuDRTHDt4<)y{`MooZ}}h4cfM>;q}N0z5uy9A z{DxHQy%sL_%4CpsZ@%+na{;ii9338fl78$tZ3aoJ3teWaZR_AX7~j&~KgnDjb6s9l-D~8Oka2wy zDCAXMc)C9E0WyGVU&Ajb=3J>kzAIjp;WHHKf(Uh_doUbx9WI7K-^R0XJLKD>HhAEhX)NqCCtq3SL{ zGb;K@^(dbU$X@h5;#Dy6D#b!dPq$irI#pVO)7zfz`r;nBBQLI^n8izzH)+(&02DOD zgC)cS)1*HH>B1X7OVOe2z}DP)qHkm@SoaRmcf5G&ZN$X9%M45mQWdk@`}$SpgBh<% zy4*DSi3k29)?p-{X7!{=x|Ff+!=joF!`RjxZXK(%7NA|BFLA>v-XJV8*%MMzr+=o2 zr;AX-6~}x`mu`jf7u8e_`{6iSD=`Z^Fz@}9cbyXNOdq~d{AE!MGI4(mA$D%JU~05U zdWLY=1$`6?nf_h#Ji<=9!VdFbpw7p3B3*d7sqNH}mf3(40zA#>(I>k!$jFwSewe&n zYSzhxuWOGm74z6COU?12gHQ}4%x!^od*|1fRnKPEv`QfFOrj#0s2O=rlKSbOn7-p?zfjX04 zzI{BG{n=5_L(=n7Rj+#G(>qp4gE|`0EWyZ7=II>;<~ia9geTCsYwIea_KjqNN2Q{W z$o+)owzda&({6g&Kg~365#vbtOcUBfpUs%DY;$Qt;p}9N0{6@K{rjw>zpFxB#RVq< z`#Q6w>9OH?jH26g>I=2sDqo@xpMUEnXGfVlm9E=x34T88J@Vrv7-&W;jkdD+B(5t* z5YO{Ntv2i>1JcGiNb+>9c2{$bAf9$B*IYiwA8LO>z$R&{@kzjJ_%}USi??Rj=ZMuY z@}%XcZ=J!y+j6PbJ29uHq3xbWvF^0xFd7*~izuh{wwaFl+M!w(G4c>0`1^+NT>Qi1 z9ya)a!{VxUX628#c6}=&IOjx&@z&GQ!jyryED&Nz^gDAwE{88L)0WQl1*_-j?HjAR zusI%l)6xDa#hNj2xfhPjp+jba=nK;iogXsA7=nehzgUZK)NdM#5gaUz-7Wrrd3}0)F)(;<=MnS)CYC z_HAF`%>yA{9r@|jH%Eepqzjo19_uxYNTly%+gwVu#)CdUalQiJ2C^iJlNBH|iqDL=V^s@;Az~^54z#S8Uv`x;}_N z$h-u4>``F|5Et(C0{uam=8E<&6&JN;ca6aOAX5QLPV0kQug{-l9ZUr>8eTwK_Yh#z zvOQ$xPcpm3%O3t4FZY2_^WY#;qKVNKz=`Z0l=DX?^jl8;J2#hfLAedqO^SY!uB*2a z-W{QiA%(ZPi=oqkIjmqa76Uqi&L`3yzi`zyk3DGlXh{ar(y#nubTBjL)IEAqbVc=Q z+Aq->fe`?w14Eh2qFoxUGmfEG#0KRgvq5-X#^)UR!TLc~2%5!Z%myV%?>^`ueBa(F zU>Sh*{GhaG;3o7U8x%aq1_>B2C5G9c!CoAhiwQtp&y+iH>-*Rsni`JzJFdb;g$W!) z;Y;YAdtFh)I)Pz>zRy8d!uAkiu=hVl!HTnF;}~JK&TNnt1i+dBHhLnrXOeJ(8(VN@ zEFx+V_tzi$x3{5=(lgkg--Q6L0$^dbZP)w%WD_e60I`36b-Tz6bejXmI0I$ff|4#} zvT$!2(6bLltPG$L6|bHbz!FVgX1y{@?KydW98dche?TCcR6>Da`|$} zl;v@Zl>bBmlc38hy8%=yqH%-*axScPUEMTlt6r1a6@P)G4)ZBQ7*F-{-W;I}-_SgV zjPC1(T!@hrEW4DKKFeJ`FFe>CUv=fmnRaLg^uP?wvn`ViIvyBlH%!IC3mCCMf=jTq zuWNIN=buki9-r`&sQM;vb7$g;nv3|x`#-lx#KjfVz~p|t$3kJO!+~{OR~UD}2PqxH9ap!@+xZNa6UMT{%tI;H zlitnwNG3cTJ9U-jV}cPsxIxv?IJW^dY|{(6OEzze9b1sEfS8_yP}@| z3nK>8`%Ti3%Qby@-$}0&3%|$Z$l;7_aBw?*1^!)>z$clX`;)cuj^tS*P8eC0m0Kcn z1`js);a_P+(z$u7C#0jAG>Td@V!qK^yz>`nJr{kuQa|+6h4xnI13FU_5sKp%Hsf86#0;c)N=>%hKr{|qV~TN@V{3I3#=3K0&LiL9#6SZ6jQh6HpbP=DdIx1_G87OX9ornFUh(R;!C& zc5eU%HXeZZLjI(zhCLI#XV*b{#2)>dasK1Zzn=W-`(oUdk!Je2_WMBx-EJZlDmCIz zXE+1I_~|MU>se5XPgsX(B$R(aq&}pNKkcM73fXhNBZ0a+ZIH0v`J_Qig<^NhAP-+z zrDQ@@_XmOV@!G`-LA-P5MJGcgx{^7n(m%kcqF;$JY&U$1g*3}jiz(ljke*%6B2C8S z&+g{exZnP65!o)HzKT&H`PbE^KGG!Ma&4`9oLy@Zx;DqrWZPD{#B)oNzN0f-E+}4W zMxnaybk6a>0K_MUtePP2CT&OGhVV~8@^q@@#}M(4D&Eh8Tx|Z~5WCzf)Vs%uKJP1L z=|CH89<5!Zp9U^>GejUhS2Z^P!Jo=l^Tc+^e0@X7sC3X4ykb{Z zJ*fZMIQcnfW6AIFI?iLp7LmLXyr|}y^*Obf)K^fgnLs39G^`AXKT#_>X{Mv=R0%fd z<{HVi@m=NnsIC5h45Q03{dp**#Khn1VLQsZ(KSVtYloEtLT7YKaN{JUMSS?ROs1FYx}E*u6qUFX#q0u;);vBbLI?P@aE*0(%4X8a=~wz z%^CRdozjVb0AuY-Yr;QwGqW~phz)hl?#!)WgXZ0(cJVWBnqbq7G0;21p3vHg^=Ip_ znnO(8VyVakkwYD=3y1n#>hunN#!{hy#XX2M<7Z+D^^7L~j?r0n^|`}^civ~(5FO`l z#V3c#C{*-@S4kXA%&WNW)4eCU7vj3VtO`PVyncuIOiw%iR+m)e-(ck~wOlH7ue=CJ zr+717=+t1)9?=EVsx@42Ggy?cNZ$R-(r=17vGa@#`i7oE)%ZxXxnHWvdi|_Db&4Zu z)jIGwSMHN-t5q`56~1&0>sb>`nasVC#e*zztn{p!r###b4&ZV`9%7fJgSd*mp>&K0lvR(M{r> zp0~Oi$<^=&A^xoCFCp=@Egv=rHwyrhB@+S@qJ0AVc_OiW{l7P9Tv(>yDMbl;j!*z} zi)9QrLJ~8q;APwjGlyw`%+1*>yuu2GuiV`(7oS6Wn|0R%E?Q&G4jUv5cwX>XTioEp z;4a{yu|ZL#<|ezvFM#K9D-Ih7Xzza=II~|dXKfYzn73qzWw9lIjgI@Z^PUYNtm5eA z5C4thQGPu<%d?b zxytN6cc=v++oH#B$r%oKJUkhO|Lay46@;()d5MD-Wq3ZD>u2C z84esK*aRo)klwGqVO~Us=B#_+kF!B7P9!!+%7hK#wwfy16@@&7(ro+yU)EyH>Ysg; z9#-Lk2v^KYOaUpmt)#I*lGm><2LajK|L9?!Ln{d8#tpSHfOI}*8k(7cR(_bc*ViBu z;9^bbu_1bm8U)l;%!Z+WlmGr=h(%&v@SfC!41EhZj-|L-);u!jJHU%Jp3ANXM`7 zZCoq-Y=-HZumlv+@&hmro`%Te0wtA1jz5;g0(DcfdQM=k0t^ra+W3(nNap3xp0vzU RL-^~(s!v22#zWa({|CnX9jX8T literal 0 HcmV?d00001 From f30332b12bef27daa3406b0067369e93177ba0df Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Tue, 6 Oct 2020 11:17:30 -0600 Subject: [PATCH 04/18] Fix RELEASE_NOTES --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 135e62afe6e..30fcae086ed 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,11 +6,11 @@ A new header is inserted each time a *tag* is created. ## Next release (v1.9.5) - Added a new Live Wallpaper Android sample +- `UiHelper` now supports managing a `SurfaceHolder`. ## v1.9.4 - New `ShadowOptions` control to render Variance Shadow Maps (VSM) with MSAA (experimental). -- `UiHelper` now supports managing a `SurfaceHolder`. ## v1.9.3 From 52913e685e8750ad2d5e55c5c26b857b61287054 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Mon, 5 Oct 2020 10:49:00 -0700 Subject: [PATCH 05/18] Vulkan: support readPixels and headless swap chains. This adds support for headless rendering as well as readPixels. These features are necessary for automated testing. NOTE: invoking readPixels with a non-headless SwapChain seems to fail with MoltenVK. I will investigate this later. --- filament/backend/src/DataReshaper.h | 51 +++- filament/backend/src/vulkan/VulkanContext.cpp | 52 ++-- filament/backend/src/vulkan/VulkanContext.h | 3 +- filament/backend/src/vulkan/VulkanDriver.cpp | 222 ++++++++++++++++-- filament/backend/src/vulkan/VulkanHandles.cpp | 122 ++++++++++ filament/backend/src/vulkan/VulkanHandles.h | 2 + 6 files changed, 398 insertions(+), 54 deletions(-) diff --git a/filament/backend/src/DataReshaper.h b/filament/backend/src/DataReshaper.h index e62289d6db1..16fca083f97 100644 --- a/filament/backend/src/DataReshaper.h +++ b/filament/backend/src/DataReshaper.h @@ -19,29 +19,56 @@ #include +#include + namespace filament { namespace backend { -// This little utility adds padding to multi-channel interleaved data by inserting dummy values. -// This is very useful for platforms that only accept 4-component data, since users often wish to -// submit 3-component data. +// This little utility adds padding to multi-channel interleaved data by inserting dummy values, or +// discards trailing channels. This is useful for platforms that only accept 4-component data, since +// users often wish to submit (or receive) 3-component data. class DataReshaper { public: - template::max()> static void reshape(void* dest, const void* src, size_t numSrcBytes) { const componentType* in = (const componentType*) src; componentType* out = (componentType*) dest; - const size_t numWords = (numSrcBytes / sizeof(componentType)) / numSrcChannels; - for (size_t word = 0; word < numWords; ++word) { - for (size_t component = 0; component < numSrcChannels; ++component) { - out[component] = in[component]; + const size_t srcWordCount = (numSrcBytes / sizeof(componentType)) / srcChannelCount; + const int minChannelCount = filament::math::min(srcChannelCount, dstChannelCount); + for (size_t word = 0; word < srcWordCount; ++word) { + for (size_t channel = 0; channel < minChannelCount; ++channel) { + out[channel] = in[channel]; + } + for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) { + out[channel] = maxValue; } - for (size_t component = (size_t)numSrcChannels; component < numDstChannels; ++component) { - out[component] = maxValue; + in += srcChannelCount; + out += dstChannelCount; + } + } + + template::max()> + static void reshapeImage(uint8_t* dest, const uint8_t* src, size_t srcBytesPerRow, + size_t dstBytesPerRow, size_t height) { + const size_t srcWordCount = (srcBytesPerRow / sizeof(componentType)) / srcChannelCount; + const int minChannelCount = filament::math::min(srcChannelCount, dstChannelCount); + for (size_t row = 0; row < height; ++row) { + const componentType* in = (const componentType*) src; + componentType* out = (componentType*) dest; + for (size_t word = 0; word < srcWordCount; ++word) { + for (size_t channel = 0; channel < minChannelCount; ++channel) { + out[channel] = in[channel]; + } + for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) { + out[channel] = maxValue; + } + in += srcChannelCount; + out += dstChannelCount; } - in += numSrcChannels; - out += numDstChannels; + src += srcBytesPerRow; + dest += dstBytesPerRow; } } }; diff --git a/filament/backend/src/vulkan/VulkanContext.cpp b/filament/backend/src/vulkan/VulkanContext.cpp index a61ca69b03a..d749d53cbc9 100644 --- a/filament/backend/src/vulkan/VulkanContext.cpp +++ b/filament/backend/src/vulkan/VulkanContext.cpp @@ -316,6 +316,13 @@ void getPresentationQueue(VulkanContext& context, VulkanSurfaceContext& sc) { sc.presentQueue = context.graphicsQueue; } ASSERT_POSTCONDITION(sc.presentQueue, "Unable to obtain presentation queue."); + sc.headlessQueue = VK_NULL_HANDLE; +} + +void getHeadlessQueue(VulkanContext& context, VulkanSurfaceContext& sc) { + vkGetDeviceQueue(context.device, context.graphicsQueueFamilyIndex, 0, &sc.headlessQueue); + ASSERT_POSTCONDITION(sc.headlessQueue, "Unable to obtain graphics queue."); + sc.presentQueue = VK_NULL_HANDLE; } static VkSurfaceCapabilitiesKHR getSurfaceCaps(VulkanContext& context, VulkanSurfaceContext& sc) { @@ -377,7 +384,9 @@ void createSwapChain(VulkanContext& context, VulkanSurfaceContext& surfaceContex .imageColorSpace = surfaceContext.surfaceFormat.colorSpace, .imageExtent = size, .imageArrayLayers = 1, - .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | + VK_IMAGE_USAGE_TRANSFER_DST_BIT | // Allows use as a blit destination. + VK_IMAGE_USAGE_TRANSFER_SRC_BIT, // Allows use as a blit source (for readPixels) // TODO: Setting the preTransform to IDENTITY means we are letting the Android Compositor // handle the rotation. In some situations it might be more efficient to handle this @@ -502,6 +511,9 @@ void destroySwapChain(VulkanContext& context, VulkanSurfaceContext& surfaceConte // VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL on the subsequent frame that writes to it. void makeSwapChainPresentable(VulkanContext& context) { VulkanSurfaceContext& surface = *context.currentSurface; + if (surface.headlessQueue) { + return; + } SwapContext& swapContext = surface.swapContexts[surface.currentSwapIndex]; VkImageMemoryBarrier barrier { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, @@ -578,29 +590,37 @@ void waitForIdle(VulkanContext& context) { bool acquireSwapCommandBuffer(VulkanContext& context) { // Ask Vulkan for the next image in the swap chain and update the currentSwapIndex. VulkanSurfaceContext& surface = *context.currentSurface; - VkResult result = vkAcquireNextImageKHR(context.device, surface.swapchain, - UINT64_MAX, surface.imageAvailable, VK_NULL_HANDLE, &surface.currentSwapIndex); - - // We should be notified of a suboptimal surface, but it should not cause a cascade of - // log messages or a loop of re-creations. - if (result == VK_SUBOPTIMAL_KHR && !surface.suboptimal) { - utils::slog.w << "Vulkan Driver: Suboptimal swap chain." << utils::io::endl; - surface.suboptimal = true; - } - // The surface can be "out of date" when it has been resized, which is not an error. - if (result == VK_ERROR_OUT_OF_DATE_KHR) { - return false; - } + if (surface.headlessQueue) { - assert(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR); + surface.currentSwapIndex = (surface.currentSwapIndex + 1) % surface.swapContexts.size(); + + } else { + + VkResult result = vkAcquireNextImageKHR(context.device, surface.swapchain, + UINT64_MAX, surface.imageAvailable, VK_NULL_HANDLE, &surface.currentSwapIndex); + + // We should be notified of a suboptimal surface, but it should not cause a cascade of + // log messages or a loop of re-creations. + if (result == VK_SUBOPTIMAL_KHR && !surface.suboptimal) { + utils::slog.w << "Vulkan Driver: Suboptimal swap chain." << utils::io::endl; + surface.suboptimal = true; + } + + // The surface can be "out of date" when it has been resized, which is not an error. + if (result == VK_ERROR_OUT_OF_DATE_KHR) { + return false; + } + + assert(result == VK_SUCCESS || result == VK_SUBOPTIMAL_KHR); + } SwapContext& swap = getSwapContext(context); // Ensure that the previous submission of this command buffer has finished. auto& cmdfence = swap.commands.fence; if (cmdfence) { - result = vkWaitForFences(context.device, 1, &cmdfence->fence, VK_FALSE, UINT64_MAX); + VkResult result = vkWaitForFences(context.device, 1, &cmdfence->fence, VK_FALSE, UINT64_MAX); ASSERT_POSTCONDITION(result == VK_SUCCESS, "vkWaitForFences error."); } diff --git a/filament/backend/src/vulkan/VulkanContext.h b/filament/backend/src/vulkan/VulkanContext.h index 53e116fa486..0e011b0f35a 100644 --- a/filament/backend/src/vulkan/VulkanContext.h +++ b/filament/backend/src/vulkan/VulkanContext.h @@ -145,18 +145,19 @@ struct VulkanSurfaceContext { VkExtent2D clientSize; std::vector surfaceFormats; VkQueue presentQueue; + VkQueue headlessQueue; std::vector swapContexts; uint32_t currentSwapIndex; VkSemaphore imageAvailable; VkSemaphore renderingFinished; VulkanAttachment depth; bool suboptimal; - void* nativeWindow; }; void selectPhysicalDevice(VulkanContext& context); void createLogicalDevice(VulkanContext& context); void getPresentationQueue(VulkanContext& context, VulkanSurfaceContext& sc); +void getHeadlessQueue(VulkanContext& context, VulkanSurfaceContext& sc); void createSwapChain(VulkanContext& context, VulkanSurfaceContext& sc); void destroySwapChain(VulkanContext& context, VulkanSurfaceContext& sc, VulkanDisposer& disposer); diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index 27f73e00172..6a4762a9014 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -17,7 +17,7 @@ #include "vulkan/VulkanDriver.h" #include "CommandStreamDispatcher.h" - +#include "DataReshaper.h" #include "VulkanBuffer.h" #include "VulkanDriverFactory.h" #include "VulkanHandles.h" @@ -553,22 +553,19 @@ void VulkanDriver::createSyncR(Handle sh, int) { } void VulkanDriver::createSwapChainR(Handle sch, void* nativeWindow, uint64_t flags) { - auto* swapChain = construct_handle(mHandleMap, sch); - VulkanSurfaceContext& sc = swapChain->surfaceContext; - sc.suboptimal = false; - sc.surface = (VkSurfaceKHR) mContextManager.createVkSurfaceKHR(nativeWindow, mContext.instance); - sc.nativeWindow = nativeWindow; - getPresentationQueue(mContext, sc); - createSwapChain(mContext, sc); + const VkInstance instance = mContext.instance; + auto vksurface = (VkSurfaceKHR) mContextManager.createVkSurfaceKHR(nativeWindow, instance); + auto* swapChain = construct_handle(mHandleMap, sch, mContext, vksurface); // TODO: move the following line into makeCurrent. - mContext.currentSurface = ≻ + mContext.currentSurface = &swapChain->surfaceContext; } void VulkanDriver::createSwapChainHeadlessR(Handle sch, uint32_t width, uint32_t height, uint64_t flags) { - //auto* swapChain = construct_handle(mHandleMap, sch); - // TODO: implement headless swapchain + assert(width > 0 && height > 0 && "Vulkan requires non-zero swap chain dimensions."); + auto* swapChain = construct_handle(mHandleMap, sch, mContext, width, height); + mContext.currentSurface = &swapChain->surfaceContext; } void VulkanDriver::createStreamFromTextureIdR(Handle sh, intptr_t externalTextureId, @@ -1174,6 +1171,12 @@ void VulkanDriver::commit(Handle sch) { .signalSemaphoreCount = 1u, .pSignalSemaphores = &surfaceContext.renderingFinished, }; + if (surfaceContext.headlessQueue) { + submitInfo.waitSemaphoreCount = 0; + submitInfo.pWaitSemaphores = nullptr; + submitInfo.signalSemaphoreCount = 0; + submitInfo.pSignalSemaphores = nullptr; + } auto& cmdfence = swapContext.commands.fence; std::unique_lock lock(cmdfence->mutex); @@ -1184,6 +1187,10 @@ void VulkanDriver::commit(Handle sch) { swapContext.invalid = true; cmdfence->condition.notify_all(); + if (surfaceContext.headlessQueue) { + return; + } + // Present the backbuffer. VulkanSurfaceContext& surface = handle_cast(mHandleMap, sch)->surfaceContext; VkPresentInfoKHR presentInfo { @@ -1288,8 +1295,172 @@ void VulkanDriver::stopCapture(int) { void VulkanDriver::readPixels(Handle src, uint32_t x, uint32_t y, uint32_t width, uint32_t height, - PixelBufferDescriptor&& p) { - scheduleDestroy(std::move(p)); + PixelBufferDescriptor&& pbd) { + // TODO: add support for all types listed in the Renderer docstring for readPixels. + assert(pbd.type == PixelBufferDescriptor::PixelDataType::UBYTE); + + const VkDevice device = mContext.device; + + // Create a host visible, linearly tiled image as a staging area. + + VkImageCreateInfo imageInfo { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .extent.width = width, + .extent.height = height, + .extent.depth = 1, + .arrayLayers = 1, + .mipLevels = 1, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_LINEAR, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT + }; + + VkImage stagingImage; + vkCreateImage(device, &imageInfo, VKALLOC, &stagingImage); + + VkMemoryRequirements memReqs; + VkDeviceMemory stagingMemory; + vkGetImageMemoryRequirements(device, stagingImage, &memReqs); + VkMemoryAllocateInfo allocInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memReqs.size, + .memoryTypeIndex = selectMemoryType(mContext, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) + }; + + vkAllocateMemory(device, &allocInfo, nullptr, &stagingMemory); + vkBindImageMemory(device, stagingImage, stagingMemory, 0); + + // TODO: Should we allow readPixels within beginFrame / endFrame? + + assert(mContext.currentCommands == nullptr); + acquireWorkCommandBuffer(mContext); + + // Transition the staging image layout. + + VulkanTexture::transitionImageLayout(mContext.work.cmdbuffer, stagingImage, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 0, 1, 1, + VK_IMAGE_ASPECT_COLOR_BIT); + + VkImageCopy imageCopyRegion = { + .srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .srcSubresource.layerCount = 1, + .srcOffset.x = (int32_t) x, + .srcOffset.y = (int32_t) y, + .dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .dstSubresource.layerCount = 1, + .extent.width = width, + .extent.height = height, + .extent.depth = 1 + }; + + // Transition the source image layout (which might be the swap chain) + + VulkanRenderTarget* srcTarget = handle_cast(mHandleMap, src); + VkImage srcImage = srcTarget->getColor(0).image; + VulkanTexture::transitionImageLayout(mContext.work.cmdbuffer, srcImage, + VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); + + // Perform the blit. + + vkCmdCopyImage(mContext.work.cmdbuffer, srcTarget->getColor(0).image, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, stagingImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &imageCopyRegion); + + // Restore the source image layout. + + VulkanTexture* srcTexture = srcTarget->getColor(0).texture; + if (srcTexture || mContext.currentSurface->presentQueue) { + const VkImageLayout present = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + VulkanTexture::transitionImageLayout(mContext.work.cmdbuffer, srcImage, + VK_IMAGE_LAYOUT_UNDEFINED, srcTexture ? getTextureLayout(srcTexture->usage) : present, + 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); + } else { + VulkanTexture::transitionImageLayout(mContext.work.cmdbuffer, srcImage, + VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, + 0, 1, 1, VK_IMAGE_ASPECT_COLOR_BIT); + } + + // Transition the staging image layout to GENERAL. + + VkImageMemoryBarrier barrier = { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = stagingImage, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.baseMipLevel = 0, + .subresourceRange.levelCount = 1, + .subresourceRange.baseArrayLayer = 0, + .subresourceRange.layerCount = 1 + }; + + vkCmdPipelineBarrier(mContext.work.cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + + flushWorkCommandBuffer(mContext); + + // Create a closure-friendly pointer that holds the rvalue reference. + + PixelBufferDescriptor* closure = new PixelBufferDescriptor(); + *closure = std::move(pbd); + + // Create a disposable to defer execution of the following code until after + // the work command buffer has completed. + + mDisposer.createDisposable(stagingImage, [=] () { + + VkImageSubresource subResource { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT }; + VkSubresourceLayout subResourceLayout; + vkGetImageSubresourceLayout(device, stagingImage, &subResource, &subResourceLayout); + + // Map image memory so we can start copying from it. + + const uint8_t* srcPixels; + vkMapMemory(device, stagingMemory, 0, VK_WHOLE_SIZE, 0, (void**) &srcPixels); + srcPixels += subResourceLayout.offset; + + uint8_t* dstPixels = (uint8_t*) closure->buffer; + const uint32_t dstStride = closure->stride ? closure->stride : width; + const int dstBytesPerRow = PixelBufferDescriptor::computeDataSize(closure->format, + closure->type, dstStride, 1, closure->alignment); + const int srcBytesPerRow = subResourceLayout.rowPitch; + + switch (closure->format) { + case PixelDataFormat::RGB: + case PixelDataFormat::RGB_INTEGER: + DataReshaper::reshapeImage(dstPixels, srcPixels, srcBytesPerRow, + dstBytesPerRow, height); + break; + case PixelDataFormat::RGBA: + case PixelDataFormat::RGBA_INTEGER: + DataReshaper::reshapeImage(dstPixels, srcPixels, srcBytesPerRow, + dstBytesPerRow, height); + break; + default: + utils::slog.e << "ReadPixels: invalid PixelDataFormat" << utils::io::endl; + break; + } + + vkUnmapMemory(device, stagingMemory); + vkFreeMemory(device, stagingMemory, nullptr); + vkDestroyImage(device, stagingImage, nullptr); + + scheduleDestroy(std::move(*closure)); + delete closure; + }); + + // Next we reduce the ref count of the image to zero, which schedules the above callback to be + // executed on the next beginFrame(), after the work command buffer is completed. + mDisposer.removeReference(stagingImage); } void VulkanDriver::readStreamPixels(Handle sh, uint32_t x, uint32_t y, uint32_t width, @@ -1297,13 +1468,10 @@ void VulkanDriver::readStreamPixels(Handle sh, uint32_t x, uint32_t y, scheduleDestroy(std::move(p)); } -void VulkanDriver::blit(TargetBufferFlags buffers, - Handle dst, backend::Viewport dstRect, - Handle src, backend::Viewport srcRect, - SamplerMagFilter filter) { - auto dstTarget = handle_cast(mHandleMap, dst); - auto srcTarget = handle_cast(mHandleMap, src); - +void VulkanDriver::blit(TargetBufferFlags buffers, Handle dst, Viewport dstRect, + Handle src, Viewport srcRect, SamplerMagFilter filter) { + VulkanRenderTarget* dstTarget = handle_cast(mHandleMap, dst); + VulkanRenderTarget* srcTarget = handle_cast(mHandleMap, src); const int targetIndex = 0; // TODO: support MRT in blit // In debug builds, verify that the two render targets have blittable formats. @@ -1371,9 +1539,7 @@ void VulkanDriver::blit(TargetBufferFlags buffers, VulkanTexture::transitionImageLayout(cmdbuffer, dstImage, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, dstLevel, 1, 1, aspect); - assert(srcTexture && "Blit source does not have an attached color texture."); - - if (srcTexture->samples > 1 && dstTexture && dstTexture->samples == 1) { + if (srcTexture && srcTexture->samples > 1 && dstTexture && dstTexture->samples == 1) { vkCmdResolveImage(cmdbuffer, srcImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dstImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, resolveRegions); } else { @@ -1382,8 +1548,13 @@ void VulkanDriver::blit(TargetBufferFlags buffers, filter == SamplerMagFilter::NEAREST ? VK_FILTER_NEAREST : VK_FILTER_LINEAR); } - VulkanTexture::transitionImageLayout(cmdbuffer, srcImage, VK_IMAGE_LAYOUT_UNDEFINED, - getTextureLayout(srcTexture->usage), srcLevel, 1, 1, aspect); + if (srcTexture) { + VulkanTexture::transitionImageLayout(cmdbuffer, srcImage, VK_IMAGE_LAYOUT_UNDEFINED, + getTextureLayout(srcTexture->usage), srcLevel, 1, 1, aspect); + } else if (!mContext.currentSurface->headlessQueue) { + VulkanTexture::transitionImageLayout(cmdbuffer, srcImage, VK_IMAGE_LAYOUT_UNDEFINED, + VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, srcLevel, 1, 1, aspect); + } // Determine the desired texture layout for the destination while ensuring that the default // render target is supported, which has no associated texture. @@ -1581,6 +1752,7 @@ void VulkanDriver::endTimerQuery(Handle tqh) { void VulkanDriver::refreshSwapChain() { VulkanSurfaceContext& surface = *mContext.currentSurface; + assert(!surface.headlessQueue && "Resizing headless swap chains is not supported."); backend::destroySwapChain(mContext, surface, mDisposer); createSwapChain(mContext, surface); diff --git a/filament/backend/src/vulkan/VulkanHandles.cpp b/filament/backend/src/vulkan/VulkanHandles.cpp index f9526324a88..ffd4bab86bd 100644 --- a/filament/backend/src/vulkan/VulkanHandles.cpp +++ b/filament/backend/src/vulkan/VulkanHandles.cpp @@ -191,6 +191,119 @@ VulkanRenderTarget::~VulkanRenderTarget() { } } +// Primary SwapChain constructor. (not headless) +VulkanSwapChain::VulkanSwapChain(VulkanContext& context, VkSurfaceKHR vksurface) { + surfaceContext.suboptimal = false; + surfaceContext.surface = vksurface; + getPresentationQueue(context, surfaceContext); + createSwapChain(context, surfaceContext); +} + +// Headless SwapChain constructor. (does not create a VkSwapChainKHR) +VulkanSwapChain::VulkanSwapChain(VulkanContext& context, uint32_t width, uint32_t height) { + surfaceContext.surface = nullptr; + getHeadlessQueue(context, surfaceContext); + + surfaceContext.surfaceFormat.format = VK_FORMAT_R8G8B8A8_UNORM; + surfaceContext.swapchain = VK_NULL_HANDLE; + + // Somewhat arbitrarily, headless rendering is double-buffered. + surfaceContext.swapContexts.resize(2); + + // Allocate a command buffer for each swap context, just like a real swap chain. + VkCommandBufferAllocateInfo allocateInfo = { + .sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, + .commandPool = context.commandPool, + .level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, + .commandBufferCount = (uint32_t) surfaceContext.swapContexts.size() + }; + std::vector cmdbufs(allocateInfo.commandBufferCount); + vkAllocateCommandBuffers(context.device, &allocateInfo, cmdbufs.data()); + for (uint32_t i = 0; i < allocateInfo.commandBufferCount; ++i) { + surfaceContext.swapContexts[i].commands.cmdbuffer = cmdbufs[i]; + } + + // Begin a new command buffer in order to transition image layouts via vkCmdPipelineBarrier. + VkCommandBuffer cmdbuffer = acquireWorkCommandBuffer(context); + + for (size_t i = 0; i < surfaceContext.swapContexts.size(); ++i) { + VkImage image; + VkImageCreateInfo iCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .imageType = VK_IMAGE_TYPE_2D, + .format = surfaceContext.surfaceFormat.format, + .extent.width = width, + .extent.height = height, + .extent.depth = 1, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, + }; + assert(iCreateInfo.extent.width > 0); + assert(iCreateInfo.extent.height > 0); + vkCreateImage(context.device, &iCreateInfo, VKALLOC, &image); + + VkMemoryRequirements memReqs = {}; + vkGetImageMemoryRequirements(context.device, image, &memReqs); + VkMemoryAllocateInfo allocInfo = { + .sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, + .allocationSize = memReqs.size, + .memoryTypeIndex = selectMemoryType(context, memReqs.memoryTypeBits, + VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) + }; + VkDeviceMemory imageMemory; + vkAllocateMemory(context.device, &allocInfo, VKALLOC, &imageMemory); + vkBindImageMemory(context.device, image, imageMemory, 0); + + surfaceContext.swapContexts[i].attachment = { + .format = surfaceContext.surfaceFormat.format, .image = image, + .view = {}, .memory = {}, .texture = {}, .layout = VK_IMAGE_LAYOUT_GENERAL + }; + VkImageViewCreateInfo ivCreateInfo = { + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = surfaceContext.surfaceFormat.format, + .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .subresourceRange.levelCount = 1, + .subresourceRange.layerCount = 1, + .image = image, + }; + vkCreateImageView(context.device, &ivCreateInfo, VKALLOC, + &surfaceContext.swapContexts[i].attachment.view); + + VkImageMemoryBarrier barrier { + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .levelCount = 1, + .layerCount = 1, + }, + }; + vkCmdPipelineBarrier(cmdbuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + } + + flushWorkCommandBuffer(context); + + surfaceContext.surfaceCapabilities.currentExtent.width = width; + surfaceContext.surfaceCapabilities.currentExtent.height = height; + + surfaceContext.clientSize.width = width; + surfaceContext.clientSize.height = height; + + surfaceContext.imageAvailable = VK_NULL_HANDLE; + surfaceContext.renderingFinished = VK_NULL_HANDLE; + + createFinalDepthBuffer(context, surfaceContext, context.finalDepthFormat); +} + void VulkanRenderTarget::transformClientRectToPlatform(VkRect2D* bounds) const { // For the backbuffer, there are corner cases where the platform's surface resolution does not // match what Filament expects, so we need to make an appropriate transformation (e.g. create a @@ -687,7 +800,16 @@ void VulkanTexture::transitionImageLayout(VkCommandBuffer cmd, VkImage image, sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT; break; + + // We support PRESENT as a target layout to allow blitting from the swap chain. + // See also makeSwapChainPresentable(). case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: + barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.dstAccessMask = 0; + sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT; + destinationStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; + break; + default: PANIC_POSTCONDITION("Unsupported layout transition."); } diff --git a/filament/backend/src/vulkan/VulkanHandles.h b/filament/backend/src/vulkan/VulkanHandles.h index 8253844dc07..966337fec12 100644 --- a/filament/backend/src/vulkan/VulkanHandles.h +++ b/filament/backend/src/vulkan/VulkanHandles.h @@ -72,6 +72,8 @@ struct VulkanRenderTarget : private HwRenderTarget { }; struct VulkanSwapChain : public HwSwapChain { + VulkanSwapChain(VulkanContext& context, VkSurfaceKHR vksurface); + VulkanSwapChain(VulkanContext& context, uint32_t width, uint32_t height); VulkanSurfaceContext surfaceContext; }; From d2ca1c79d22aaf3b7e41e5fad4fabced9200f268 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Tue, 6 Oct 2020 11:50:42 -0700 Subject: [PATCH 06/18] Fix Windows build by avoiding nested initializers. Note that designated initializers are technically a C++20 feature so we should not be using them. However they are really nice for Vulkan code. --- filament/backend/src/vulkan/VulkanDriver.cpp | 56 +++++++++++-------- filament/backend/src/vulkan/VulkanHandles.cpp | 18 +++--- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index 6a4762a9014..908f410ed18 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -1307,15 +1307,17 @@ void VulkanDriver::readPixels(Handle src, .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = VK_FORMAT_R8G8B8A8_UNORM, - .extent.width = width, - .extent.height = height, - .extent.depth = 1, - .arrayLayers = 1, + .extent = { + .width = width, + .height = height, + .depth = 1, + }, .mipLevels = 1, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_LINEAR, - .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, }; VkImage stagingImage; @@ -1346,15 +1348,23 @@ void VulkanDriver::readPixels(Handle src, VK_IMAGE_ASPECT_COLOR_BIT); VkImageCopy imageCopyRegion = { - .srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .srcSubresource.layerCount = 1, - .srcOffset.x = (int32_t) x, - .srcOffset.y = (int32_t) y, - .dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .dstSubresource.layerCount = 1, - .extent.width = width, - .extent.height = height, - .extent.depth = 1 + .srcSubresource = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .layerCount = 1, + }, + .srcOffset = { + .x = (int32_t) x, + .y = (int32_t) y, + }, + .dstSubresource = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .layerCount = 1, + }, + .extent = { + .width = width, + .height = height, + .depth = 1, + }, }; // Transition the source image layout (which might be the swap chain) @@ -1389,18 +1399,20 @@ void VulkanDriver::readPixels(Handle src, VkImageMemoryBarrier barrier = { .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, + .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, .newLayout = VK_IMAGE_LAYOUT_GENERAL, .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = stagingImage, - .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT, - .dstAccessMask = VK_ACCESS_MEMORY_READ_BIT, - .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .subresourceRange.baseMipLevel = 0, - .subresourceRange.levelCount = 1, - .subresourceRange.baseArrayLayer = 0, - .subresourceRange.layerCount = 1 + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + } }; vkCmdPipelineBarrier(mContext.work.cmdbuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, diff --git a/filament/backend/src/vulkan/VulkanHandles.cpp b/filament/backend/src/vulkan/VulkanHandles.cpp index ffd4bab86bd..0875ce60a51 100644 --- a/filament/backend/src/vulkan/VulkanHandles.cpp +++ b/filament/backend/src/vulkan/VulkanHandles.cpp @@ -232,9 +232,11 @@ VulkanSwapChain::VulkanSwapChain(VulkanContext& context, uint32_t width, uint32_ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = surfaceContext.surfaceFormat.format, - .extent.width = width, - .extent.height = height, - .extent.depth = 1, + .extent = { + .width = width, + .height = height, + .depth = 1, + }, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT, @@ -263,12 +265,14 @@ VulkanSwapChain::VulkanSwapChain(VulkanContext& context, uint32_t width, uint32_ }; VkImageViewCreateInfo ivCreateInfo = { .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .image = image, .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = surfaceContext.surfaceFormat.format, - .subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .subresourceRange.levelCount = 1, - .subresourceRange.layerCount = 1, - .image = image, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .levelCount = 1, + .layerCount = 1, + } }; vkCreateImageView(context.device, &ivCreateInfo, VKALLOC, &surfaceContext.swapContexts[i].attachment.view); From 1dd7b06e6fdcc8ea70d813aa9f3214d581ac7d6a Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Tue, 6 Oct 2020 15:27:28 -0600 Subject: [PATCH 07/18] Fix null pointer dereference in FIndirectLight (#3163) --- filament/src/IndirectLight.cpp | 4 ++-- filament/src/details/IndirectLight.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/filament/src/IndirectLight.cpp b/filament/src/IndirectLight.cpp index 8195f13745d..cdd701a7309 100644 --- a/filament/src/IndirectLight.cpp +++ b/filament/src/IndirectLight.cpp @@ -201,11 +201,11 @@ void FIndirectLight::terminate(FEngine& engine) { } backend::Handle FIndirectLight::getReflectionHwHandle() const noexcept { - return mReflectionsTexture->getHwHandle(); + return mReflectionsTexture ? mReflectionsTexture->getHwHandle() : backend::Handle {}; } backend::Handle FIndirectLight::getIrradianceHwHandle() const noexcept { - return mIrradianceTexture->getHwHandle(); + return mIrradianceTexture ? mIrradianceTexture->getHwHandle() : backend::Handle {}; } math::float3 FIndirectLight::getDirectionEstimate(math::float3 const* f) noexcept { diff --git a/filament/src/details/IndirectLight.h b/filament/src/details/IndirectLight.h index 9acf75f36fd..6fdd2e84f80 100644 --- a/filament/src/details/IndirectLight.h +++ b/filament/src/details/IndirectLight.h @@ -58,8 +58,8 @@ class FIndirectLight : public IndirectLight { static math::float4 getColorEstimate(const math::float3 sh[9], math::float3 direction) noexcept; private: - FTexture const* mReflectionsTexture; - FTexture const* mIrradianceTexture; + FTexture const* mReflectionsTexture = nullptr; + FTexture const* mIrradianceTexture = nullptr; std::array mIrradianceCoefs; float mIntensity = DEFAULT_INTENSITY; math::mat3f mRotation; From 3d121eb873fd138de10a458a65973d1b4af8a84c Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Tue, 6 Oct 2020 14:38:20 -0700 Subject: [PATCH 08/18] Vulkan ReadPixels: fix platforms that use BGRA. Fixes a color issue seen with SwiftShader in non-headless mode. --- filament/backend/src/DataReshaper.h | 10 ++++++++-- filament/backend/src/vulkan/VulkanDriver.cpp | 6 ++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/filament/backend/src/DataReshaper.h b/filament/backend/src/DataReshaper.h index 16fca083f97..20e53507bfa 100644 --- a/filament/backend/src/DataReshaper.h +++ b/filament/backend/src/DataReshaper.h @@ -51,15 +51,21 @@ class DataReshaper { template::max()> static void reshapeImage(uint8_t* dest, const uint8_t* src, size_t srcBytesPerRow, - size_t dstBytesPerRow, size_t height) { + size_t dstBytesPerRow, size_t height, bool swizzle03) { const size_t srcWordCount = (srcBytesPerRow / sizeof(componentType)) / srcChannelCount; const int minChannelCount = filament::math::min(srcChannelCount, dstChannelCount); + assert(minChannelCount <= 4); + int inds[4] = {0, 1, 2, 3}; + if (swizzle03) { + inds[0] = 2; + inds[2] = 0; + } for (size_t row = 0; row < height; ++row) { const componentType* in = (const componentType*) src; componentType* out = (componentType*) dest; for (size_t word = 0; word < srcWordCount; ++word) { for (size_t channel = 0; channel < minChannelCount; ++channel) { - out[channel] = in[channel]; + out[channel] = in[inds[channel]]; } for (size_t channel = srcChannelCount; channel < dstChannelCount; ++channel) { out[channel] = maxValue; diff --git a/filament/backend/src/vulkan/VulkanDriver.cpp b/filament/backend/src/vulkan/VulkanDriver.cpp index 908f410ed18..421c6a243ab 100644 --- a/filament/backend/src/vulkan/VulkanDriver.cpp +++ b/filament/backend/src/vulkan/VulkanDriver.cpp @@ -1445,17 +1445,19 @@ void VulkanDriver::readPixels(Handle src, const int dstBytesPerRow = PixelBufferDescriptor::computeDataSize(closure->format, closure->type, dstStride, 1, closure->alignment); const int srcBytesPerRow = subResourceLayout.rowPitch; + const VkFormat swapChainFormat = mContext.currentSurface->surfaceFormat.format; + const bool swizzle = !srcTexture && swapChainFormat == VK_FORMAT_B8G8R8A8_UNORM; switch (closure->format) { case PixelDataFormat::RGB: case PixelDataFormat::RGB_INTEGER: DataReshaper::reshapeImage(dstPixels, srcPixels, srcBytesPerRow, - dstBytesPerRow, height); + dstBytesPerRow, height, swizzle); break; case PixelDataFormat::RGBA: case PixelDataFormat::RGBA_INTEGER: DataReshaper::reshapeImage(dstPixels, srcPixels, srcBytesPerRow, - dstBytesPerRow, height); + dstBytesPerRow, height, swizzle); break; default: utils::slog.e << "ReadPixels: invalid PixelDataFormat" << utils::io::endl; From 52698aab1465bb45dfc04ee1096b0850279c955b Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 7 Oct 2020 12:19:02 -0600 Subject: [PATCH 09/18] Destroy dummyOneTextureArray in PostProcessManager (#3171) * Destroy dummyOneTextureArray in PostProcessManager Fixes #3166 * Update RELEASE_NOTES.md --- RELEASE_NOTES.md | 1 + filament/src/PostProcessManager.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 30fcae086ed..53f9a79492a 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -7,6 +7,7 @@ A new header is inserted each time a *tag* is created. - Added a new Live Wallpaper Android sample - `UiHelper` now supports managing a `SurfaceHolder`. +- Fix: an internal texture resource was never destroyed ## v1.9.4 diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index c6ed7576843..ce6007c3e8c 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -244,6 +244,7 @@ void PostProcessManager::init() noexcept { void PostProcessManager::terminate(DriverApi& driver) noexcept { FEngine& engine = mEngine; driver.destroyTexture(mDummyOneTexture); + driver.destroyTexture(mDummyOneTextureArray); driver.destroyTexture(mDummyZeroTexture); auto first = mMaterialRegistry.begin(); auto last = mMaterialRegistry.end(); From 7471237178ae4af1e13f54959ed1003910271531 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Wed, 7 Oct 2020 17:43:37 +0000 Subject: [PATCH 10/18] Linux fixes for headless SwiftShader. --- .../backend/src/vulkan/PlatformVkLinux.cpp | 19 +++++++++++-------- libs/bluevk/src/BlueVKLinuxAndroid.cpp | 4 ++++ libs/filameshio/tests/test_filamesh.cpp | 2 +- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/filament/backend/src/vulkan/PlatformVkLinux.cpp b/filament/backend/src/vulkan/PlatformVkLinux.cpp index 930455c216e..c1d8010b683 100644 --- a/filament/backend/src/vulkan/PlatformVkLinux.cpp +++ b/filament/backend/src/vulkan/PlatformVkLinux.cpp @@ -43,7 +43,7 @@ struct X11Functions { X11_OPEN_DISPLAY openDisplay; X11_CLOSE_DISPLAY closeDisplay; X11_GET_GEOMETRY getGeometry; - void* library; + void* library = nullptr; } g_x11; Driver* PlatformVkLinux::createDriver(void* const sharedContext) noexcept { @@ -51,22 +51,25 @@ Driver* PlatformVkLinux::createDriver(void* const sharedContext) noexcept { const char* requiredInstanceExtensions[] = { "VK_KHR_surface", "VK_KHR_xlib_surface", + "VK_KHR_get_physical_device_properties2", #if VK_ENABLE_VALIDATION "VK_EXT_debug_utils", #endif }; - g_x11.library = dlopen(LIBRARY_X11, RTLD_LOCAL | RTLD_NOW); - ASSERT_PRECONDITION(g_x11.library, "Unable to open X11 library."); - g_x11.openDisplay = (X11_OPEN_DISPLAY) dlsym(g_x11.library, "XOpenDisplay"); - g_x11.closeDisplay = (X11_CLOSE_DISPLAY) dlsym(g_x11.library, "XCloseDisplay"); - g_x11.getGeometry = (X11_GET_GEOMETRY) dlsym(g_x11.library, "XGetGeometry"); - mDisplay = g_x11.openDisplay(NULL); - ASSERT_PRECONDITION(mDisplay, "Unable to open X11 display."); return VulkanDriverFactory::create(this, requiredInstanceExtensions, sizeof(requiredInstanceExtensions) / sizeof(requiredInstanceExtensions[0])); } void* PlatformVkLinux::createVkSurfaceKHR(void* nativeWindow, void* instance) noexcept { + if (g_x11.library == nullptr) { + g_x11.library = dlopen(LIBRARY_X11, RTLD_LOCAL | RTLD_NOW); + ASSERT_PRECONDITION(g_x11.library, "Unable to open X11 library."); + g_x11.openDisplay = (X11_OPEN_DISPLAY) dlsym(g_x11.library, "XOpenDisplay"); + g_x11.closeDisplay = (X11_CLOSE_DISPLAY) dlsym(g_x11.library, "XCloseDisplay"); + g_x11.getGeometry = (X11_GET_GEOMETRY) dlsym(g_x11.library, "XGetGeometry"); + mDisplay = g_x11.openDisplay(NULL); + ASSERT_PRECONDITION(mDisplay, "Unable to open X11 display."); + } ASSERT_POSTCONDITION(vkCreateXlibSurfaceKHR, "Unable to load vkCreateXlibSurfaceKHR function."); VkSurfaceKHR surface = nullptr; VkXlibSurfaceCreateInfoKHR createInfo = {}; diff --git a/libs/bluevk/src/BlueVKLinuxAndroid.cpp b/libs/bluevk/src/BlueVKLinuxAndroid.cpp index 5c06ccdd997..bfa4d79ede7 100644 --- a/libs/bluevk/src/BlueVKLinuxAndroid.cpp +++ b/libs/bluevk/src/BlueVKLinuxAndroid.cpp @@ -30,7 +30,11 @@ static const char* VKLIBRARY_PATH = "libvulkan.so.1"; static void* module = nullptr; bool loadLibrary() { +#ifndef FILAMENT_VKLIBRARY_PATH module = dlopen(VKLIBRARY_PATH, RTLD_NOW | RTLD_LOCAL); +#else + module = dlopen(FILAMENT_VKLIBRARY_PATH, RTLD_NOW | RTLD_LOCAL); +#endif return module != nullptr; } diff --git a/libs/filameshio/tests/test_filamesh.cpp b/libs/filameshio/tests/test_filamesh.cpp index dbcd06ff3ef..57d3db26332 100644 --- a/libs/filameshio/tests/test_filamesh.cpp +++ b/libs/filameshio/tests/test_filamesh.cpp @@ -29,7 +29,7 @@ #include -#include +#include using namespace filament; using namespace filamesh; From bd625e42e986474343cb1e5d473fa59cddcd4944 Mon Sep 17 00:00:00 2001 From: Ben Doherty Date: Wed, 7 Oct 2020 15:36:45 -0600 Subject: [PATCH 11/18] Fix Vulkan crash when using shadow cascades (#3172) --- filament/backend/src/vulkan/VulkanHandles.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/filament/backend/src/vulkan/VulkanHandles.cpp b/filament/backend/src/vulkan/VulkanHandles.cpp index 0875ce60a51..94f31fad14c 100644 --- a/filament/backend/src/vulkan/VulkanHandles.cpp +++ b/filament/backend/src/vulkan/VulkanHandles.cpp @@ -499,6 +499,7 @@ VulkanTexture::VulkanTexture(VulkanContext& context, SamplerType target, uint8_t } if (target == SamplerType::SAMPLER_2D_ARRAY) { imageInfo.arrayLayers = depth; + imageInfo.extent.depth = 1; // NOTE: We do not use VK_IMAGE_CREATE_2D_ARRAY_COMPATIBLE_BIT here because: // // (a) MoltenVK does not support it, and @@ -600,8 +601,11 @@ VulkanTexture::VulkanTexture(VulkanContext& context, SamplerType target, uint8_t if (any(usage & (TextureUsage::COLOR_ATTACHMENT | TextureUsage::DEPTH_ATTACHMENT))) { auto transition = [=](VulkanCommandBuffer commands) { + // If this is a SAMPLER_2D_ARRAY texture, then the depth argument stores the number of + // texture layers. + const uint32_t layers = target == SamplerType::SAMPLER_2D_ARRAY ? depth : 1; VulkanTexture::transitionImageLayout(commands.cmdbuffer, textureImage, - VK_IMAGE_LAYOUT_UNDEFINED, getTextureLayout(usage), 0, 1, levels, mAspect); + VK_IMAGE_LAYOUT_UNDEFINED, getTextureLayout(usage), 0, layers, levels, mAspect); }; if (mContext.currentCommands) { transition(*mContext.currentCommands); From 5ebc6bce0409f0149b533e4ed0e83b7de92d5b9a Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Thu, 8 Oct 2020 10:56:06 -0700 Subject: [PATCH 12/18] Add schema for automation specs. This can help when authoring a spec using your favorite text editor. --- libs/viewer/schemas/README.md | 14 ++++++++++ libs/viewer/schemas/automation.json | 43 +++++++++++++++++++++++++++++ libs/viewer/tests/basic.json | 22 +++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 libs/viewer/schemas/README.md create mode 100644 libs/viewer/schemas/automation.json create mode 100644 libs/viewer/tests/basic.json diff --git a/libs/viewer/schemas/README.md b/libs/viewer/schemas/README.md new file mode 100644 index 00000000000..d8fbc61ec60 --- /dev/null +++ b/libs/viewer/schemas/README.md @@ -0,0 +1,14 @@ +The schema in this folder can be used to enable autocomplete and help features when authoring +automation specs in your favorite text editor. For example, for Visual Studio Code you can add the +following to `.vscode/settings`: + +``` +"json.schemas": [ + { + "fileMatch": [ + "libs/viewer/tests/basic.json" + ], + "url": "./libs/viewer/schemas/automation.json" + } +] +``` diff --git a/libs/viewer/schemas/automation.json b/libs/viewer/schemas/automation.json new file mode 100644 index 00000000000..9aa564e450a --- /dev/null +++ b/libs/viewer/schemas/automation.json @@ -0,0 +1,43 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://github.com/google/filament/automation.json", + "title": "Spec", + "description": "A specification that generates a set of Settings objects", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Prefix used for screenshots and exported settings" + }, + "base": { + "type": "object", + "description": "The base configuration to which permutations are applied", + "patternProperties": { + "[A-Z0-9_\\.]+": { "type": ["number", "boolean", "string" ] } + }, + "additionalProperties": false + }, + "permute": { + "type": "object", + "description": "Specifies a cross-product of property values", + "patternProperties": { + "[A-Z0-9_\\.]+": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "oneOf": [ + { "items": { "type": "number" } }, + { "items": { "type": "string" } }, + { "items": { "type": "boolean" } } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "minItems": 1 +} \ No newline at end of file diff --git a/libs/viewer/tests/basic.json b/libs/viewer/tests/basic.json new file mode 100644 index 00000000000..9ee8c0af37a --- /dev/null +++ b/libs/viewer/tests/basic.json @@ -0,0 +1,22 @@ +[ + { + "name": "view", + "base": { + "view.dof.focusDistance": 0.1, + "view.sampleCount": 1, + "view.taa.enabled": false + }, + "permute": { + "view.antiAliasing": [ "NONE", "FXAA" ], + "view.ssao.enabled": [ false, true ], + "view.bloom.enabled": [ false, true ], + "view.dof.enabled": [ false, true ] + } + }, + { + "name": "ppoff", + "base": { + "view.postProcessingEnabled": false + } + } +] \ No newline at end of file From b82dca4fac18458d0fbc97002ad8d5fc650619b4 Mon Sep 17 00:00:00 2001 From: Philip Rideout Date: Fri, 9 Oct 2020 10:25:37 -0700 Subject: [PATCH 13/18] JobSystem: work around hang on 2-CPU machines If hardware_concurrency() returned 2 while UTILS_HAS_HYPER_THREADING was enabled, `mThreadCount` was resulting in zero, so jobs (such as texture decoding) would simply never start. This problem was noticed with GitHub Actions. I tested this fix locally by replacing `hardware_concurrency()` with fixed values like 1 or 2, and verified that the hang went away. --- libs/utils/src/JobSystem.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/utils/src/JobSystem.cpp b/libs/utils/src/JobSystem.cpp index 878e6b4c2db..7883023ab54 100644 --- a/libs/utils/src/JobSystem.cpp +++ b/libs/utils/src/JobSystem.cpp @@ -119,8 +119,8 @@ JobSystem::JobSystem(const size_t userThreadCount, const size_t adoptableThreads // since we assumed HT, always round-up to an even number of cores (to play it safe) hwThreads = (hwThreads + 1) / 2; } - // make sure we have at least one h/w thread (could be an assert instead) - hwThreads = std::max(0, hwThreads); + // make sure we have at least one thread in the thread pool + hwThreads = std::max(2, hwThreads); // one of the thread will be the user thread threadPoolCount = hwThreads - 1; } From 40e10817dd4ff8811044ada4daf8650c01edafba Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Mon, 12 Oct 2020 11:52:49 -0600 Subject: [PATCH 14/18] Bump version to 1.9.4 --- README.md | 4 ++-- android/gradle.properties | 2 +- ios/CocoaPods/Filament.podspec | 2 +- web/filament-js/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f5d2c88d4ac..c8d4c3b18ea 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ repositories { } dependencies { - implementation 'com.google.android.filament:filament-android:1.9.3' + implementation 'com.google.android.filament:filament-android:1.9.4' } ``` @@ -63,7 +63,7 @@ A much smaller alternative to `filamat-android` that can only generate OpenGL sh iOS projects can use CocoaPods to install the latest release: ``` -pod 'Filament', '~> 1.9.3' +pod 'Filament', '~> 1.9.4' ``` ### Snapshots diff --git a/android/gradle.properties b/android/gradle.properties index fa4a9bf46ba..0bb0355b4d1 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.google.android.filament -VERSION_NAME=1.9.3 +VERSION_NAME=1.9.4 POM_DESCRIPTION=Real-time physically based rendering engine for Android. diff --git a/ios/CocoaPods/Filament.podspec b/ios/CocoaPods/Filament.podspec index 4439c961622..32eb26a0f4b 100644 --- a/ios/CocoaPods/Filament.podspec +++ b/ios/CocoaPods/Filament.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Filament" - spec.version = "1.9.3" + spec.version = "1.9.4" spec.license = { :type => "Apache 2.0", :file => "LICENSE" } spec.homepage = "https://google.github.io/filament" spec.authors = "Google LLC." diff --git a/web/filament-js/package.json b/web/filament-js/package.json index e7d9bf4179e..d5b79beec65 100644 --- a/web/filament-js/package.json +++ b/web/filament-js/package.json @@ -1,6 +1,6 @@ { "name": "filament", - "version": "1.9.3", + "version": "1.9.4", "description": "Real-time physically based rendering engine", "main": "filament.js", "module": "filament.js", From 03df97d0e3f2b8fae405118df2147d5d216e121c Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Mon, 12 Oct 2020 11:52:01 -0600 Subject: [PATCH 15/18] Update RELEASE_NOTES for 1.9.4 --- RELEASE_NOTES.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 53f9a79492a..e8a6c0f68d3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,7 +3,9 @@ This file contains one line summaries of commits that are worthy of mentioning in release notes. A new header is inserted each time a *tag* is created. -## Next release (v1.9.5) +## Next release (main branch) + +## v1.9.5 - Added a new Live Wallpaper Android sample - `UiHelper` now supports managing a `SurfaceHolder`. @@ -11,7 +13,12 @@ A new header is inserted each time a *tag* is created. ## v1.9.4 -- New `ShadowOptions` control to render Variance Shadow Maps (VSM) with MSAA (experimental). +- Add screen space cone tracing (SSCT) +- Improvements to VSM shadow quality +- New `ShadowOptions` control to render Variance Shadow Maps (VSM) with MSAA (experimental) +- Improvements and fixes to screen-space ambient occlusion +- gltf_viewer: add --headless option +- gltf_viewer: Add new automation UI and functionality ## v1.9.3 From 269d6367855122bcc4aa112fac8c3161c602c5fa Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Mon, 12 Oct 2020 12:03:29 -0600 Subject: [PATCH 16/18] Bump version to 1.9.5 --- README.md | 4 ++-- android/gradle.properties | 2 +- ios/CocoaPods/Filament.podspec | 2 +- web/filament-js/package.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c8d4c3b18ea..a5d46b834dd 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ repositories { } dependencies { - implementation 'com.google.android.filament:filament-android:1.9.4' + implementation 'com.google.android.filament:filament-android:1.9.5' } ``` @@ -63,7 +63,7 @@ A much smaller alternative to `filamat-android` that can only generate OpenGL sh iOS projects can use CocoaPods to install the latest release: ``` -pod 'Filament', '~> 1.9.4' +pod 'Filament', '~> 1.9.5' ``` ### Snapshots diff --git a/android/gradle.properties b/android/gradle.properties index 0bb0355b4d1..0ad0816b065 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ GROUP=com.google.android.filament -VERSION_NAME=1.9.4 +VERSION_NAME=1.9.5 POM_DESCRIPTION=Real-time physically based rendering engine for Android. diff --git a/ios/CocoaPods/Filament.podspec b/ios/CocoaPods/Filament.podspec index 32eb26a0f4b..f39ecd2f3fd 100644 --- a/ios/CocoaPods/Filament.podspec +++ b/ios/CocoaPods/Filament.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Filament" - spec.version = "1.9.4" + spec.version = "1.9.5" spec.license = { :type => "Apache 2.0", :file => "LICENSE" } spec.homepage = "https://google.github.io/filament" spec.authors = "Google LLC." diff --git a/web/filament-js/package.json b/web/filament-js/package.json index d5b79beec65..8e26fc510c5 100644 --- a/web/filament-js/package.json +++ b/web/filament-js/package.json @@ -1,6 +1,6 @@ { "name": "filament", - "version": "1.9.4", + "version": "1.9.5", "description": "Real-time physically based rendering engine", "main": "filament.js", "module": "filament.js", From f54a0a345299b25dad4a70b365659d0dc03cfab0 Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Tue, 13 Oct 2020 15:15:02 -0600 Subject: [PATCH 17/18] Fix CocoaPod version --- ios/CocoaPods/Filament.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/CocoaPods/Filament.podspec b/ios/CocoaPods/Filament.podspec index f39ecd2f3fd..f5d1d41b7da 100644 --- a/ios/CocoaPods/Filament.podspec +++ b/ios/CocoaPods/Filament.podspec @@ -6,7 +6,7 @@ Pod::Spec.new do |spec| spec.authors = "Google LLC." spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL." spec.platform = :ios, "11.0" - spec.source = { :http => "https://github.com/google/filament/releases/download/v1.9.3/filament-v1.9.3-ios.tgz" } + spec.source = { :http => "https://github.com/google/filament/releases/download/v1.9.5/filament-v1.9.5-ios.tgz" } # Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon. spec.pod_target_xcconfig = { From 8bccfc28637e1fde5a58f38321b0f26b600e4a9f Mon Sep 17 00:00:00 2001 From: Benjamin Doherty Date: Mon, 19 Oct 2020 11:49:05 -0600 Subject: [PATCH 18/18] Update RELEASE_NOTES for 1.9.5 --- RELEASE_NOTES.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index e8a6c0f68d3..a3d0efb2b2f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,8 +8,15 @@ A new header is inserted each time a *tag* is created. ## v1.9.5 - Added a new Live Wallpaper Android sample -- `UiHelper` now supports managing a `SurfaceHolder`. +- `UiHelper` now supports managing a `SurfaceHolder` - Fix: an internal texture resource was never destroyed +- Fix: hang on 2-CPU machines +- Fix: Vulkan crash when using shadow cascades +- Linux fixes for headless SwiftShader +- Fix null pointer dereference in `FIndirectLight` +- Fix Windows build by avoiding nested initializers +- Vulkan: support readPixels and headless swap chains +- VSM improvements ## v1.9.4