diff options
author | Jamie Gennis <jgennis@google.com> | 2011-07-22 16:24:03 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-07-22 16:24:03 -0700 |
commit | bbb85c8e4e4e67f4fa018694b587fd3228b0d1b2 (patch) | |
tree | 1eb340996b65cd6f2af43be135691e78aba37200 | |
parent | 5bae58dff90025dd1419bd7508d17bbd32c26beb (diff) | |
parent | 5ef59bc764d6bcd0ccf0a266d7d9ab792668a3e9 (diff) | |
download | frameworks_base-bbb85c8e4e4e67f4fa018694b587fd3228b0d1b2.zip frameworks_base-bbb85c8e4e4e67f4fa018694b587fd3228b0d1b2.tar.gz frameworks_base-bbb85c8e4e4e67f4fa018694b587fd3228b0d1b2.tar.bz2 |
Merge changes Ibd261f7b,Ifdd234ef,I0e2f0bc3
* changes:
SurfaceTexture: add the abandon method.
SurfaceTexture: add a deadlock scenario test
SurfaceMediaSource: remove getAllocator method
-rw-r--r-- | include/gui/ISurfaceTexture.h | 2 | ||||
-rw-r--r-- | include/gui/SurfaceTexture.h | 20 | ||||
-rw-r--r-- | include/media/stagefright/SurfaceMediaSource.h | 9 | ||||
-rw-r--r-- | libs/gui/ISurfaceTexture.cpp | 14 | ||||
-rw-r--r-- | libs/gui/SurfaceTexture.cpp | 83 | ||||
-rw-r--r-- | libs/gui/SurfaceTextureClient.cpp | 9 | ||||
-rw-r--r-- | libs/gui/tests/SurfaceTexture_test.cpp | 151 | ||||
-rw-r--r-- | media/libstagefright/SurfaceMediaSource.cpp | 19 |
8 files changed, 264 insertions, 43 deletions
diff --git a/include/gui/ISurfaceTexture.h b/include/gui/ISurfaceTexture.h index 405a25a..1eda646 100644 --- a/include/gui/ISurfaceTexture.h +++ b/include/gui/ISurfaceTexture.h @@ -51,7 +51,7 @@ protected: // the given slot index, and the client is expected to mirror the // slot->buffer mapping so that it's not necessary to transfer a // GraphicBuffer for every dequeue operation. - virtual sp<GraphicBuffer> requestBuffer(int slot) = 0; + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf) = 0; // setBufferCount sets the number of buffer slots available. Calling this // will also cause all buffer slots to be emptied. The caller should empty diff --git a/include/gui/SurfaceTexture.h b/include/gui/SurfaceTexture.h index 62ea943..134c208 100644 --- a/include/gui/SurfaceTexture.h +++ b/include/gui/SurfaceTexture.h @@ -69,7 +69,7 @@ public: // SurfaceTexture object (i.e. they are not owned by the client). virtual status_t setBufferCount(int bufferCount); - virtual sp<GraphicBuffer> requestBuffer(int buf); + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf); // dequeueBuffer gets the next buffer slot index for the client to use. If a // buffer slot is available then that slot index is written to the location @@ -190,6 +190,17 @@ public: // getCurrentScalingMode returns the scaling mode of the current buffer uint32_t getCurrentScalingMode() const; + // abandon frees all the buffers and puts the SurfaceTexture into the + // 'abandoned' state. Once put in this state the SurfaceTexture can never + // leave it. When in the 'abandoned' state, all methods of the + // ISurfaceTexture interface will fail with the NO_INIT error. + // + // Note that while calling this method causes all the buffers to be freed + // from the perspective of the the SurfaceTexture, if there are additional + // references on the buffers (e.g. if a buffer is referenced by a client or + // by OpenGL ES as a texture) then those buffer will remain allocated. + void abandon(); + // dump our state in a String void dump(String8& result) const; void dump(String8& result, const char* prefix, char* buffer, size_t SIZE) const; @@ -411,6 +422,13 @@ private: typedef Vector<int> Fifo; Fifo mQueue; + // mAbandoned indicates that the SurfaceTexture will no longer be used to + // consume images buffers pushed to it using the ISurfaceTexture interface. + // It is initialized to false, and set to true in the abandon method. A + // SurfaceTexture that has been abandoned will return the NO_INIT error from + // all ISurfaceTexture methods capable of returning an error. + bool mAbandoned; + // mMutex is the mutex used to prevent concurrent access to the member // variables of SurfaceTexture objects. It must be locked whenever the // member variables are accessed. diff --git a/include/media/stagefright/SurfaceMediaSource.h b/include/media/stagefright/SurfaceMediaSource.h index e1852ec..4251ba9 100644 --- a/include/media/stagefright/SurfaceMediaSource.h +++ b/include/media/stagefright/SurfaceMediaSource.h @@ -81,7 +81,7 @@ public: // SurfaceMediaSource object (i.e. they are not owned by the client). virtual status_t setBufferCount(int bufferCount); - virtual sp<GraphicBuffer> requestBuffer(int buf); + virtual status_t requestBuffer(int slot, sp<GraphicBuffer>* buf); // dequeueBuffer gets the next buffer slot index for the client to use. If a // buffer slot is available then that slot index is written to the location @@ -163,13 +163,6 @@ public: // when a new frame becomes available. void setFrameAvailableListener(const sp<FrameAvailableListener>& listener); - // getAllocator retrieves the binder object that must be referenced as long - // as the GraphicBuffers dequeued from this SurfaceMediaSource are referenced. - // Holding this binder reference prevents SurfaceFlinger from freeing the - // buffers before the client is done with them. - sp<IBinder> getAllocator(); - - // getCurrentBuffer returns the buffer associated with the current image. sp<GraphicBuffer> getCurrentBuffer() const; diff --git a/libs/gui/ISurfaceTexture.cpp b/libs/gui/ISurfaceTexture.cpp index c9c7397..55246dc 100644 --- a/libs/gui/ISurfaceTexture.cpp +++ b/libs/gui/ISurfaceTexture.cpp @@ -54,18 +54,18 @@ public: { } - virtual sp<GraphicBuffer> requestBuffer(int bufferIdx) { + virtual status_t requestBuffer(int bufferIdx, sp<GraphicBuffer>* buf) { Parcel data, reply; data.writeInterfaceToken(ISurfaceTexture::getInterfaceDescriptor()); data.writeInt32(bufferIdx); remote()->transact(REQUEST_BUFFER, data, &reply); - sp<GraphicBuffer> buffer; bool nonNull = reply.readInt32(); if (nonNull) { - buffer = new GraphicBuffer(); - reply.read(*buffer); + *buf = new GraphicBuffer(); + reply.read(**buf); } - return buffer; + status_t result = reply.readInt32(); + return result; } virtual status_t setBufferCount(int bufferCount) @@ -192,11 +192,13 @@ status_t BnSurfaceTexture::onTransact( case REQUEST_BUFFER: { CHECK_INTERFACE(ISurfaceTexture, data, reply); int bufferIdx = data.readInt32(); - sp<GraphicBuffer> buffer(requestBuffer(bufferIdx)); + sp<GraphicBuffer> buffer; + int result = requestBuffer(bufferIdx, &buffer); reply->writeInt32(buffer != 0); if (buffer != 0) { reply->write(*buffer); } + reply->writeInt32(result); return NO_ERROR; } break; case SET_BUFFER_COUNT: { diff --git a/libs/gui/SurfaceTexture.cpp b/libs/gui/SurfaceTexture.cpp index 54d963f..c190195 100644 --- a/libs/gui/SurfaceTexture.cpp +++ b/libs/gui/SurfaceTexture.cpp @@ -94,7 +94,8 @@ SurfaceTexture::SurfaceTexture(GLuint tex, bool allowSynchronousMode) : mTexName(tex), mSynchronousMode(false), mAllowSynchronousMode(allowSynchronousMode), - mConnectedApi(NO_CONNECTED_API) { + mConnectedApi(NO_CONNECTED_API), + mAbandoned(false) { LOGV("SurfaceTexture::SurfaceTexture"); sp<ISurfaceComposer> composer(ComposerService::getComposerService()); mGraphicBufferAlloc = composer->createGraphicBufferAlloc(); @@ -150,6 +151,11 @@ status_t SurfaceTexture::setBufferCount(int bufferCount) { LOGV("SurfaceTexture::setBufferCount"); Mutex::Autolock lock(mMutex); + if (mAbandoned) { + LOGE("setBufferCount: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + if (bufferCount > NUM_BUFFER_SLOTS) { LOGE("setBufferCount: bufferCount larger than slots available"); return BAD_VALUE; @@ -199,22 +205,32 @@ status_t SurfaceTexture::setDefaultBufferSize(uint32_t w, uint32_t h) return OK; } -sp<GraphicBuffer> SurfaceTexture::requestBuffer(int buf) { +status_t SurfaceTexture::requestBuffer(int slot, sp<GraphicBuffer>* buf) { LOGV("SurfaceTexture::requestBuffer"); Mutex::Autolock lock(mMutex); - if (buf < 0 || mBufferCount <= buf) { + if (mAbandoned) { + LOGE("requestBuffer: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + if (slot < 0 || mBufferCount <= slot) { LOGE("requestBuffer: slot index out of range [0, %d]: %d", - mBufferCount, buf); - return 0; + mBufferCount, slot); + return BAD_VALUE; } - mSlots[buf].mRequestBufferCalled = true; - return mSlots[buf].mGraphicBuffer; + mSlots[slot].mRequestBufferCalled = true; + *buf = mSlots[slot].mGraphicBuffer; + return NO_ERROR; } status_t SurfaceTexture::dequeueBuffer(int *outBuf, uint32_t w, uint32_t h, uint32_t format, uint32_t usage) { LOGV("SurfaceTexture::dequeueBuffer"); + if (mAbandoned) { + LOGE("dequeueBuffer: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + if ((w && !h) || (!w && h)) { LOGE("dequeueBuffer: invalid size: w=%u, h=%u", w, h); return BAD_VALUE; @@ -252,6 +268,11 @@ status_t SurfaceTexture::dequeueBuffer(int *outBuf, uint32_t w, uint32_t h, // wait for the FIFO to drain while (!mQueue.isEmpty()) { mDequeueCondition.wait(mMutex); + if (mAbandoned) { + LOGE("dequeueBuffer: SurfaceTexture was abandoned while " + "blocked!"); + return NO_INIT; + } } minBufferCountNeeded = mSynchronousMode ? MIN_SYNC_BUFFER_SLOTS : MIN_ASYNC_BUFFER_SLOTS; @@ -380,6 +401,11 @@ status_t SurfaceTexture::dequeueBuffer(int *outBuf, uint32_t w, uint32_t h, status_t SurfaceTexture::setSynchronousMode(bool enabled) { Mutex::Autolock lock(mMutex); + if (mAbandoned) { + LOGE("setSynchronousMode: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + status_t err = OK; if (!mAllowSynchronousMode && enabled) return err; @@ -410,6 +436,10 @@ status_t SurfaceTexture::queueBuffer(int buf, int64_t timestamp, { // scope for the lock Mutex::Autolock lock(mMutex); + if (mAbandoned) { + LOGE("queueBuffer: SurfaceTexture has been abandoned!"); + return NO_INIT; + } if (buf < 0 || buf >= mBufferCount) { LOGE("queueBuffer: slot index out of range [0, %d]: %d", mBufferCount, buf); @@ -475,6 +505,12 @@ status_t SurfaceTexture::queueBuffer(int buf, int64_t timestamp, void SurfaceTexture::cancelBuffer(int buf) { LOGV("SurfaceTexture::cancelBuffer"); Mutex::Autolock lock(mMutex); + + if (mAbandoned) { + LOGW("cancelBuffer: SurfaceTexture has been abandoned!"); + return; + } + if (buf < 0 || buf >= mBufferCount) { LOGE("cancelBuffer: slot index out of range [0, %d]: %d", mBufferCount, buf); @@ -491,6 +527,10 @@ void SurfaceTexture::cancelBuffer(int buf) { status_t SurfaceTexture::setCrop(const Rect& crop) { LOGV("SurfaceTexture::setCrop"); Mutex::Autolock lock(mMutex); + if (mAbandoned) { + LOGE("setCrop: SurfaceTexture has been abandoned!"); + return NO_INIT; + } mNextCrop = crop; return OK; } @@ -498,6 +538,10 @@ status_t SurfaceTexture::setCrop(const Rect& crop) { status_t SurfaceTexture::setTransform(uint32_t transform) { LOGV("SurfaceTexture::setTransform"); Mutex::Autolock lock(mMutex); + if (mAbandoned) { + LOGE("setTransform: SurfaceTexture has been abandoned!"); + return NO_INIT; + } mNextTransform = transform; return OK; } @@ -505,6 +549,12 @@ status_t SurfaceTexture::setTransform(uint32_t transform) { status_t SurfaceTexture::connect(int api) { LOGV("SurfaceTexture::connect(this=%p, %d)", this, api); Mutex::Autolock lock(mMutex); + + if (mAbandoned) { + LOGE("connect: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + int err = NO_ERROR; switch (api) { case NATIVE_WINDOW_API_EGL: @@ -529,6 +579,12 @@ status_t SurfaceTexture::connect(int api) { status_t SurfaceTexture::disconnect(int api) { LOGV("SurfaceTexture::disconnect(this=%p, %d)", this, api); Mutex::Autolock lock(mMutex); + + if (mAbandoned) { + LOGE("connect: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + int err = NO_ERROR; switch (api) { case NATIVE_WINDOW_API_EGL: @@ -837,6 +893,12 @@ uint32_t SurfaceTexture::getCurrentScalingMode() const { int SurfaceTexture::query(int what, int* outValue) { Mutex::Autolock lock(mMutex); + + if (mAbandoned) { + LOGE("query: SurfaceTexture has been abandoned!"); + return NO_INIT; + } + int value; switch (what) { case NATIVE_WINDOW_WIDTH: @@ -863,6 +925,13 @@ int SurfaceTexture::query(int what, int* outValue) return NO_ERROR; } +void SurfaceTexture::abandon() { + Mutex::Autolock lock(mMutex); + freeAllBuffers(); + mAbandoned = true; + mDequeueCondition.signal(); +} + void SurfaceTexture::dump(String8& result) const { char buffer[1024]; diff --git a/libs/gui/SurfaceTextureClient.cpp b/libs/gui/SurfaceTextureClient.cpp index 688b99b..df0ad5a 100644 --- a/libs/gui/SurfaceTextureClient.cpp +++ b/libs/gui/SurfaceTextureClient.cpp @@ -148,10 +148,11 @@ int SurfaceTextureClient::dequeueBuffer(android_native_buffer_t** buffer) { } if ((result & ISurfaceTexture::BUFFER_NEEDS_REALLOCATION) || gbuf == 0) { - gbuf = mSurfaceTexture->requestBuffer(buf); - if (gbuf == 0) { - LOGE("dequeueBuffer: ISurfaceTexture::requestBuffer failed"); - return NO_MEMORY; + result = mSurfaceTexture->requestBuffer(buf, &gbuf); + if (result != NO_ERROR) { + LOGE("dequeueBuffer: ISurfaceTexture::requestBuffer failed: %d", + result); + return result; } mQueryWidth = gbuf->width; mQueryHeight = gbuf->height; diff --git a/libs/gui/tests/SurfaceTexture_test.cpp b/libs/gui/tests/SurfaceTexture_test.cpp index 9abe89d..0fac6cd 100644 --- a/libs/gui/tests/SurfaceTexture_test.cpp +++ b/libs/gui/tests/SurfaceTexture_test.cpp @@ -1018,6 +1018,83 @@ TEST_F(SurfaceTextureGLTest, DISABLED_TexturingFromGLFilledRGBABufferPow2) { EXPECT_TRUE(checkPixel( 3, 52, 153, 153, 153, 153)); } +TEST_F(SurfaceTextureGLTest, AbandonUnblocksDequeueBuffer) { + class ProducerThread : public Thread { + public: + ProducerThread(const sp<ANativeWindow>& anw): + mANW(anw), + mDequeueError(NO_ERROR) { + } + + virtual ~ProducerThread() { + } + + virtual bool threadLoop() { + Mutex::Autolock lock(mMutex); + ANativeWindowBuffer* anb; + + // Frame 1 + if (mANW->dequeueBuffer(mANW.get(), &anb) != NO_ERROR) { + return false; + } + if (anb == NULL) { + return false; + } + if (mANW->queueBuffer(mANW.get(), anb) + != NO_ERROR) { + return false; + } + + // Frame 2 + if (mANW->dequeueBuffer(mANW.get(), &anb) != NO_ERROR) { + return false; + } + if (anb == NULL) { + return false; + } + if (mANW->queueBuffer(mANW.get(), anb) + != NO_ERROR) { + return false; + } + + // Frame 3 - error expected + mDequeueError = mANW->dequeueBuffer(mANW.get(), &anb); + return false; + } + + status_t getDequeueError() { + Mutex::Autolock lock(mMutex); + return mDequeueError; + } + + private: + sp<ANativeWindow> mANW; + status_t mDequeueError; + Mutex mMutex; + }; + + sp<FrameWaiter> fw(new FrameWaiter); + mST->setFrameAvailableListener(fw); + ASSERT_EQ(OK, mST->setSynchronousMode(true)); + ASSERT_EQ(OK, mST->setBufferCountServer(2)); + + sp<Thread> pt(new ProducerThread(mANW)); + pt->run(); + + fw->waitForFrame(); + fw->waitForFrame(); + + // Sleep for 100ms to allow the producer thread's dequeueBuffer call to + // block waiting for a buffer to become available. + usleep(100000); + + mST->abandon(); + + pt->requestExitAndWait(); + ASSERT_EQ(NO_INIT, + reinterpret_cast<ProducerThread*>(pt.get())->getDequeueError()); +} + /* * This test is for testing GL -> GL texture streaming via SurfaceTexture. It * contains functionality to create a producer thread that will perform GL @@ -1205,7 +1282,7 @@ protected: sp<FrameCondition> mFC; }; -TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageBeforeFrameFinishedWorks) { +TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageBeforeFrameFinishedCompletes) { class PT : public ProducerThread { virtual void render() { glClearColor(0.0f, 1.0f, 0.0f, 1.0f); @@ -1223,7 +1300,7 @@ TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageBeforeFrameFinishedWorks) { // TODO: Add frame verification once RGB TEX_EXTERNAL_OES is supported! } -TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageAfterFrameFinishedWorks) { +TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageAfterFrameFinishedCompletes) { class PT : public ProducerThread { virtual void render() { glClearColor(0.0f, 1.0f, 0.0f, 1.0f); @@ -1241,7 +1318,7 @@ TEST_F(SurfaceTextureGLToGLTest, UpdateTexImageAfterFrameFinishedWorks) { // TODO: Add frame verification once RGB TEX_EXTERNAL_OES is supported! } -TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageBeforeFrameFinishedWorks) { +TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageBeforeFrameFinishedCompletes) { enum { NUM_ITERATIONS = 1024 }; class PT : public ProducerThread { @@ -1269,7 +1346,7 @@ TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageBeforeFrameFinishedWorks) } } -TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageAfterFrameFinishedWorks) { +TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageAfterFrameFinishedCompletes) { enum { NUM_ITERATIONS = 1024 }; class PT : public ProducerThread { @@ -1297,4 +1374,70 @@ TEST_F(SurfaceTextureGLToGLTest, RepeatedUpdateTexImageAfterFrameFinishedWorks) } } +// XXX: This test is disabled because it is currently hanging on some devices. +TEST_F(SurfaceTextureGLToGLTest, DISABLED_RepeatedSwapBuffersWhileDequeueStalledCompletes) { + enum { NUM_ITERATIONS = 64 }; + + class PT : public ProducerThread { + virtual void render() { + for (int i = 0; i < NUM_ITERATIONS; i++) { + glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + LOGV("+swapBuffers"); + swapBuffers(); + LOGV("-swapBuffers"); + } + } + }; + + ASSERT_EQ(OK, mST->setSynchronousMode(true)); + ASSERT_EQ(OK, mST->setBufferCountServer(2)); + + runProducerThread(new PT()); + + // Allow three frames to be rendered and queued before starting the + // rendering in this thread. For the latter two frames we don't call + // updateTexImage so the next dequeue from the producer thread will block + // waiting for a frame to become available. + mFC->waitForFrame(); + mFC->finishFrame(); + + // We must call updateTexImage to consume the first frame so that the + // SurfaceTexture is able to reduce the buffer count to 2. This is because + // the GL driver may dequeue a buffer when the EGLSurface is created, and + // that happens before we call setBufferCountServer. It's possible that the + // driver does not dequeue a buffer at EGLSurface creation time, so we + // cannot rely on this to cause the second dequeueBuffer call to block. + mST->updateTexImage(); + + mFC->waitForFrame(); + mFC->finishFrame(); + mFC->waitForFrame(); + mFC->finishFrame(); + + // Sleep for 100ms to allow the producer thread's dequeueBuffer call to + // block waiting for a buffer to become available. + usleep(100000); + + // Render and present a number of images. This thread should not be blocked + // by the fact that the producer thread is blocking in dequeue. + for (int i = 0; i < NUM_ITERATIONS; i++) { + glClear(GL_COLOR_BUFFER_BIT); + eglSwapBuffers(mEglDisplay, mEglSurface); + } + + // Consume the two pending buffers to unblock the producer thread. + mST->updateTexImage(); + mST->updateTexImage(); + + // Consume the remaining buffers from the producer thread. + for (int i = 0; i < NUM_ITERATIONS-3; i++) { + mFC->waitForFrame(); + mFC->finishFrame(); + LOGV("+updateTexImage"); + mST->updateTexImage(); + LOGV("-updateTexImage"); + } +} + } // namespace android diff --git a/media/libstagefright/SurfaceMediaSource.cpp b/media/libstagefright/SurfaceMediaSource.cpp index db208dd..7d08f09 100644 --- a/media/libstagefright/SurfaceMediaSource.cpp +++ b/media/libstagefright/SurfaceMediaSource.cpp @@ -147,16 +147,17 @@ status_t SurfaceMediaSource::setBufferCount(int bufferCount) { return OK; } -sp<GraphicBuffer> SurfaceMediaSource::requestBuffer(int buf) { +status_t SurfaceMediaSource::requestBuffer(int slot, sp<GraphicBuffer>* buf) { LOGV("SurfaceMediaSource::requestBuffer"); Mutex::Autolock lock(mMutex); - if (buf < 0 || mBufferCount <= buf) { + if (slot < 0 || mBufferCount <= slot) { LOGE("requestBuffer: slot index out of range [0, %d]: %d", - mBufferCount, buf); - return 0; + mBufferCount, slot); + return BAD_VALUE; } - mSlots[buf].mRequestBufferCalled = true; - return mSlots[buf].mGraphicBuffer; + mSlots[slot].mRequestBufferCalled = true; + *buf = mSlots[slot].mGraphicBuffer; + return NO_ERROR; } status_t SurfaceMediaSource::dequeueBuffer(int *outBuf, uint32_t w, uint32_t h, @@ -525,12 +526,6 @@ void SurfaceMediaSource::setFrameAvailableListener( mFrameAvailableListener = listener; } -sp<IBinder> SurfaceMediaSource::getAllocator() { - LOGV("getAllocator"); - return mGraphicBufferAlloc->asBinder(); -} - - void SurfaceMediaSource::freeAllBuffers() { LOGV("freeAllBuffers"); for (int i = 0; i < NUM_BUFFER_SLOTS; i++) { |