diff --git a/tools/sk_app/MetalWindowContext.h b/tools/sk_app/MetalWindowContext.h
index 106d366415..08dc19b5c8 100644
--- a/tools/sk_app/MetalWindowContext.h
+++ b/tools/sk_app/MetalWindowContext.h
@@ -14,13 +14,18 @@
 
 #include "tools/sk_app/WindowContext.h"
 
+#ifdef __OBJC__
 #import <Metal/Metal.h>
 #import <QuartzCore/CAMetalLayer.h>
+#endif
 
 namespace sk_app {
 
+#ifdef __OBJC__
 class MetalWindowContext : public WindowContext {
 public:
+    static GrDirectContext* getSharedGrDirectContext() { return fGlobalShared ? fGlobalShared->fContext.get() : nullptr; }
+
     sk_sp<SkSurface> getBackbufferSurface() override;
 
     bool isValid() override { return fValid; }
@@ -46,16 +51,34 @@ protected:
     void destroyContext();
     virtual void onDestroyContext() = 0;
 
+    static void checkDestroyShared();
+
     bool                        fValid;
+
+    // We need to use just one GrDirectContext, so share all the relevant data.
+    struct Shared : public SkRefCnt
+    {
     sk_cfp<id<MTLDevice>>       fDevice;
     sk_cfp<id<MTLCommandQueue>> fQueue;
-    CAMetalLayer*               fMetalLayer;
-    GrMTLHandle                 fDrawableHandle;
 #if GR_METAL_SDK_VERSION >= 230
     // wrapping this in sk_cfp throws up an availability warning, so we'll track lifetime manually
     id<MTLBinaryArchive>        fPipelineArchive SK_API_AVAILABLE(macos(11.0), ios(14.0));
 #endif
+
+    sk_sp<GrDirectContext> fContext;
+    };
+
+    sk_sp<Shared> fShared;
+
+    static sk_sp<Shared> fGlobalShared;
+
+    CAMetalLayer*               fMetalLayer;
+    GrMTLHandle                 fDrawableHandle;
 };
+#endif // __OBJC__
+
+// Access function when header is used from C++ code that wouldn't handle ObjC++ headers.
+extern "C" SK_API GrDirectContext* getMetalSharedGrDirectContext();
 
 }   // namespace sk_app
 
diff --git a/tools/sk_app/MetalWindowContext.mm b/tools/sk_app/MetalWindowContext.mm
index d972e321a6..9f576944b7 100644
--- a/tools/sk_app/MetalWindowContext.mm
+++ b/tools/sk_app/MetalWindowContext.mm
@@ -37,24 +37,30 @@ NSURL* MetalWindowContext::CacheURL() {
 }
 
 void MetalWindowContext::initializeContext() {
+    fShared = fGlobalShared;
+    if( !fShared )
+    {
+    // TODO do we need a mutex?
+
+    fGlobalShared = sk_make_sp<Shared>();
+    Shared* d = fGlobalShared.get(); // shorter variable name
+
     SkASSERT(!fContext);
 
-    fDevice.reset(MTLCreateSystemDefaultDevice());
-    fQueue.reset([*fDevice newCommandQueue]);
+    d->fDevice.reset(MTLCreateSystemDefaultDevice());
+    d->fQueue.reset([*d->fDevice newCommandQueue]);
 
     if (fDisplayParams.fMSAASampleCount > 1) {
         if (@available(macOS 10.11, iOS 9.0, *)) {
-            if (![*fDevice supportsTextureSampleCount:fDisplayParams.fMSAASampleCount]) {
+            if (![*d->fDevice supportsTextureSampleCount:fDisplayParams.fMSAASampleCount]) {
+                fGlobalShared.reset();
                 return;
             }
         } else {
+            fGlobalShared.reset();
             return;
         }
     }
-    fSampleCount = fDisplayParams.fMSAASampleCount;
-    fStencilBits = 8;
-
-    fValid = this->onInitializeContext();
 
 #if GR_METAL_SDK_VERSION >= 230
     if (fDisplayParams.fEnableBinaryArchive) {
@@ -62,11 +68,11 @@ void MetalWindowContext::initializeContext() {
             sk_cfp<MTLBinaryArchiveDescriptor*> desc([MTLBinaryArchiveDescriptor new]);
             (*desc).url = CacheURL(); // try to load
             NSError* error;
-            fPipelineArchive = [*fDevice newBinaryArchiveWithDescriptor:*desc error:&error];
-            if (!fPipelineArchive) {
+            d->fPipelineArchive = [*d->fDevice newBinaryArchiveWithDescriptor:*desc error:&error];
+            if (!d->fPipelineArchive) {
                 (*desc).url = nil; // create new
-                fPipelineArchive = [*fDevice newBinaryArchiveWithDescriptor:*desc error:&error];
-                if (!fPipelineArchive) {
+                d->fPipelineArchive = [*d->fDevice newBinaryArchiveWithDescriptor:*desc error:&error];
+                if (!d->fPipelineArchive) {
                     SkDebugf("Error creating MTLBinaryArchive:\n%s\n",
                              error.debugDescription.UTF8String);
                 }
@@ -74,46 +80,75 @@ void MetalWindowContext::initializeContext() {
         }
     } else {
         if (@available(macOS 11.0, iOS 14.0, *)) {
-            fPipelineArchive = nil;
+            d->fPipelineArchive = nil;
         }
     }
 #endif
 
     GrMtlBackendContext backendContext = {};
-    backendContext.fDevice.retain((GrMTLHandle)fDevice.get());
-    backendContext.fQueue.retain((GrMTLHandle)fQueue.get());
+    backendContext.fDevice.retain((GrMTLHandle)d->fDevice.get());
+    backendContext.fQueue.retain((GrMTLHandle)d->fQueue.get());
 #if GR_METAL_SDK_VERSION >= 230
     if (@available(macOS 11.0, iOS 14.0, *)) {
-        backendContext.fBinaryArchive.retain((__bridge GrMTLHandle)fPipelineArchive);
+        backendContext.fBinaryArchive.retain((__bridge GrMTLHandle)d->fPipelineArchive);
     }
 #endif
-    fContext = GrDirectContext::MakeMetal(backendContext, fDisplayParams.fGrContextOptions);
-    if (!fContext && fDisplayParams.fMSAASampleCount > 1) {
+    d->fContext = GrDirectContext::MakeMetal(backendContext, fDisplayParams.fGrContextOptions);
+    if (!d->fContext && fDisplayParams.fMSAASampleCount > 1) {
         fDisplayParams.fMSAASampleCount /= 2;
+        fGlobalShared.reset();
         this->initializeContext();
         return;
     }
+
+    fShared = fGlobalShared;
+    } // if( !fShared )
+
+    fContext = fShared->fContext;
+
+    fSampleCount = fDisplayParams.fMSAASampleCount;
+    fStencilBits = 8;
+
+    fValid = this->onInitializeContext();
 }
 
 void MetalWindowContext::destroyContext() {
-    if (fContext) {
-        // in case we have outstanding refs to this (lua?)
-        fContext->abandonContext();
-        fContext.reset();
-    }
-
     this->onDestroyContext();
 
     fMetalLayer = nil;
     fValid = false;
 
+    fContext.reset();
+    fShared.reset();
+
+    checkDestroyShared();
+}
+
+void MetalWindowContext::checkDestroyShared()
+{
+    if(!fGlobalShared || !fGlobalShared->unique()) // TODO mutex?
+        return;
+#ifndef SK_TRACE_VK_RESOURCES
+    if(!fGlobalShared->fContext->unique())
+        return;
+#endif
+    SkASSERT(fGlobalShared->fContext->unique());
+
+    if (fGlobalShared->fContext) {
+        // in case we have outstanding refs to this (lua?)
+        fGlobalShared->fContext->abandonContext();
+        fGlobalShared->fContext.reset();
+    }
+
 #if GR_METAL_SDK_VERSION >= 230
     if (@available(macOS 11.0, iOS 14.0, *)) {
-        [fPipelineArchive release];
+        [fGlobalShared->fPipelineArchive release];
     }
 #endif
-    fQueue.reset();
-    fDevice.reset();
+    fGlobalShared->fQueue.reset();
+    fGlobalShared->fDevice.reset();
+
+    fGlobalShared.reset();
 }
 
 sk_sp<SkSurface> MetalWindowContext::getBackbufferSurface() {
@@ -154,7 +189,7 @@ sk_sp<SkSurface> MetalWindowContext::getBackbufferSurface() {
 void MetalWindowContext::swapBuffers() {
     id<CAMetalDrawable> currentDrawable = (id<CAMetalDrawable>)fDrawableHandle;
 
-    id<MTLCommandBuffer> commandBuffer([*fQueue commandBuffer]);
+    id<MTLCommandBuffer> commandBuffer([*fShared->fQueue commandBuffer]);
     commandBuffer.label = @"Present";
 
     [commandBuffer presentDrawable:currentDrawable];
@@ -175,9 +210,9 @@ void MetalWindowContext::activate(bool isActive) {
     if (!isActive) {
 #if GR_METAL_SDK_VERSION >= 230
         if (@available(macOS 11.0, iOS 14.0, *)) {
-            if (fPipelineArchive) {
+            if (fShared->fPipelineArchive) {
                 NSError* error;
-                [fPipelineArchive serializeToURL:CacheURL() error:&error];
+                [fShared->fPipelineArchive serializeToURL:CacheURL() error:&error];
                 if (error) {
                     SkDebugf("Error storing MTLBinaryArchive:\n%s\n",
                              error.debugDescription.UTF8String);
@@ -188,4 +223,11 @@ void MetalWindowContext::activate(bool isActive) {
     }
 }
 
+SK_API sk_sp<MetalWindowContext::Shared> MetalWindowContext::fGlobalShared;
+
+GrDirectContext* getMetalSharedGrDirectContext()
+{
+    return MetalWindowContext::getSharedGrDirectContext();
+}
+
 }   //namespace sk_app
diff --git a/tools/sk_app/VulkanWindowContext.cpp b/tools/sk_app/VulkanWindowContext.cpp
index c9db528ca4..634034da5a 100644
--- a/tools/sk_app/VulkanWindowContext.cpp
+++ b/tools/sk_app/VulkanWindowContext.cpp
@@ -25,9 +25,13 @@
 #endif
 
 #define GET_PROC(F) f ## F = \
-    (PFN_vk ## F) backendContext.fGetProc("vk" #F, fInstance, VK_NULL_HANDLE)
+    (PFN_vk ## F) fGlobalShared->backendContext.fGetProc("vk" #F, fShared->fInstance, VK_NULL_HANDLE)
 #define GET_DEV_PROC(F) f ## F = \
-    (PFN_vk ## F) backendContext.fGetProc("vk" #F, VK_NULL_HANDLE, fDevice)
+    (PFN_vk ## F) fGlobalShared->backendContext.fGetProc("vk" #F, VK_NULL_HANDLE, fShared->fDevice)
+#define GET_PROC_GLOBAL(F) fGlobalShared->f ## F = \
+    (PFN_vk ## F) fGlobalShared->backendContext.fGetProc("vk" #F, fGlobalShared->fInstance, VK_NULL_HANDLE)
+#define GET_DEV_PROC_GLOBAL(F) fGlobalShared->f ## F = \
+    (PFN_vk ## F) fGlobalShared->backendContext.fGetProc("vk" #F, VK_NULL_HANDLE, fGlobalShared->fDevice)
 
 namespace sk_app {
 
@@ -49,31 +53,39 @@ VulkanWindowContext::VulkanWindowContext(const DisplayParams& params,
 }
 
 void VulkanWindowContext::initializeContext() {
+    fShared = fGlobalShared;
+    if( !fShared )
+    {
+    // TODO do we need a mutex?
+
+    fGlobalShared = sk_make_sp<Shared>();
+    Shared* d = fGlobalShared.get(); // shorter variable name
+
     SkASSERT(!fContext);
     // any config code here (particularly for msaa)?
 
     PFN_vkGetInstanceProcAddr getInstanceProc = fGetInstanceProcAddr;
-    GrVkBackendContext backendContext;
+    GrVkBackendContext& backendContext = fGlobalShared->backendContext;
     skgpu::VulkanExtensions extensions;
-    VkPhysicalDeviceFeatures2 features;
-    if (!sk_gpu_test::CreateVkBackendContext(getInstanceProc, &backendContext, &extensions,
-                                             &features, &fDebugCallback, &fPresentQueueIndex,
-                                             fCanPresentFn)) {
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
+    if (!sk_gpu_test::CreateVkBackendContext(getInstanceProc, &backendContext, &extensions, &d->features,
+                                             &d->fDebugCallback, &d->fPresentQueueIndex, fCanPresentFn)) {
+        sk_gpu_test::FreeVulkanFeaturesStructs(&d->features);
+        fGlobalShared.reset();
         return;
     }
 
     if (!extensions.hasExtension(VK_KHR_SURFACE_EXTENSION_NAME, 25) ||
         !extensions.hasExtension(VK_KHR_SWAPCHAIN_EXTENSION_NAME, 68)) {
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
+        sk_gpu_test::FreeVulkanFeaturesStructs(&d->features);
+        fGlobalShared.reset();
         return;
     }
 
-    fInstance = backendContext.fInstance;
-    fPhysicalDevice = backendContext.fPhysicalDevice;
-    fDevice = backendContext.fDevice;
-    fGraphicsQueueIndex = backendContext.fGraphicsQueueIndex;
-    fGraphicsQueue = backendContext.fQueue;
+    d->fInstance = backendContext.fInstance;
+    d->fPhysicalDevice = backendContext.fPhysicalDevice;
+    d->fDevice = backendContext.fDevice;
+    d->fGraphicsQueueIndex = backendContext.fGraphicsQueueIndex;
+    d->fGraphicsQueue = backendContext.fQueue;
 
     PFN_vkGetPhysicalDeviceProperties localGetPhysicalDeviceProperties =
             reinterpret_cast<PFN_vkGetPhysicalDeviceProperties>(
@@ -81,21 +93,30 @@ void VulkanWindowContext::initializeContext() {
                                             backendContext.fInstance,
                                             VK_NULL_HANDLE));
     if (!localGetPhysicalDeviceProperties) {
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
+        sk_gpu_test::FreeVulkanFeaturesStructs(&d->features);
+        fGlobalShared.reset();
         return;
     }
-    VkPhysicalDeviceProperties physDeviceProperties;
-    localGetPhysicalDeviceProperties(backendContext.fPhysicalDevice, &physDeviceProperties);
-    uint32_t physDevVersion = physDeviceProperties.apiVersion;
+    localGetPhysicalDeviceProperties(backendContext.fPhysicalDevice, &d->physDeviceProperties);
+    uint32_t physDevVersion = d->physDeviceProperties.apiVersion;
 
-    fInterface.reset(new skgpu::VulkanInterface(backendContext.fGetProc, fInstance, fDevice,
+    d->fInterface.reset(new skgpu::VulkanInterface(backendContext.fGetProc, d->fInstance, d->fDevice,
                                                 backendContext.fInstanceVersion, physDevVersion,
                                                 &extensions));
 
-    GET_PROC(DestroyInstance);
-    if (fDebugCallback != VK_NULL_HANDLE) {
-        GET_PROC(DestroyDebugReportCallbackEXT);
+    d->fContext = GrDirectContext::MakeVulkan(backendContext, fDisplayParams.fGrContextOptions);
+
+    GET_PROC_GLOBAL(DestroyInstance);
+    GET_DEV_PROC_GLOBAL(DestroyDevice);
+    if (fGlobalShared->fDebugCallback != VK_NULL_HANDLE) {
+        GET_PROC_GLOBAL(DestroyDebugReportCallbackEXT);
     }
+
+    fShared = fGlobalShared;
+    } // if( !fShared )
+
+    fContext = fShared->fContext;
+
     GET_PROC(DestroySurfaceKHR);
     GET_PROC(GetPhysicalDeviceSurfaceSupportKHR);
     GET_PROC(GetPhysicalDeviceSurfaceCapabilitiesKHR);
@@ -103,7 +124,6 @@ void VulkanWindowContext::initializeContext() {
     GET_PROC(GetPhysicalDeviceSurfacePresentModesKHR);
     GET_DEV_PROC(DeviceWaitIdle);
     GET_DEV_PROC(QueueWaitIdle);
-    GET_DEV_PROC(DestroyDevice);
     GET_DEV_PROC(CreateSwapchainKHR);
     GET_DEV_PROC(DestroySwapchainKHR);
     GET_DEV_PROC(GetSwapchainImagesKHR);
@@ -111,46 +131,44 @@ void VulkanWindowContext::initializeContext() {
     GET_DEV_PROC(QueuePresentKHR);
     GET_DEV_PROC(GetDeviceQueue);
 
-    fContext = GrDirectContext::MakeVulkan(backendContext, fDisplayParams.fGrContextOptions);
+    // No actual window, used just to create the shared GrContext.
+    if(fCreateVkSurfaceFn == nullptr)
+        return;
 
-    fSurface = fCreateVkSurfaceFn(fInstance);
+    fSurface = fCreateVkSurfaceFn(fShared->fInstance);
     if (VK_NULL_HANDLE == fSurface) {
         this->destroyContext();
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
         return;
     }
 
+    // create presentQueue
+    fGetDeviceQueue(fShared->fDevice, fShared->fPresentQueueIndex, 0, &fPresentQueue);
+
     VkBool32 supported;
-    VkResult res = fGetPhysicalDeviceSurfaceSupportKHR(fPhysicalDevice, fPresentQueueIndex,
+    VkResult res = fGetPhysicalDeviceSurfaceSupportKHR(fShared->fPhysicalDevice, fShared->fPresentQueueIndex,
                                                        fSurface, &supported);
     if (VK_SUCCESS != res) {
         this->destroyContext();
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
         return;
     }
 
     if (!this->createSwapchain(-1, -1, fDisplayParams)) {
         this->destroyContext();
-        sk_gpu_test::FreeVulkanFeaturesStructs(&features);
         return;
     }
-
-    // create presentQueue
-    fGetDeviceQueue(fDevice, fPresentQueueIndex, 0, &fPresentQueue);
-    sk_gpu_test::FreeVulkanFeaturesStructs(&features);
 }
 
 bool VulkanWindowContext::createSwapchain(int width, int height,
                                           const DisplayParams& params) {
     // check for capabilities
     VkSurfaceCapabilitiesKHR caps;
-    VkResult res = fGetPhysicalDeviceSurfaceCapabilitiesKHR(fPhysicalDevice, fSurface, &caps);
+    VkResult res = fGetPhysicalDeviceSurfaceCapabilitiesKHR(fShared->fPhysicalDevice, fSurface, &caps);
     if (VK_SUCCESS != res) {
         return false;
     }
 
     uint32_t surfaceFormatCount;
-    res = fGetPhysicalDeviceSurfaceFormatsKHR(fPhysicalDevice, fSurface, &surfaceFormatCount,
+    res = fGetPhysicalDeviceSurfaceFormatsKHR(fShared->fPhysicalDevice, fSurface, &surfaceFormatCount,
                                               nullptr);
     if (VK_SUCCESS != res) {
         return false;
@@ -158,14 +176,14 @@ bool VulkanWindowContext::createSwapchain(int width, int height,
 
     SkAutoMalloc surfaceFormatAlloc(surfaceFormatCount * sizeof(VkSurfaceFormatKHR));
     VkSurfaceFormatKHR* surfaceFormats = (VkSurfaceFormatKHR*)surfaceFormatAlloc.get();
-    res = fGetPhysicalDeviceSurfaceFormatsKHR(fPhysicalDevice, fSurface, &surfaceFormatCount,
+    res = fGetPhysicalDeviceSurfaceFormatsKHR(fShared->fPhysicalDevice, fSurface, &surfaceFormatCount,
                                               surfaceFormats);
     if (VK_SUCCESS != res) {
         return false;
     }
 
     uint32_t presentModeCount;
-    res = fGetPhysicalDeviceSurfacePresentModesKHR(fPhysicalDevice, fSurface, &presentModeCount,
+    res = fGetPhysicalDeviceSurfacePresentModesKHR(fShared->fPhysicalDevice, fSurface, &presentModeCount,
                                                    nullptr);
     if (VK_SUCCESS != res) {
         return false;
@@ -173,7 +191,7 @@ bool VulkanWindowContext::createSwapchain(int width, int height,
 
     SkAutoMalloc presentModeAlloc(presentModeCount * sizeof(VkPresentModeKHR));
     VkPresentModeKHR* presentModes = (VkPresentModeKHR*)presentModeAlloc.get();
-    res = fGetPhysicalDeviceSurfacePresentModesKHR(fPhysicalDevice, fSurface, &presentModeCount,
+    res = fGetPhysicalDeviceSurfacePresentModesKHR(fShared->fPhysicalDevice, fSurface, &presentModeCount,
                                                    presentModes);
     if (VK_SUCCESS != res) {
         return false;
@@ -286,8 +304,8 @@ bool VulkanWindowContext::createSwapchain(int width, int height,
     swapchainCreateInfo.imageArrayLayers = 1;
     swapchainCreateInfo.imageUsage = usageFlags;
 
-    uint32_t queueFamilies[] = { fGraphicsQueueIndex, fPresentQueueIndex };
-    if (fGraphicsQueueIndex != fPresentQueueIndex) {
+    uint32_t queueFamilies[] = { fShared->fGraphicsQueueIndex, fShared->fPresentQueueIndex };
+    if (fShared->fGraphicsQueueIndex != fShared->fPresentQueueIndex) {
         swapchainCreateInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
         swapchainCreateInfo.queueFamilyIndexCount = 2;
         swapchainCreateInfo.pQueueFamilyIndices = queueFamilies;
@@ -303,27 +321,27 @@ bool VulkanWindowContext::createSwapchain(int width, int height,
     swapchainCreateInfo.clipped = true;
     swapchainCreateInfo.oldSwapchain = fSwapchain;
 
-    res = fCreateSwapchainKHR(fDevice, &swapchainCreateInfo, nullptr, &fSwapchain);
+    res = fCreateSwapchainKHR(fShared->fDevice, &swapchainCreateInfo, nullptr, &fSwapchain);
     if (VK_SUCCESS != res) {
         return false;
     }
 
     // destroy the old swapchain
     if (swapchainCreateInfo.oldSwapchain != VK_NULL_HANDLE) {
-        fDeviceWaitIdle(fDevice);
+        fDeviceWaitIdle(fShared->fDevice);
 
         this->destroyBuffers();
 
-        fDestroySwapchainKHR(fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
+        fDestroySwapchainKHR(fShared->fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
     }
 
     if (!this->createBuffers(swapchainCreateInfo.imageFormat, usageFlags, colorType,
                              swapchainCreateInfo.imageSharingMode)) {
-        fDeviceWaitIdle(fDevice);
+        fDeviceWaitIdle(fShared->fDevice);
 
         this->destroyBuffers();
 
-        fDestroySwapchainKHR(fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
+        fDestroySwapchainKHR(fShared->fDevice, swapchainCreateInfo.oldSwapchain, nullptr);
     }
 
     return true;
@@ -332,10 +350,10 @@ bool VulkanWindowContext::createSwapchain(int width, int height,
 bool VulkanWindowContext::createBuffers(VkFormat format, VkImageUsageFlags usageFlags,
                                         SkColorType colorType,
                                         VkSharingMode sharingMode) {
-    fGetSwapchainImagesKHR(fDevice, fSwapchain, &fImageCount, nullptr);
+    fGetSwapchainImagesKHR(fShared->fDevice, fSwapchain, &fImageCount, nullptr);
     SkASSERT(fImageCount);
     fImages = new VkImage[fImageCount];
-    fGetSwapchainImagesKHR(fDevice, fSwapchain, &fImageCount, fImages);
+    fGetSwapchainImagesKHR(fShared->fDevice, fSwapchain, &fImageCount, fImages);
 
     // set up initial image layouts and create surfaces
     fImageLayouts = new VkImageLayout[fImageCount];
@@ -351,7 +369,7 @@ bool VulkanWindowContext::createBuffers(VkFormat format, VkImageUsageFlags usage
         info.fFormat = format;
         info.fImageUsageFlags = usageFlags;
         info.fLevelCount = 1;
-        info.fCurrentQueueFamily = fPresentQueueIndex;
+        info.fCurrentQueueFamily = fShared->fPresentQueueIndex;
         info.fSharingMode = sharingMode;
 
         if (usageFlags & VK_IMAGE_USAGE_SAMPLED_BIT) {
@@ -387,8 +405,8 @@ bool VulkanWindowContext::createBuffers(VkFormat format, VkImageUsageFlags usage
     fBackbuffers = new BackbufferInfo[fImageCount + 1];
     for (uint32_t i = 0; i < fImageCount + 1; ++i) {
         fBackbuffers[i].fImageIndex = -1;
-        SkDEBUGCODE(VkResult result = )GR_VK_CALL(fInterface,
-                CreateSemaphore(fDevice, &semaphoreInfo, nullptr,
+        SkDEBUGCODE(VkResult result = )GR_VK_CALL(fShared->fInterface,
+                CreateSemaphore(fShared->fDevice, &semaphoreInfo, nullptr,
                                 &fBackbuffers[i].fRenderSemaphore));
         SkASSERT(result == VK_SUCCESS);
     }
@@ -401,8 +419,8 @@ void VulkanWindowContext::destroyBuffers() {
     if (fBackbuffers) {
         for (uint32_t i = 0; i < fImageCount + 1; ++i) {
             fBackbuffers[i].fImageIndex = -1;
-            GR_VK_CALL(fInterface,
-                       DestroySemaphore(fDevice,
+            GR_VK_CALL(fShared->fInterface,
+                       DestroySemaphore(fShared->fDevice,
                                         fBackbuffers[i].fRenderSemaphore,
                                         nullptr));
         }
@@ -427,42 +445,59 @@ VulkanWindowContext::~VulkanWindowContext() {
 void VulkanWindowContext::destroyContext() {
     if (this->isValid()) {
         fQueueWaitIdle(fPresentQueue);
-        fDeviceWaitIdle(fDevice);
+        fDeviceWaitIdle(fShared->fDevice);
 
         this->destroyBuffers();
 
         if (VK_NULL_HANDLE != fSwapchain) {
-            fDestroySwapchainKHR(fDevice, fSwapchain, nullptr);
+            fDestroySwapchainKHR(fShared->fDevice, fSwapchain, nullptr);
             fSwapchain = VK_NULL_HANDLE;
         }
 
         if (VK_NULL_HANDLE != fSurface) {
-            fDestroySurfaceKHR(fInstance, fSurface, nullptr);
+            fDestroySurfaceKHR(fShared->fInstance, fSurface, nullptr);
             fSurface = VK_NULL_HANDLE;
         }
     }
 
-    SkASSERT(fContext->unique());
     fContext.reset();
-    fInterface.reset();
+    fShared.reset();
+
+    checkDestroyShared();
+}
+
+void VulkanWindowContext::checkDestroyShared()
+{
+    if(!fGlobalShared || !fGlobalShared->unique()) // TODO mutex?
+        return;
+#ifndef SK_TRACE_VK_RESOURCES
+    if(!fGlobalShared->fContext->unique())
+        return;
+#endif
+    SkASSERT(fGlobalShared->fContext->unique());
+    fGlobalShared->fContext.reset();
+    fGlobalShared->fInterface.reset();
 
-    if (VK_NULL_HANDLE != fDevice) {
-        fDestroyDevice(fDevice, nullptr);
-        fDevice = VK_NULL_HANDLE;
+    if (VK_NULL_HANDLE != fGlobalShared->fDevice) {
+        fGlobalShared->fDestroyDevice(fGlobalShared->fDevice, nullptr);
+        fGlobalShared->fDevice = VK_NULL_HANDLE;
     }
 
 #ifdef SK_ENABLE_VK_LAYERS
-    if (fDebugCallback != VK_NULL_HANDLE) {
-        fDestroyDebugReportCallbackEXT(fInstance, fDebugCallback, nullptr);
+    if (fGlobalShared->fDebugCallback != VK_NULL_HANDLE) {
+        fGlobalShared->fDestroyDebugReportCallbackEXT(fGlobalShared->fInstance, fDebugCallback, nullptr);
     }
 #endif
 
-    fPhysicalDevice = VK_NULL_HANDLE;
+    fGlobalShared->fPhysicalDevice = VK_NULL_HANDLE;
 
-    if (VK_NULL_HANDLE != fInstance) {
-        fDestroyInstance(fInstance, nullptr);
-        fInstance = VK_NULL_HANDLE;
+    if (VK_NULL_HANDLE != fGlobalShared->fInstance) {
+        fGlobalShared->fDestroyInstance(fGlobalShared->fInstance, nullptr);
+        fGlobalShared->fInstance = VK_NULL_HANDLE;
     }
+
+    sk_gpu_test::FreeVulkanFeaturesStructs(&fGlobalShared->features);
+    fGlobalShared.reset();
 }
 
 VulkanWindowContext::BackbufferInfo* VulkanWindowContext::getAvailableBackbuffer() {
@@ -488,35 +523,35 @@ sk_sp<SkSurface> VulkanWindowContext::getBackbufferSurface() {
     semaphoreInfo.pNext = nullptr;
     semaphoreInfo.flags = 0;
     VkSemaphore semaphore;
-    SkDEBUGCODE(VkResult result = )GR_VK_CALL(fInterface, CreateSemaphore(fDevice, &semaphoreInfo,
+    SkDEBUGCODE(VkResult result = )GR_VK_CALL(fShared->fInterface, CreateSemaphore(fShared->fDevice, &semaphoreInfo,
                                                                           nullptr, &semaphore));
     SkASSERT(result == VK_SUCCESS);
 
     // acquire the image
-    VkResult res = fAcquireNextImageKHR(fDevice, fSwapchain, UINT64_MAX,
+    VkResult res = fAcquireNextImageKHR(fShared->fDevice, fSwapchain, UINT64_MAX,
                                         semaphore, VK_NULL_HANDLE,
                                         &backbuffer->fImageIndex);
     if (VK_ERROR_SURFACE_LOST_KHR == res) {
         // need to figure out how to create a new vkSurface without the platformData*
         // maybe use attach somehow? but need a Window
-        GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
+        GR_VK_CALL(fShared->fInterface, DestroySemaphore(fShared->fDevice, semaphore, nullptr));
         return nullptr;
     }
     if (VK_ERROR_OUT_OF_DATE_KHR == res) {
         // tear swapchain down and try again
         if (!this->createSwapchain(-1, -1, fDisplayParams)) {
-            GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
+            GR_VK_CALL(fShared->fInterface, DestroySemaphore(fShared->fDevice, semaphore, nullptr));
             return nullptr;
         }
         backbuffer = this->getAvailableBackbuffer();
 
         // acquire the image
-        res = fAcquireNextImageKHR(fDevice, fSwapchain, UINT64_MAX,
+        res = fAcquireNextImageKHR(fShared->fDevice, fSwapchain, UINT64_MAX,
                                    semaphore, VK_NULL_HANDLE,
                                    &backbuffer->fImageIndex);
 
         if (VK_SUCCESS != res) {
-            GR_VK_CALL(fInterface, DestroySemaphore(fDevice, semaphore, nullptr));
+            GR_VK_CALL(fShared->fInterface, DestroySemaphore(fShared->fDevice, semaphore, nullptr));
             return nullptr;
         }
     }
@@ -542,7 +577,7 @@ void VulkanWindowContext::swapBuffers() {
     GrFlushInfo info;
     info.fNumSemaphores = 1;
     info.fSignalSemaphores = &beSemaphore;
-    skgpu::MutableTextureState presentState(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, fPresentQueueIndex);
+    skgpu::MutableTextureState presentState(VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, fShared->fPresentQueueIndex);
     surface->flush(info, &presentState);
     surface->recordingContext()->asDirectContext()->submit();
 
@@ -562,4 +597,6 @@ void VulkanWindowContext::swapBuffers() {
     fQueuePresentKHR(fPresentQueue, &presentInfo);
 }
 
+SK_API sk_sp<VulkanWindowContext::Shared> VulkanWindowContext::fGlobalShared;
+
 }   //namespace sk_app
diff --git a/tools/sk_app/VulkanWindowContext.h b/tools/sk_app/VulkanWindowContext.h
index 7e1fdd9af5..946bca7522 100644
--- a/tools/sk_app/VulkanWindowContext.h
+++ b/tools/sk_app/VulkanWindowContext.h
@@ -19,20 +19,24 @@
 #include "tools/gpu/vk/VkTestUtils.h"
 #include "tools/sk_app/WindowContext.h"
 
+#include <cassert>
+
 class GrRenderTarget;
 
 namespace skgpu { struct VulkanInterface; }
 
 namespace sk_app {
 
-class VulkanWindowContext : public WindowContext {
+class SK_API VulkanWindowContext : public WindowContext {
 public:
     ~VulkanWindowContext() override;
 
+    static GrDirectContext* getSharedGrDirectContext() { return fGlobalShared ? fGlobalShared->fContext.get() : nullptr; }
+
     sk_sp<SkSurface> getBackbufferSurface() override;
     void swapBuffers() override;
 
-    bool isValid() override { return fDevice != VK_NULL_HANDLE; }
+    bool isValid() override { return fSurface != VK_NULL_HANDLE; }
 
     void resize(int w, int h) override {
         this->createSwapchain(w, h, fDisplayParams);
@@ -50,9 +54,15 @@ public:
     VulkanWindowContext(const DisplayParams&, CreateVkSurfaceFn, CanPresentFn,
                         PFN_vkGetInstanceProcAddr);
 
+    static const VkPhysicalDeviceProperties& getPhysDeviceProperties() {
+        assert( fGlobalShared != nullptr );
+        return fGlobalShared->physDeviceProperties;
+    }
+
 private:
     void initializeContext();
     void destroyContext();
+    static void checkDestroyShared();
 
     struct BackbufferInfo {
         uint32_t        fImageIndex;          // image this is associated with
@@ -64,11 +74,6 @@ private:
     bool createBuffers(VkFormat format, VkImageUsageFlags, SkColorType colorType, VkSharingMode);
     void destroyBuffers();
 
-    VkInstance fInstance = VK_NULL_HANDLE;
-    VkPhysicalDevice fPhysicalDevice = VK_NULL_HANDLE;
-    VkDevice fDevice = VK_NULL_HANDLE;
-    VkDebugReportCallbackEXT fDebugCallback = VK_NULL_HANDLE;
-
     // Create functions
     CreateVkSurfaceFn fCreateVkSurfaceFn;
     CanPresentFn      fCanPresentFn;
@@ -88,20 +93,46 @@ private:
     PFN_vkAcquireNextImageKHR fAcquireNextImageKHR = nullptr;
     PFN_vkQueuePresentKHR fQueuePresentKHR = nullptr;
 
-    PFN_vkDestroyInstance fDestroyInstance = nullptr;
     PFN_vkDeviceWaitIdle fDeviceWaitIdle = nullptr;
-    PFN_vkDestroyDebugReportCallbackEXT fDestroyDebugReportCallbackEXT = nullptr;
     PFN_vkQueueWaitIdle fQueueWaitIdle = nullptr;
-    PFN_vkDestroyDevice fDestroyDevice = nullptr;
     PFN_vkGetDeviceQueue fGetDeviceQueue = nullptr;
 
+    // We need to use just one GrDirectContext, so share all the relevant data.
+    struct Shared : public SkRefCnt
+    {
+    PFN_vkDestroyInstance fDestroyInstance = nullptr;
+    PFN_vkDestroyDevice fDestroyDevice = nullptr;
+    PFN_vkDestroyDebugReportCallbackEXT fDestroyDebugReportCallbackEXT = nullptr;
+
+    VkInstance fInstance = VK_NULL_HANDLE;
+    VkPhysicalDevice fPhysicalDevice = VK_NULL_HANDLE;
+    VkDevice fDevice = VK_NULL_HANDLE;
+    VkDebugReportCallbackEXT fDebugCallback = VK_NULL_HANDLE;
+
     sk_sp<const skgpu::VulkanInterface> fInterface;
 
-    VkSurfaceKHR      fSurface;
-    VkSwapchainKHR    fSwapchain;
+    // Original code had this as a function-local variable, but that seems wrong.
+    // It should exist as long as the context exists.
+    VkPhysicalDeviceFeatures2 features;
+
+    // Store this to make it accessible.
+    VkPhysicalDeviceProperties physDeviceProperties;
+
+    GrVkBackendContext backendContext;
+
     uint32_t          fGraphicsQueueIndex;
     VkQueue           fGraphicsQueue;
     uint32_t          fPresentQueueIndex;
+
+    sk_sp<GrDirectContext> fContext;
+    };
+
+    sk_sp<Shared> fShared;
+
+    static sk_sp<Shared> fGlobalShared;
+
+    VkSurfaceKHR      fSurface;
+    VkSwapchainKHR    fSwapchain;
     VkQueue           fPresentQueue;
 
     uint32_t               fImageCount;
diff --git a/tools/sk_app/WindowContext.h b/tools/sk_app/WindowContext.h
index 65ab8b9aa4..2d222385a3 100644
--- a/tools/sk_app/WindowContext.h
+++ b/tools/sk_app/WindowContext.h
@@ -10,9 +10,9 @@
 #include "include/core/SkRefCnt.h"
 #include "include/core/SkSurfaceProps.h"
 #include "include/gpu/GrTypes.h"
+#include "include/gpu/GrDirectContext.h"
 #include "tools/sk_app/DisplayParams.h"
 
-class GrDirectContext;
 class SkSurface;
 #ifdef SK_GRAPHITE_ENABLED
 namespace skgpu::graphite {
diff --git a/tools/sk_app/mac/MetalWindowContext_mac.mm b/tools/sk_app/mac/MetalWindowContext_mac.mm
index 5bea8578fa..f7df061af0 100644
--- a/tools/sk_app/mac/MetalWindowContext_mac.mm
+++ b/tools/sk_app/mac/MetalWindowContext_mac.mm
@@ -49,10 +49,14 @@ MetalWindowContext_mac::~MetalWindowContext_mac() {
 }
 
 bool MetalWindowContext_mac::onInitializeContext() {
+    // Allow creating just the shared context, without an associated window.
+    if(fMainView == nil)
+        return true;
+
     SkASSERT(nil != fMainView);
 
     fMetalLayer = [CAMetalLayer layer];
-    fMetalLayer.device = fDevice.get();
+    fMetalLayer.device = fShared->fDevice.get();
     fMetalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm;
 
     // resize ignores the passed values and uses the fMainView directly.
diff --git a/tools/sk_app/unix/VulkanWindowContext_unix.cpp b/tools/sk_app/unix/VulkanWindowContext_unix.cpp
index 2b31fedc19..0c05fbfc92 100644
--- a/tools/sk_app/unix/VulkanWindowContext_unix.cpp
+++ b/tools/sk_app/unix/VulkanWindowContext_unix.cpp
@@ -30,7 +30,7 @@ std::unique_ptr<WindowContext> MakeVulkanForXlib(const XlibWindowInfo& info,
         return nullptr;
     }
 
-    auto createVkSurface = [&info, instProc](VkInstance instance) -> VkSurfaceKHR {
+    VulkanWindowContext::CreateVkSurfaceFn createVkSurface = [&info, instProc](VkInstance instance) -> VkSurfaceKHR {
         static PFN_vkCreateXcbSurfaceKHR createXcbSurfaceKHR = nullptr;
         if (!createXcbSurfaceKHR) {
             createXcbSurfaceKHR =
@@ -54,6 +54,9 @@ std::unique_ptr<WindowContext> MakeVulkanForXlib(const XlibWindowInfo& info,
 
         return surface;
     };
+    // Allow creating just the shared context, without an associated window.
+    if(info.fWindow == None)
+        createVkSurface = nullptr;
 
     auto canPresent = [&info, instProc](VkInstance instance, VkPhysicalDevice physDev,
                               uint32_t queueFamilyIndex) {
@@ -76,7 +79,7 @@ std::unique_ptr<WindowContext> MakeVulkanForXlib(const XlibWindowInfo& info,
     };
     std::unique_ptr<WindowContext> ctx(
             new VulkanWindowContext(displayParams, createVkSurface, canPresent, instProc));
-    if (!ctx->isValid()) {
+    if (!ctx->isValid() && createVkSurface != nullptr) {
         return nullptr;
     }
     return ctx;
diff --git a/tools/sk_app/win/VulkanWindowContext_win.cpp b/tools/sk_app/win/VulkanWindowContext_win.cpp
index 976c42556e..c8f6b162bf 100644
--- a/tools/sk_app/win/VulkanWindowContext_win.cpp
+++ b/tools/sk_app/win/VulkanWindowContext_win.cpp
@@ -29,7 +29,7 @@ std::unique_ptr<WindowContext> MakeVulkanForWin(HWND hwnd, const DisplayParams&
         return nullptr;
     }
 
-    auto createVkSurface = [hwnd, instProc] (VkInstance instance) -> VkSurfaceKHR {
+    VulkanWindowContext::CreateVkSurfaceFn createVkSurface = [hwnd, instProc] (VkInstance instance) -> VkSurfaceKHR {
         static PFN_vkCreateWin32SurfaceKHR createWin32SurfaceKHR = nullptr;
         if (!createWin32SurfaceKHR) {
             createWin32SurfaceKHR = (PFN_vkCreateWin32SurfaceKHR)
@@ -53,6 +53,9 @@ std::unique_ptr<WindowContext> MakeVulkanForWin(HWND hwnd, const DisplayParams&
 
         return surface;
     };
+    // Allow creating just the shared context, without an associated window.
+    if(hwnd == nullptr)
+        createVkSurface = nullptr;
 
     auto canPresent = [instProc] (VkInstance instance, VkPhysicalDevice physDev,
                                   uint32_t queueFamilyIndex) {
@@ -70,7 +73,7 @@ std::unique_ptr<WindowContext> MakeVulkanForWin(HWND hwnd, const DisplayParams&
 
     std::unique_ptr<WindowContext> ctx(
             new VulkanWindowContext(params, createVkSurface, canPresent, instProc));
-    if (!ctx->isValid()) {
+    if (!ctx->isValid() && createVkSurface != nullptr) {
         return nullptr;
     }
     return ctx;
