blob: 91eba8afc600b99e96c96a8438f77555966551ac [file] [log] [blame]
/*
* Copyright 2020 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkCanvas.h"
#include "include/core/SkDeferredDisplayListRecorder.h"
#include "include/core/SkSurfaceCharacterization.h"
#include "src/gpu/GrContextPriv.h"
#include "src/gpu/GrProxyProvider.h"
#include "src/gpu/GrRecordingContextPriv.h"
#include "src/gpu/GrRenderTargetContext.h"
#include "src/gpu/GrThreadSafeUniquelyKeyedProxyViewCache.h"
#include "tests/Test.h"
#include "tests/TestUtils.h"
static constexpr int kImageWH = 32;
static constexpr auto kImageOrigin = kBottomLeft_GrSurfaceOrigin;
static SkImageInfo default_ii(int wh) {
return SkImageInfo::Make(wh, wh, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
}
static void create_key(GrUniqueKey* key, int wh) {
static const GrUniqueKey::Domain kDomain = GrUniqueKey::GenerateDomain();
GrUniqueKey::Builder builder(key, kDomain, 1);
builder[0] = wh;
builder.finish();
};
SkBitmap create_up_arrow_bitmap(int wh) {
SkBitmap bitmap;
bitmap.allocPixels(default_ii(wh));
SkCanvas tmp(bitmap);
tmp.clear(SK_ColorWHITE);
SkPaint blue;
blue.setColor(SK_ColorBLUE);
blue.setAntiAlias(true);
float halfW = wh / 2.0f;
float halfH = wh / 2.0f;
float thirdW = wh / 3.0f;
SkPath path;
path.moveTo(0, halfH);
path.lineTo(thirdW, halfH);
path.lineTo(thirdW, wh);
path.lineTo(2*thirdW, wh);
path.lineTo(2*thirdW, halfH);
path.lineTo(wh, halfH);
path.lineTo(halfW, 0);
path.close();
tmp.drawPath(path, blue);
return bitmap;
}
class TestHelper {
public:
struct Stats {
int fCacheHits = 0;
int fCacheMisses = 0;
int fNumSWCreations = 0;
int fNumHWCreations = 0;
};
TestHelper(GrDirectContext* dContext) : fDContext(dContext) {
fDst = SkSurface::MakeRenderTarget(dContext, SkBudgeted::kNo, default_ii(kImageWH));
SkAssertResult(fDst);
SkSurfaceCharacterization characterization;
SkAssertResult(fDst->characterize(&characterization));
fRecorder1 = std::make_unique<SkDeferredDisplayListRecorder>(characterization);
fRecorder2 = std::make_unique<SkDeferredDisplayListRecorder>(characterization);
}
~TestHelper() {
fDContext->flush();
fDContext->submit(true);
}
Stats* stats() { return &fStats; }
int numCacheEntries() const { return this->threadSafeViewCache()->numEntries(); }
GrDirectContext* dContext() { return fDContext; }
SkCanvas* liveCanvas() { return fDst ? fDst->getCanvas() : nullptr; }
SkCanvas* ddlCanvas1() { return fRecorder1 ? fRecorder1->getCanvas() : nullptr; }
sk_sp<SkDeferredDisplayList> snap1() {
if (fRecorder1) {
sk_sp<SkDeferredDisplayList> tmp = fRecorder1->detach();
fRecorder1 = nullptr;
return tmp;
}
return nullptr;
}
SkCanvas* ddlCanvas2() { return fRecorder2 ? fRecorder2->getCanvas() : nullptr; }
sk_sp<SkDeferredDisplayList> snap2() {
if (fRecorder2) {
sk_sp<SkDeferredDisplayList> tmp = fRecorder2->detach();
fRecorder2 = nullptr;
return tmp;
}
return nullptr;
}
GrThreadSafeUniquelyKeyedProxyViewCache* threadSafeViewCache() {
return fDContext->priv().threadSafeViewCache();
}
const GrThreadSafeUniquelyKeyedProxyViewCache* threadSafeViewCache() const {
return fDContext->priv().threadSafeViewCache();
}
// Add a draw on 'canvas' that will introduce a ref on the 'wh' view
void accessCachedView(SkCanvas* canvas,
int wh,
bool failLookup = false) {
GrRecordingContext* rContext = canvas->recordingContext();
auto view = AccessCachedView(rContext, this->threadSafeViewCache(),
wh, failLookup, &fStats);
SkASSERT(view);
auto rtc = canvas->internal_private_accessTopLayerRenderTargetContext();
rtc->drawTexture(nullptr,
view,
kPremul_SkAlphaType,
GrSamplerState::Filter::kNearest,
GrSamplerState::MipmapMode::kNone,
SkBlendMode::kSrcOver,
SkPMColor4f(),
SkRect::MakeWH(wh, wh),
SkRect::MakeWH(wh, wh),
GrAA::kNo,
GrQuadAAFlags::kNone,
SkCanvas::kFast_SrcRectConstraint,
SkMatrix::I(),
nullptr);
}
// Besides checking that the number of refs and cache hits and misses are as expected, this
// method also validates that the unique key doesn't appear in any of the other caches.
bool checkView(SkCanvas* canvas, int wh, int hits, int misses, int numRefs) {
if (fStats.fCacheHits != hits || fStats.fCacheMisses != misses) {
SkDebugf("Hits E: %d A: %d --- Misses E: %d A: %d\n",
hits, fStats.fCacheHits, misses, fStats.fCacheMisses);
return false;
}
GrUniqueKey key;
create_key(&key, wh);
auto threadSafeViewCache = this->threadSafeViewCache();
GrSurfaceProxyView view = threadSafeViewCache->find(key);
if (!view.proxy()) {
return false;
}
if (!view.proxy()->refCntGreaterThan(numRefs+1) || // +1 for 'view's ref
view.proxy()->refCntGreaterThan(numRefs+2)) {
return false;
}
if (canvas) {
GrRecordingContext* rContext = canvas->recordingContext();
GrProxyProvider* recordingProxyProvider = rContext->priv().proxyProvider();
sk_sp<GrTextureProxy> result = recordingProxyProvider->findProxyByUniqueKey(key);
if (result) {
// views in this cache should never appear in the recorder's cache
return false;
}
}
{
GrProxyProvider* directProxyProvider = fDContext->priv().proxyProvider();
sk_sp<GrTextureProxy> result = directProxyProvider->findProxyByUniqueKey(key);
if (result) {
// views in this cache should never appear in the main proxy cache
return false;
}
}
{
auto resourceProvider = fDContext->priv().resourceProvider();
sk_sp<GrSurface> surf = resourceProvider->findByUniqueKey<GrSurface>(key);
if (surf) {
// the textures backing the views in this cache should never be discoverable in the
// resource cache
return false;
}
}
return true;
}
size_t gpuSize(int wh) const {
GrBackendFormat format = fDContext->defaultBackendFormat(kRGBA_8888_SkColorType,
GrRenderable::kNo);
return GrSurface::ComputeSize(*fDContext->priv().caps(), format,
{wh, wh}, 1, GrMipMapped::kNo, false);
}
private:
static GrSurfaceProxyView AccessCachedView(GrRecordingContext*,
GrThreadSafeUniquelyKeyedProxyViewCache*,
int wh,
bool failLookup, Stats*);
static GrSurfaceProxyView CreateViewOnCpu(GrRecordingContext*, int wh, Stats*);
static GrSurfaceProxyView CreateViewOnGpu(GrDirectContext*, int wh, Stats*);
Stats fStats;
GrDirectContext* fDContext = nullptr;
sk_sp<SkSurface> fDst;
std::unique_ptr<SkDeferredDisplayListRecorder> fRecorder1;
std::unique_ptr<SkDeferredDisplayListRecorder> fRecorder2;
};
GrSurfaceProxyView TestHelper::CreateViewOnCpu(GrRecordingContext* rContext,
int wh,
Stats* stats) {
GrProxyProvider* proxyProvider = rContext->priv().proxyProvider();
sk_sp<GrTextureProxy> proxy = proxyProvider->createProxyFromBitmap(create_up_arrow_bitmap(wh),
GrMipmapped::kNo,
SkBackingFit::kExact,
SkBudgeted::kYes);
if (!proxy) {
return {};
}
GrSwizzle swizzle = rContext->priv().caps()->getReadSwizzle(proxy->backendFormat(),
GrColorType::kRGBA_8888);
++stats->fNumSWCreations;
return {std::move(proxy), kImageOrigin, swizzle};
}
GrSurfaceProxyView TestHelper::CreateViewOnGpu(GrDirectContext* dContext,
int wh,
Stats* stats) {
GrProxyProvider* proxyProvider = dContext->priv().proxyProvider();
sk_sp<GrTextureProxy> proxy = proxyProvider->createProxyFromBitmap(create_up_arrow_bitmap(wh),
GrMipmapped::kNo,
SkBackingFit::kExact,
SkBudgeted::kYes);
GrSwizzle swizzle = dContext->priv().caps()->getReadSwizzle(proxy->backendFormat(),
GrColorType::kRGBA_8888);
++stats->fNumHWCreations;
return {std::move(proxy), kImageOrigin, swizzle};
}
// TODO: this doesn't actually implement the correct behavior for the gpu-thread. It needs to
// add a view to the cache and then queue up the calls to draw the content.
GrSurfaceProxyView TestHelper::AccessCachedView(
GrRecordingContext* rContext,
GrThreadSafeUniquelyKeyedProxyViewCache* threadSafeViewCache,
int wh,
bool failLookup,
Stats* stats) {
GrUniqueKey key;
create_key(&key, wh);
// We can "fail the lookup" to simulate a threaded race condition
if (auto view = threadSafeViewCache->find(key); !failLookup && view) {
++stats->fCacheHits;
return view;
}
++stats->fCacheMisses;
GrSurfaceProxyView view;
if (GrDirectContext* dContext = rContext->asDirectContext()) {
view = CreateViewOnGpu(dContext, wh, stats);
} else {
view = CreateViewOnCpu(rContext, wh, stats);
}
SkASSERT(view);
return threadSafeViewCache->add(key, view);
}
// Case 1: ensure two DDL recorders share the view
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache1, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1);
}
// Case 2: ensure that, if the direct context version wins, it is reused by the DDL recorders
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache2, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.liveCanvas(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
/*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
}
// Case 3: ensure that, if the cpu-version wins, it is reused by the direct context
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache3, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.liveCanvas(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 1);
}
// Case 4: ensure that, if two DDL recorders get in a race, they still end up sharing a single view
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache4, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
static const bool kFailLookup = true;
helper.accessCachedView(helper.ddlCanvas2(), kImageWH, kFailLookup);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
/*hits*/ 0, /*misses*/ 2, /*refs*/ 2));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 0);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 2);
}
// Case 5: ensure that expanding the map works
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache5, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
auto threadSafeViewCache = helper.threadSafeViewCache();
int size = 16;
helper.accessCachedView(helper.ddlCanvas1(), size);
size_t initialSize = threadSafeViewCache->approxBytesUsedForHash();
while (initialSize == threadSafeViewCache->approxBytesUsedForHash()) {
size *= 2;
helper.accessCachedView(helper.ddlCanvas1(), size);
}
}
// Case 6: check on dropping refs
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache6, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
ddl1 = nullptr;
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 1));
ddl2 = nullptr;
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 0));
// The cache still has its ref
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 0));
}
// Case 7: check that invoking dropAllRefs and dropAllUniqueRefs directly works as expected
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache7, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
REPORTER_ASSERT(reporter, helper.checkView(nullptr, kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas2(), 2*kImageWH);
sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
REPORTER_ASSERT(reporter, helper.checkView(nullptr, 2*kImageWH,
/*hits*/ 0, /*misses*/ 2, /*refs*/ 1));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
helper.threadSafeViewCache()->dropAllUniqueRefs(nullptr);
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
ddl1 = nullptr;
helper.threadSafeViewCache()->dropAllUniqueRefs(nullptr);
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.checkView(nullptr, 2*kImageWH,
/*hits*/ 0, /*misses*/ 2, /*refs*/ 1));
helper.threadSafeViewCache()->dropAllRefs();
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
ddl2 = nullptr;
}
// Case 8: This checks that GrContext::abandonContext works as expected wrt the thread
// safe cache. This simulates the case where we have one DDL that has finished
// recording but one still recording when the abandonContext fires.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache8, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.liveCanvas(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
/*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
ctxInfo.directContext()->abandonContext(); // This should exercise dropAllRefs
sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
ddl1 = nullptr;
ddl2 = nullptr;
}
// Case 9: This checks that GrContext::releaseResourcesAndAbandonContext works as expected wrt
// the thread safe cache. This simulates the case where we have one DDL that has finished
// recording but one still recording when the releaseResourcesAndAbandonContext fires.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache9, reporter, ctxInfo) {
TestHelper helper(ctxInfo.directContext());
helper.accessCachedView(helper.liveCanvas(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
helper.accessCachedView(helper.ddlCanvas2(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), kImageWH,
/*hits*/ 2, /*misses*/ 1, /*refs*/ 3));
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumHWCreations == 1);
REPORTER_ASSERT(reporter, helper.stats()->fNumSWCreations == 0);
ctxInfo.directContext()->releaseResourcesAndAbandonContext(); // This should hit dropAllRefs
sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 0);
ddl1 = nullptr;
ddl2 = nullptr;
}
// Case 10: This checks that the GrContext::purgeUnlockedResources(size_t) variant works as
// expected wrt the thread safe cache. It, in particular, tests out the MRU behavior
// of the shared cache.
DEF_GPUTEST_FOR_RENDERING_CONTEXTS(GrThreadSafeViewCache10, reporter, ctxInfo) {
auto dContext = ctxInfo.directContext();
if (GrBackendApi::kOpenGL != dContext->backend()) {
// The lower-level backends have too much going on for the following simple purging
// test to work
return;
}
TestHelper helper(dContext);
helper.accessCachedView(helper.liveCanvas(), kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 0, /*misses*/ 1, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas1(), kImageWH);
sk_sp<SkDeferredDisplayList> ddl1 = helper.snap1();
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas1(), kImageWH,
/*hits*/ 1, /*misses*/ 1, /*refs*/ 2));
helper.accessCachedView(helper.liveCanvas(), 2*kImageWH);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
/*hits*/ 1, /*misses*/ 2, /*refs*/ 1));
helper.accessCachedView(helper.ddlCanvas2(), 2*kImageWH);
sk_sp<SkDeferredDisplayList> ddl2 = helper.snap2();
REPORTER_ASSERT(reporter, helper.checkView(helper.ddlCanvas2(), 2*kImageWH,
/*hits*/ 2, /*misses*/ 2, /*refs*/ 2));
dContext->flush();
dContext->submit(true);
// This should clear out everything but the textures locked in the thread-safe cache
dContext->purgeUnlockedResources(false);
ddl1 = nullptr;
ddl2 = nullptr;
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 2);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), kImageWH,
/*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
/*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
// Regardless of which image is MRU, this should force the other out
size_t desiredBytes = helper.gpuSize(2*kImageWH) + helper.gpuSize(kImageWH)/2;
auto cache = dContext->priv().getResourceCache();
size_t currentBytes = cache->getResourceBytes();
SkASSERT(currentBytes >= desiredBytes);
size_t amountToPurge = currentBytes - desiredBytes;
// The 2*kImageWH texture should be MRU.
dContext->purgeUnlockedResources(amountToPurge, true);
REPORTER_ASSERT(reporter, helper.numCacheEntries() == 1);
REPORTER_ASSERT(reporter, helper.checkView(helper.liveCanvas(), 2*kImageWH,
/*hits*/ 2, /*misses*/ 2, /*refs*/ 0));
}