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/RELEASE_NOTES.md b/RELEASE_NOTES.md index b28903423cb..a3d0efb2b2f 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -3,7 +3,20 @@ 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` +- 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 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/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 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/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 00000000000..f2772f51f14 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/drawable/wallpaper_thumb.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000000..6d5e5d094cb --- /dev/null +++ b/android/samples/sample-live-wallpaper/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ 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 00000000000..a2f5908281d Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000000..1b523998081 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..ff10afd6e18 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000000..115a4c768a2 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..dcd3cd80833 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 00000000000..459ca609d3a Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ 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 00000000000..8ca12fe024b Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 00000000000..8e19b410a1b Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000000..b824ebdd48d Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 00000000000..4c19a13c239 Binary files /dev/null and b/android/samples/sample-live-wallpaper/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ 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 00000000000..74ad33f1aeb Binary files /dev/null and b/docs/images/samples/example_live_wallpaper.jpg differ diff --git a/filament/backend/src/DataReshaper.h b/filament/backend/src/DataReshaper.h index e62289d6db1..20e53507bfa 100644 --- a/filament/backend/src/DataReshaper.h +++ b/filament/backend/src/DataReshaper.h @@ -19,29 +19,62 @@ #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, 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[inds[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/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/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..421c6a243ab 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,186 @@ 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, + .height = height, + .depth = 1, + }, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_LINEAR, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + + 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, + .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) + + 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, + .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, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .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; + 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, swizzle); + break; + case PixelDataFormat::RGBA: + case PixelDataFormat::RGBA_INTEGER: + DataReshaper::reshapeImage(dstPixels, srcPixels, srcBytesPerRow, + dstBytesPerRow, height, swizzle); + 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 +1482,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 +1553,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 +1562,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 +1766,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..94f31fad14c 100644 --- a/filament/backend/src/vulkan/VulkanHandles.cpp +++ b/filament/backend/src/vulkan/VulkanHandles.cpp @@ -191,6 +191,123 @@ 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, + .height = height, + .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, + .image = image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = surfaceContext.surfaceFormat.format, + .subresourceRange = { + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .levelCount = 1, + .layerCount = 1, + } + }; + 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 @@ -382,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 @@ -483,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); @@ -687,7 +808,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; }; 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/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/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(); 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/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; 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/ios/CocoaPods/Filament.podspec b/ios/CocoaPods/Filament.podspec index fe2ebb25865..f5d1d41b7da 100644 --- a/ios/CocoaPods/Filament.podspec +++ b/ios/CocoaPods/Filament.podspec @@ -1,12 +1,12 @@ 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." 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.4/filament-v1.9.4-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 = { 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; 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; } 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 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; 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",