Remove test mode from VirtualCameraRenderThread.

Instead of rendering the input in the render thread of virtual camera
connect the test client with dedicated thread for input rendering
when creating a test camera.

Rendering input in test mode was slowing down the virtual camera render
thread, causing it to fail tests requiring high fps on some targets.

This cl also moves test pattern rendering to be done on GPU and cleans
up corresponding shader.

Bug: 338251124
Test: atest virtual_camera_tests
Test: manual with OpenCamera
Test: atest CtsVirtualDevicesCameraCtsTestCases
Change-Id: I4bcefd91701f2f9e195eb71c0189efbb674e58f8
diff --git a/services/camera/virtualcamera/Android.bp b/services/camera/virtualcamera/Android.bp
index 90530f6..fc186fb 100644
--- a/services/camera/virtualcamera/Android.bp
+++ b/services/camera/virtualcamera/Android.bp
@@ -50,7 +50,6 @@
         "util/JpegUtil.cc",
         "util/MetadataUtil.cc",
         "util/Util.cc",
-        "util/TestPatternHelper.cc",
         "util/EglDisplayContext.cc",
         "util/EglFramebuffer.cc",
         "util/EglProgram.cc",
@@ -72,6 +71,7 @@
         "VirtualCameraStream.cc",
         "VirtualCameraService.cc",
         "VirtualCameraSessionContext.cc",
+        "VirtualCameraTestInstance.cc",
         "VirtualCameraRenderThread.cc",
     ],
     defaults: [
diff --git a/services/camera/virtualcamera/VirtualCameraRenderThread.cc b/services/camera/virtualcamera/VirtualCameraRenderThread.cc
index f5cf092..a5ee922 100644
--- a/services/camera/virtualcamera/VirtualCameraRenderThread.cc
+++ b/services/camera/virtualcamera/VirtualCameraRenderThread.cc
@@ -51,7 +51,6 @@
 #include "util/EglFramebuffer.h"
 #include "util/JpegUtil.h"
 #include "util/MetadataUtil.h"
-#include "util/TestPatternHelper.h"
 #include "util/Util.h"
 #include "utils/Errors.h"
 
@@ -300,11 +299,10 @@
 VirtualCameraRenderThread::VirtualCameraRenderThread(
     VirtualCameraSessionContext& sessionContext,
     const Resolution inputSurfaceSize, const Resolution reportedSensorSize,
-    std::shared_ptr<ICameraDeviceCallback> cameraDeviceCallback, bool testMode)
+    std::shared_ptr<ICameraDeviceCallback> cameraDeviceCallback)
     : mCameraDeviceCallback(cameraDeviceCallback),
       mInputSurfaceSize(inputSurfaceSize),
       mReportedSensorSize(reportedSensorSize),
-      mTestMode(testMode),
       mSessionContext(sessionContext) {
 }
 
@@ -401,10 +399,6 @@
   mEglSurfaceTexture = std::make_unique<EglSurfaceTexture>(
       mInputSurfaceSize.width, mInputSurfaceSize.height);
 
-  sp<Surface> inputSurface = mEglSurfaceTexture->getSurface();
-  if (mTestMode) {
-    inputSurface->connect(NATIVE_WINDOW_API_CPU, false, nullptr);
-  }
   mInputSurfacePromise.set_value(mEglSurfaceTexture->getSurface());
 
   while (std::unique_ptr<ProcessCaptureRequestTask> task = dequeueTask()) {
@@ -422,12 +416,6 @@
 
 void VirtualCameraRenderThread::processCaptureRequest(
     const ProcessCaptureRequestTask& request) {
-  if (mTestMode) {
-    // In test mode let's just render something to the Surface ourselves.
-    renderTestPatternYCbCr420(mEglSurfaceTexture->getSurface(),
-                              request.getFrameNumber());
-  }
-
   std::chrono::nanoseconds timestamp =
       std::chrono::duration_cast<std::chrono::nanoseconds>(
           std::chrono::steady_clock::now().time_since_epoch());
diff --git a/services/camera/virtualcamera/VirtualCameraRenderThread.h b/services/camera/virtualcamera/VirtualCameraRenderThread.h
index dfb6f7b..ce8965f 100644
--- a/services/camera/virtualcamera/VirtualCameraRenderThread.h
+++ b/services/camera/virtualcamera/VirtualCameraRenderThread.h
@@ -111,8 +111,7 @@
       Resolution reportedSensorSize,
       std::shared_ptr<
           ::aidl::android::hardware::camera::device::ICameraDeviceCallback>
-          cameraDeviceCallback,
-      bool testMode = false);
+          cameraDeviceCallback);
 
   ~VirtualCameraRenderThread();
 
@@ -184,7 +183,6 @@
 
   const Resolution mInputSurfaceSize;
   const Resolution mReportedSensorSize;
-  const int mTestMode;
 
   VirtualCameraSessionContext& mSessionContext;
 
diff --git a/services/camera/virtualcamera/VirtualCameraService.cc b/services/camera/virtualcamera/VirtualCameraService.cc
index b5b07f0..b78f120 100644
--- a/services/camera/virtualcamera/VirtualCameraService.cc
+++ b/services/camera/virtualcamera/VirtualCameraService.cc
@@ -30,10 +30,12 @@
 
 #include "VirtualCameraDevice.h"
 #include "VirtualCameraProvider.h"
+#include "VirtualCameraTestInstance.h"
 #include "aidl/android/companion/virtualcamera/Format.h"
 #include "aidl/android/companion/virtualcamera/LensFacing.h"
 #include "aidl/android/companion/virtualcamera/VirtualCameraConfiguration.h"
 #include "android/binder_auto_utils.h"
+#include "android/binder_interface_utils.h"
 #include "android/binder_libbinder.h"
 #include "android/binder_status.h"
 #include "binder/Status.h"
@@ -64,6 +66,7 @@
 constexpr int kVgaWidth = 640;
 constexpr int kVgaHeight = 480;
 constexpr int kMaxFps = 60;
+constexpr int kTestCameraInputFps = 30;
 constexpr char kEnableTestCameraCmd[] = "enable_test_camera";
 constexpr char kDisableTestCameraCmd[] = "disable_test_camera";
 constexpr char kHelp[] = "help";
@@ -94,6 +97,13 @@
         Status::EX_ILLEGAL_ARGUMENT);
   }
 
+  if (configuration.virtualCameraCallback == nullptr) {
+    ALOGE("%s: Input configuration is missing virtual camera callback",
+          __func__);
+    return ndk::ScopedAStatus::fromServiceSpecificError(
+        Status::EX_ILLEGAL_ARGUMENT);
+  }
+
   for (const SupportedStreamConfiguration& config :
        configuration.supportedStreamConfigs) {
     if (!isFormatSupportedForInput(config.width, config.height,
@@ -418,9 +428,11 @@
   VirtualCameraConfiguration configuration;
   configuration.supportedStreamConfigs.push_back({.width = kVgaWidth,
                                                   .height = kVgaHeight,
-                                                  Format::YUV_420_888,
+                                                  Format::RGBA_8888,
                                                   .maxFps = kMaxFps});
   configuration.lensFacing = lensFacing.value_or(LensFacing::EXTERNAL);
+  configuration.virtualCameraCallback =
+      ndk::SharedRefBase::make<VirtualCameraTestInstance>(kTestCameraInputFps);
   registerCamera(mTestCameraToken, configuration, cameraId.value_or(sNextId++),
                  kDefaultDeviceId, &ret);
   if (ret) {
diff --git a/services/camera/virtualcamera/VirtualCameraSession.cc b/services/camera/virtualcamera/VirtualCameraSession.cc
index 28fa495..7f0adc3 100644
--- a/services/camera/virtualcamera/VirtualCameraSession.cc
+++ b/services/camera/virtualcamera/VirtualCameraSession.cc
@@ -66,7 +66,6 @@
 #include "util/EglProgram.h"
 #include "util/JpegUtil.h"
 #include "util/MetadataUtil.h"
-#include "util/TestPatternHelper.h"
 #include "util/Util.h"
 
 namespace android {
@@ -360,12 +359,9 @@
       return cameraStatus(Status::ILLEGAL_ARGUMENT);
     }
     if (mRenderThread == nullptr) {
-      // If there's no client callback, start camera in test mode.
-      const bool testMode = mVirtualCameraClientCallback == nullptr;
       mRenderThread = std::make_unique<VirtualCameraRenderThread>(
           mSessionContext, resolutionFromInputConfig(*inputConfig),
-          virtualCamera->getMaxInputResolution(), mCameraDeviceCallback,
-          testMode);
+          virtualCamera->getMaxInputResolution(), mCameraDeviceCallback);
       mRenderThread->start();
       inputSurface = mRenderThread->getInputSurface();
     }
diff --git a/services/camera/virtualcamera/VirtualCameraTestInstance.cc b/services/camera/virtualcamera/VirtualCameraTestInstance.cc
new file mode 100644
index 0000000..5e1d450
--- /dev/null
+++ b/services/camera/virtualcamera/VirtualCameraTestInstance.cc
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+// #define LOG_NDEBUG 0
+#define LOG_TAG "VirtualCameraTestInstance"
+
+#include "VirtualCameraTestInstance.h"
+
+#include <atomic>
+#include <chrono>
+#include <memory>
+#include <mutex>
+#include <ratio>
+#include <thread>
+
+#include "GLES/gl.h"
+#include "android/binder_auto_utils.h"
+#include "android/native_window.h"
+#include "log/log.h"
+#include "util/EglDisplayContext.h"
+#include "util/EglProgram.h"
+
+namespace android {
+namespace companion {
+namespace virtualcamera {
+
+using ::aidl::android::companion::virtualcamera::Format;
+using ::aidl::android::view::Surface;
+using ::ndk::ScopedAStatus;
+
+namespace {
+
+std::shared_ptr<ANativeWindow> nativeWindowFromSurface(const Surface& surface) {
+  ANativeWindow* nativeWindow = surface.get();
+  if (nativeWindow != nullptr) {
+    ANativeWindow_acquire(nativeWindow);
+  }
+  return std::shared_ptr<ANativeWindow>(nativeWindow, ANativeWindow_release);
+}
+
+std::chrono::nanoseconds getCurrentTimestamp() {
+  return std::chrono::duration_cast<std::chrono::nanoseconds>(
+      std::chrono::steady_clock::now().time_since_epoch());
+}
+
+}  // namespace
+
+TestPatternRenderer::TestPatternRenderer(
+    std::shared_ptr<ANativeWindow> nativeWindow, int fps)
+    : mFps(fps), mNativeWindow(nativeWindow) {
+}
+
+void TestPatternRenderer::start() {
+  std::lock_guard<std::mutex> lock(mLock);
+  if (mRunning.exchange(true, std::memory_order_relaxed)) {
+    ALOGW("Render thread already started.");
+    return;
+  }
+  mThread =
+      std::thread(&TestPatternRenderer::renderThreadLoop, this, mNativeWindow);
+}
+
+void TestPatternRenderer::stop() {
+  std::lock_guard<std::mutex> lock(mLock);
+  if (!mRunning.exchange(false, std::memory_order_relaxed)) {
+    ALOGW("Render thread already stopped.");
+    return;
+  }
+  mThread.detach();
+  mRunning = false;
+}
+
+void TestPatternRenderer::renderThreadLoop(
+    std::shared_ptr<ANativeWindow> nativeWindow) {
+  // Prevent destruction of this instance until the thread terminates.
+  std::shared_ptr<TestPatternRenderer> thiz = shared_from_this();
+
+  ALOGV("Starting test client render loop");
+
+  EglDisplayContext eglDisplayContext(nativeWindow);
+  EglTestPatternProgram testPatternProgram;
+
+  const std::chrono::nanoseconds frameDuration(
+      static_cast<uint64_t>(1e9 / mFps));
+
+  std::chrono::nanoseconds lastFrameTs(0);
+  int frameNumber = 0;
+  while (mRunning) {
+    // Wait for appropriate amount of time to meet configured FPS.
+    std::chrono::nanoseconds ts = getCurrentTimestamp();
+    std::chrono::nanoseconds currentDuration = ts - lastFrameTs;
+    if (currentDuration < frameDuration) {
+      std::this_thread::sleep_for(frameDuration - currentDuration);
+    }
+
+    // Render the test pattern and update timestamp.
+    testPatternProgram.draw(frameNumber++);
+    eglDisplayContext.swapBuffers();
+    lastFrameTs = getCurrentTimestamp();
+  }
+
+  ALOGV("Terminating test client render loop");
+}
+
+VirtualCameraTestInstance::VirtualCameraTestInstance(const int fps)
+    : mFps(fps) {
+}
+
+ScopedAStatus VirtualCameraTestInstance::onStreamConfigured(
+    const int32_t streamId, const Surface& surface, const int32_t width,
+    const int32_t height, const Format pixelFormat) {
+  ALOGV("%s: streamId %d, %dx%d pixFmt=%d", __func__, streamId, width, height,
+        pixelFormat);
+
+  std::lock_guard<std::mutex> lock(mLock);
+  mRenderer = std::make_shared<TestPatternRenderer>(
+      nativeWindowFromSurface(surface), mFps);
+  mRenderer->start();
+
+  return ScopedAStatus::ok();
+}
+
+ScopedAStatus VirtualCameraTestInstance::onProcessCaptureRequest(
+    const int32_t /*in_streamId*/, const int32_t /*in_frameId*/) {
+  return ScopedAStatus::ok();
+}
+
+ScopedAStatus VirtualCameraTestInstance::onStreamClosed(const int32_t streamId) {
+  ALOGV("%s: streamId %d", __func__, streamId);
+
+  std::lock_guard<std::mutex> lock(mLock);
+  if (mRenderer != nullptr) {
+    mRenderer->stop();
+    mRenderer.reset();
+  }
+  return ScopedAStatus::ok();
+}
+
+}  // namespace virtualcamera
+}  // namespace companion
+}  // namespace android
diff --git a/services/camera/virtualcamera/VirtualCameraTestInstance.h b/services/camera/virtualcamera/VirtualCameraTestInstance.h
new file mode 100644
index 0000000..43e33d5
--- /dev/null
+++ b/services/camera/virtualcamera/VirtualCameraTestInstance.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+#ifndef ANDROID_COMPANION_VIRTUALCAMERA_VIRTUALCAMERATESTINSTANCE_H
+#define ANDROID_COMPANION_VIRTUALCAMERA_VIRTUALCAMERATESTINSTANCE_H
+
+#include <atomic>
+#include <condition_variable>
+#include <memory>
+#include <thread>
+
+#include "aidl/android/companion/virtualcamera/BnVirtualCameraCallback.h"
+#include "android/native_window.h"
+#include "utils/Mutex.h"
+
+namespace android {
+namespace companion {
+namespace virtualcamera {
+
+// Wraps render loop run in a dedicated thread, rendering test pattern to
+// provided Surface (a.k.a. native window) at configured FPS.
+class TestPatternRenderer
+    : public std::enable_shared_from_this<TestPatternRenderer> {
+ public:
+  TestPatternRenderer(std::shared_ptr<ANativeWindow> nativeWindow, int fps);
+
+  // Start rendering.
+  void start() EXCLUDES(mLock);
+
+  // Stop rendering.
+  // Call returns immediatelly, render thread might take some time (1 frame)
+  // to finish rendering and terminate the thread.
+  void stop() EXCLUDES(mLock);
+
+ private:
+  // Render thread entry point.
+  void renderThreadLoop(std::shared_ptr<ANativeWindow> nativeWindow);
+
+  const int mFps;
+
+  std::shared_ptr<ANativeWindow> mNativeWindow;
+
+  std::mutex mLock;
+  std::atomic_bool mRunning;
+  std::thread mThread GUARDED_BY(mLock);
+};
+
+// VirtualCamera callback implementation for test camera.
+//
+// For every configure call, starts rendering of test pattern on provided surface.
+class VirtualCameraTestInstance
+    : public aidl::android::companion::virtualcamera::BnVirtualCameraCallback {
+ public:
+  explicit VirtualCameraTestInstance(int fps = 30);
+
+  ::ndk::ScopedAStatus onStreamConfigured(
+      int32_t streamId, const ::aidl::android::view::Surface& surface,
+      int32_t width, int32_t height,
+      ::aidl::android::companion::virtualcamera::Format pixelFormat) override
+      EXCLUDES(mLock);
+
+  ::ndk::ScopedAStatus onProcessCaptureRequest(int32_t in_streamId,
+                                               int32_t in_frameId) override;
+
+  ::ndk::ScopedAStatus onStreamClosed(int32_t streamId) override EXCLUDES(mLock);
+
+ private:
+  const int mFps;
+
+  std::mutex mLock;
+  std::shared_ptr<TestPatternRenderer> mRenderer GUARDED_BY(mLock);
+};
+
+}  // namespace virtualcamera
+}  // namespace companion
+}  // namespace android
+
+#endif  // ANDROID_COMPANION_VIRTUALCAMERA_VIRTUALCAMERATESTINSTANCE_H
diff --git a/services/camera/virtualcamera/tests/EglUtilTest.cc b/services/camera/virtualcamera/tests/EglUtilTest.cc
index 589e312..813be75 100644
--- a/services/camera/virtualcamera/tests/EglUtilTest.cc
+++ b/services/camera/virtualcamera/tests/EglUtilTest.cc
@@ -55,6 +55,11 @@
 };
 
 TEST_F(EglTest, EglTestPatternProgramSuccessfulInit) {
+  if (!isGlExtensionSupported(kGlExtYuvTarget)) {
+    GTEST_SKIP() << "Skipping test because of missing required GL extension "
+                 << kGlExtYuvTarget;
+  }
+
   EglTestPatternProgram eglTestPatternProgram;
 
   // Verify the shaders compiled and linked successfully.
diff --git a/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc b/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
index 5927b05..550227e 100644
--- a/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
+++ b/services/camera/virtualcamera/tests/VirtualCameraServiceTest.cc
@@ -15,6 +15,7 @@
  */
 
 #include <algorithm>
+#include <cstdint>
 #include <cstdio>
 #include <iterator>
 #include <memory>
@@ -75,6 +76,17 @@
 
 const VirtualCameraConfiguration kEmptyVirtualCameraConfiguration;
 
+class MockVirtualCameraCallback : public BnVirtualCameraCallback {
+ public:
+  MOCK_METHOD(ndk::ScopedAStatus, onStreamConfigured,
+              (int32_t, const ::aidl::android::view::Surface&, int, int,
+               ::aidl::android::companion::virtualcamera::Format pixelFormat),
+              (override));
+  MOCK_METHOD(ndk::ScopedAStatus, onProcessCaptureRequest, (int32_t, int32_t),
+              (override));
+  MOCK_METHOD(ndk::ScopedAStatus, onStreamClosed, (int32_t), (override));
+};
+
 VirtualCameraConfiguration createConfiguration(const int width, const int height,
                                                const Format format,
                                                const int maxFps) {
@@ -85,6 +97,8 @@
                                                   .maxFps = maxFps});
   configuration.sensorOrientation = kSensorOrientation;
   configuration.lensFacing = kLensFacing;
+  configuration.virtualCameraCallback =
+      ndk::SharedRefBase::make<MockVirtualCameraCallback>();
   return configuration;
 }
 
@@ -247,6 +261,24 @@
   EXPECT_THAT(getCameraIds(), IsEmpty());
 }
 
+TEST_F(VirtualCameraServiceTest,
+       ConfigurationWithoutVirtualCameraCallbackFails) {
+  sp<BBinder> token = sp<BBinder>::make();
+  ndk::SpAIBinder ndkToken(AIBinder_fromPlatformBinder(token));
+  bool aidlRet;
+
+  VirtualCameraConfiguration config =
+      createConfiguration(kVgaWidth, kVgaHeight, Format::RGBA_8888, kMaxFps);
+  config.virtualCameraCallback = nullptr;
+
+  ASSERT_FALSE(mCameraService
+                   ->registerCamera(ndkToken, config, kDefaultDeviceId, &aidlRet)
+                   .isOk());
+
+  EXPECT_FALSE(aidlRet);
+  EXPECT_THAT(getCameraIds(), IsEmpty());
+}
+
 TEST_F(VirtualCameraServiceTest, ConfigurationWithUnsupportedPixelFormatFails) {
   bool aidlRet;
 
diff --git a/services/camera/virtualcamera/util/EglDisplayContext.cc b/services/camera/virtualcamera/util/EglDisplayContext.cc
index 6d343a2..166ac75 100644
--- a/services/camera/virtualcamera/util/EglDisplayContext.cc
+++ b/services/camera/virtualcamera/util/EglDisplayContext.cc
@@ -30,8 +30,9 @@
 namespace companion {
 namespace virtualcamera {
 
-EglDisplayContext::EglDisplayContext()
+EglDisplayContext::EglDisplayContext(std::shared_ptr<ANativeWindow> nativeWindow)
     : mEglDisplay(EGL_NO_DISPLAY),
+      mEglSurface(EGL_NO_SURFACE),
       mEglContext(EGL_NO_CONTEXT),
       mEglConfig(nullptr) {
   EGLBoolean result;
@@ -52,8 +53,10 @@
 
   EGLint numConfigs = 0;
   EGLint configAttribs[] = {
-      EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE,
-      EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8,
+      EGL_SURFACE_TYPE,
+      nativeWindow == nullptr ? EGL_PBUFFER_BIT : EGL_WINDOW_BIT,
+      EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL_RED_SIZE, 8, EGL_GREEN_SIZE,
+      8, EGL_BLUE_SIZE, 8,
       // no alpha
       EGL_NONE};
 
@@ -72,6 +75,14 @@
     return;
   }
 
+  if (nativeWindow != nullptr) {
+    mEglSurface = eglCreateWindowSurface(mEglDisplay, mEglConfig,
+                                         nativeWindow.get(), NULL);
+    if (mEglSurface == EGL_NO_SURFACE) {
+      ALOGE("eglCreateWindowSurface error: %#x", eglGetError());
+    }
+  }
+
   if (!makeCurrent()) {
     ALOGE(
         "Failed to set newly initialized EGLContext and EGLDisplay connection "
@@ -82,6 +93,9 @@
 }
 
 EglDisplayContext::~EglDisplayContext() {
+  if (mEglSurface != EGL_NO_SURFACE) {
+    eglDestroySurface(mEglDisplay, mEglSurface);
+  }
   if (mEglDisplay != EGL_NO_DISPLAY) {
     eglTerminate(mEglDisplay);
   }
@@ -99,8 +113,14 @@
   return mEglContext != EGL_NO_CONTEXT && mEglDisplay != EGL_NO_DISPLAY;
 }
 
+void EglDisplayContext::swapBuffers() const {
+  if (mEglSurface != EGL_NO_SURFACE) {
+    eglSwapBuffers(mEglDisplay, mEglSurface);
+  }
+}
+
 bool EglDisplayContext::makeCurrent() {
-  if (!eglMakeCurrent(mEglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, mEglContext)) {
+  if (!eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
     ALOGE("eglMakeCurrent failed: %#x", eglGetError());
     return false;
   }
diff --git a/services/camera/virtualcamera/util/EglDisplayContext.h b/services/camera/virtualcamera/util/EglDisplayContext.h
index 402ca3c..6dc3080 100644
--- a/services/camera/virtualcamera/util/EglDisplayContext.h
+++ b/services/camera/virtualcamera/util/EglDisplayContext.h
@@ -17,7 +17,10 @@
 #ifndef ANDROID_COMPANION_VIRTUALCAMERA_EGLDISPLAYCONTEXT_H
 #define ANDROID_COMPANION_VIRTUALCAMERA_EGLDISPLAYCONTEXT_H
 
+#include <memory>
+
 #include "EGL/egl.h"
+#include "system/window.h"
 
 namespace android {
 namespace companion {
@@ -30,7 +33,7 @@
 // out of scope.
 class EglDisplayContext {
  public:
-  EglDisplayContext();
+  EglDisplayContext(std::shared_ptr<ANativeWindow> nativeWindow = nullptr);
   ~EglDisplayContext();
 
   // Sets EGLDisplay & EGLContext for current thread.
@@ -44,8 +47,13 @@
   // EGLDisplay & EGLContext.
   bool isInitialized() const;
 
+  void swapBuffers() const;
+
  private:
+  std::shared_ptr<ANativeWindow> mNativeWindow;
+
   EGLDisplay mEglDisplay;
+  EGLSurface mEglSurface;
   EGLContext mEglContext;
   EGLConfig mEglConfig;
 };
diff --git a/services/camera/virtualcamera/util/EglProgram.cc b/services/camera/virtualcamera/util/EglProgram.cc
index 7554a67..61f5005 100644
--- a/services/camera/virtualcamera/util/EglProgram.cc
+++ b/services/camera/virtualcamera/util/EglProgram.cc
@@ -35,19 +35,28 @@
 
 constexpr char kGlExtYuvTarget[] = "GL_EXT_YUV_target";
 
-constexpr char kIdentityVertexShader[] = R"(
-    attribute vec4 vPosition;
+constexpr char kJuliaFractalVertexShader[] = R"(#version 300 es
+    in vec4 aPosition;
+    in vec2 aTextureCoord;
+    out vec2 vFractalCoord;
+    out vec2 vUVCoord;
     void main() {
-      gl_Position = vPosition;
+      gl_Position = aPosition;
+      vUVCoord = aTextureCoord;
+      vFractalCoord = vec2(aTextureCoord.x - 0.5, aTextureCoord.y - 0.5) * 4.0;
     })";
 
-constexpr char kJuliaFractalFragmentShader[] = R"(
+constexpr char kJuliaFractalFragmentShader[] = R"(#version 300 es
+    #extension GL_EXT_YUV_target : require
     precision mediump float;
-    uniform vec2 uResolution;
-    uniform vec2 uC;
-    uniform vec2 uUV;
+
     const float kIter = 64.0;
 
+    in vec2 vFractalCoord;
+    in vec2 vUVCoord;
+    out vec4 fragColor;
+    uniform vec2 uC;
+
     vec2 imSq(vec2 n){
       return vec2(pow(n.x,2.0)-pow(n.y,2.0), 2.0*n.x*n.y);
     }
@@ -62,9 +71,8 @@
     }
 
     void main() {
-      vec2 uv = vec2(gl_FragCoord.x / uResolution.x - 0.5, gl_FragCoord.y / uResolution.y - 0.5);
-      float juliaVal = julia(uv * 4.0, uC);
-      gl_FragColor = vec4( juliaVal,uUV.x,uUV.y,0.0);
+      float juliaVal = julia(vFractalCoord, uC);
+      fragColor = vec4(yuv_2_rgb(vec3(juliaVal, vUVCoord.x, vUVCoord.y), itu_601_full_range), 0.0);
     })";
 
 constexpr char kExternalTextureVertexShader[] = R"(#version 300 es
@@ -200,17 +208,28 @@
 }
 
 EglTestPatternProgram::EglTestPatternProgram() {
-  if (initialize(kIdentityVertexShader, kJuliaFractalFragmentShader)) {
+  if (initialize(kJuliaFractalVertexShader, kJuliaFractalFragmentShader)) {
     ALOGV("Successfully initialized EGL shaders for test pattern program.");
   } else {
     ALOGE("Test pattern EGL shader program initialization failed.");
   }
+
+  mCHandle = glGetUniformLocation(mProgram, "uC");
+  mPositionHandle = glGetAttribLocation(mProgram, "aPosition");
+  mTextureCoordHandle = glGetAttribLocation(mProgram, "aTextureCoord");
+
+  // Pass vertex array to draw.
+  glEnableVertexAttribArray(mPositionHandle);
+  // Prepare the triangle coordinate data.
+  glVertexAttribPointer(mPositionHandle, kCoordsPerVertex, GL_FLOAT, false,
+                        kSquareCoords.size(), kSquareCoords.data());
+
+  glEnableVertexAttribArray(mTextureCoordHandle);
+  glVertexAttribPointer(mTextureCoordHandle, 2, GL_FLOAT, false,
+                        kTextureCoords.size(), kTextureCoords.data());
 }
 
-bool EglTestPatternProgram::draw(int width, int height, int frameNumber) {
-  glViewport(0, 0, static_cast<GLsizei>(width), static_cast<GLsizei>(height));
-  checkEglError("glViewport");
-
+bool EglTestPatternProgram::draw(int frameNumber) {
   // Load compiled shader.
   glUseProgram(mProgram);
   checkEglError("glUseProgram");
@@ -219,28 +238,8 @@
   float time = float(frameNumber) / 120.0f;
   const std::complex<float> c(std::sin(time) * 0.78f, std::cos(time) * 0.78f);
 
-  // Pass uniform values to the shader.
-  int resolutionHandle = glGetUniformLocation(mProgram, "uResolution");
-  checkEglError("glGetUniformLocation -> uResolution");
-  glUniform2f(resolutionHandle, static_cast<float>(width),
-              static_cast<float>(height));
-  checkEglError("glUniform2f -> uResolution");
-
   // Pass "C" constant value determining the Julia set to the shader.
-  int cHandle = glGetUniformLocation(mProgram, "uC");
-  glUniform2f(cHandle, c.imag(), c.real());
-
-  // Pass chroma value to the shader.
-  int uvHandle = glGetUniformLocation(mProgram, "uUV");
-  glUniform2f(uvHandle, (c.imag() + 1.f) / 2.f, (c.real() + 1.f) / 2.f);
-
-  // Pass vertex array to draw.
-  int positionHandle = glGetAttribLocation(mProgram, "vPosition");
-  glEnableVertexAttribArray(positionHandle);
-
-  // Prepare the triangle coordinate data.
-  glVertexAttribPointer(positionHandle, kCoordsPerVertex, GL_FLOAT, false,
-                        kSquareCoords.size(), kSquareCoords.data());
+  glUniform2f(mCHandle, c.imag(), c.real());
 
   // Draw triangle strip forming a square filling the viewport.
   glDrawElements(GL_TRIANGLES, kDrawOrder.size(), GL_UNSIGNED_BYTE,
diff --git a/services/camera/virtualcamera/util/EglProgram.h b/services/camera/virtualcamera/util/EglProgram.h
index c695cbb..d09f11e 100644
--- a/services/camera/virtualcamera/util/EglProgram.h
+++ b/services/camera/virtualcamera/util/EglProgram.h
@@ -46,7 +46,12 @@
  public:
   EglTestPatternProgram();
 
-  bool draw(int width, int height, int frameNumber);
+  bool draw(int frameNumber);
+
+ private:
+  int mPositionHandle = -1;
+  int mTextureCoordHandle = -1;
+  int mCHandle = -1;
 };
 
 // Shader program to  draw texture.
diff --git a/services/camera/virtualcamera/util/TestPatternHelper.cc b/services/camera/virtualcamera/util/TestPatternHelper.cc
deleted file mode 100644
index 274996a..0000000
--- a/services/camera/virtualcamera/util/TestPatternHelper.cc
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-// #define LOG_NDEBUG 0
-
-#define LOG_TAG "TestPatternHelper"
-
-#include "TestPatternHelper.h"
-
-#include <complex>
-#include <cstdint>
-
-#include "log/log.h"
-#include "nativebase/nativebase.h"
-#include "system/graphics.h"
-#include "ui/GraphicBuffer.h"
-#include "utils/Errors.h"
-
-namespace android {
-namespace companion {
-namespace virtualcamera {
-
-namespace {
-
-using namespace std::chrono_literals;
-
-static constexpr std::chrono::milliseconds kAcquireFenceTimeout = 500ms;
-
-uint8_t julia(const std::complex<float> n, const std::complex<float> c) {
-  std::complex<float> z = n;
-  for (int i = 0; i < 64; i++) {
-    z = z * z + c;
-    if (std::abs(z) > 2.0) return i * 4;
-  }
-  return 0xff;
-}
-
-uint8_t pixelToFractal(const int x, const int y, const int width,
-                       const int height, const std::complex<float> c) {
-  std::complex<float> n(float(x) / float(width) - 0.5,
-                        float(y) / float(height) - 0.5);
-  return julia(n * 5.f, c);
-}
-
-void renderTestPatternYcbCr420(const android_ycbcr& ycbr, const int width,
-                               const int height, const int frameNumber) {
-  float time = float(frameNumber) / 120.0f;
-  const std::complex<float> c(std::sin(time), std::cos(time));
-
-  uint8_t* y = reinterpret_cast<uint8_t*>(ycbr.y);
-  uint8_t* cb = reinterpret_cast<uint8_t*>(ycbr.cb);
-  uint8_t* cr = reinterpret_cast<uint8_t*>(ycbr.cr);
-
-  for (int row = 0; row < height; row++) {
-    for (int col = 0; col < width; col++) {
-      y[row * ycbr.ystride + col] =
-          pixelToFractal(col, row, width, height, c * 0.78f);
-    }
-  }
-
-  int cWidth = width / 2;
-  int cHeight = height / 2;
-  for (int row = 0; row < cHeight; row++) {
-    for (int col = 0; col < cWidth; col++) {
-      cb[row * ycbr.cstride + col * ycbr.chroma_step] =
-          static_cast<uint8_t>((float(col) / float(cWidth)) * 255.f);
-      cr[row * ycbr.cstride + col * ycbr.chroma_step] =
-          static_cast<uint8_t>((float(row) / float(cHeight)) * 255.f);
-    }
-  }
-}
-
-}  // namespace
-
-void renderTestPatternYCbCr420(sp<Surface> surface, int frameNumber) {
-  if (surface == nullptr) {
-    ALOGE("%s: null surface, skipping render", __func__);
-    return;
-  }
-
-  ANativeWindowBuffer* buffer;
-  int fenceFd;
-  int ret = ANativeWindow_dequeueBuffer(surface.get(), &buffer, &fenceFd);
-  if (ret != NO_ERROR) {
-    ALOGE(
-        "%s: Error while deuqueing buffer from surface, "
-        "ANativeWindow_dequeueBuffer returned %d",
-        __func__, ret);
-    return;
-  }
-
-  if (buffer == nullptr) {
-    ALOGE("%s: ANativeWindowBuffer is null after dequeing", __func__);
-    return;
-  }
-
-  sp<Fence> fence = sp<Fence>::make(fenceFd);
-  if (fence->isValid()) {
-    ret = fence->wait(kAcquireFenceTimeout.count());
-    if (ret != NO_ERROR) {
-      ALOGE("%s: Timeout while waiting for the fence to clear", __func__);
-      ANativeWindow_queueBuffer(surface.get(), buffer, fence->dup());
-      return;
-    }
-  }
-
-  sp<GraphicBuffer> gBuffer = GraphicBuffer::from(buffer);
-  android_ycbcr ycbr;
-
-  ret = gBuffer->lockAsyncYCbCr(GraphicBuffer::USAGE_SW_WRITE_OFTEN, &ycbr,
-                                fence->dup());
-  if (ret != NO_ERROR) {
-    ALOGE("%s: Failed to lock buffer retrieved from surface, ret %d", __func__,
-          ret);
-    return;
-  }
-
-  renderTestPatternYcbCr420(ycbr, gBuffer->getWidth(), gBuffer->getHeight(),
-                            frameNumber);
-
-  ret = gBuffer->unlock();
-  if (ret != NO_ERROR) {
-    ALOGE("%s: Failed to unlock buffer, ret %d", __func__, ret);
-    return;
-  }
-
-  ret = ANativeWindow_queueBuffer(surface.get(), buffer, /*fenceFd=*/-1);
-  if (ret != NO_ERROR) {
-    ALOGE(
-        "%s: Error while queing buffer to surface, ANativeWindow_queueBuffer "
-        "returned %d",
-        __func__, ret);
-    return;
-  }
-}
-
-}  // namespace virtualcamera
-}  // namespace companion
-}  // namespace android
diff --git a/services/camera/virtualcamera/util/TestPatternHelper.h b/services/camera/virtualcamera/util/TestPatternHelper.h
deleted file mode 100644
index f842b29..0000000
--- a/services/camera/virtualcamera/util/TestPatternHelper.h
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2023 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.
- */
-
-#ifndef ANDROID_COMPANION_VIRTUALCAMERA_TESTPATTERNHELPER_H
-#define ANDROID_COMPANION_VIRTUALCAMERA_TESTPATTERNHELPER_H
-
-#include "gui/Surface.h"
-
-namespace android {
-namespace companion {
-namespace virtualcamera {
-
-// Helper function for rendering test pattern into Surface.
-void renderTestPatternYCbCr420(sp<Surface> surface, int frameNumber);
-
-}  // namespace virtualcamera
-}  // namespace companion
-}  // namespace android
-
-#endif  // ANDROID_COMPANION_VIRTUALCAMERA_TESTPATTERNHELPER_H