From 766010858ea7696d64f1b559413670bdd8627595 Mon Sep 17 00:00:00 2001 From: Jamie Gennis Date: Sun, 6 Nov 2011 14:14:33 -0800 Subject: [PATCH 1/2] EGL: use an in-memory the blob cache This change makes the makes the stub EGL_ANDROID_blob_cache callbacks actually use a BlobCache object. Bug: 5474671 Change-Id: I5cbaae2dea3aad2fe306c9f57029c3f215a0863a --- opengl/include/EGL/eglext.h | 15 ++++++ opengl/libs/EGL/egl_cache.cpp | 94 +++++++++++++++++++++++++++------ opengl/libs/EGL/egl_cache.h | 75 ++++++++++++++++++++++++++ opengl/libs/EGL/egl_display.cpp | 1 + 4 files changed, 168 insertions(+), 17 deletions(-) diff --git a/opengl/include/EGL/eglext.h b/opengl/include/EGL/eglext.h index a08932a3d..8e8e26c0a 100644 --- a/opengl/include/EGL/eglext.h +++ b/opengl/include/EGL/eglext.h @@ -256,6 +256,21 @@ typedef EGLuint64NV (EGLAPIENTRYP PFNEGLGETSYSTEMTIMEFREQUENCYNVPROC)(void); typedef EGLuint64NV (EGLAPIENTRYP PFNEGLGETSYSTEMTIMENVPROC)(void); #endif + +/* EGL_ANDROID_blob_cache + */ +#ifndef EGL_ANDROID_blob_cache +#define EGL_ANDROID_blob_cache 1 +typedef khronos_ssize_t EGLsizei; +typedef void (*EGLSetBlobFunc) (const void* key, EGLsizei keySize, const void* value, EGLsizei valueSize); +typedef EGLsizei (*EGLGetBlobFunc) (const void* key, EGLsizei keySize, void* value, EGLsizei valueSize); +#ifdef EGL_EGLEXT_PROTOTYPES +EGLAPI void EGLAPIENTRY eglSetBlobCacheFuncs(EGLDisplay dpy, EGLSetBlobFunc set, EGLGetBlobFunc get); +#endif /* EGL_EGLEXT_PROTOTYPES */ +typedef void (EGLAPIENTRYP PFNEGLSETBLOBCACHEFUNCSPROC) (EGLDisplay dpy, + EGLSetBlobFunc set, EGLGetBlobFunc get); +#endif + #ifdef __cplusplus } #endif diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp index 1e64302f2..adb2a13d6 100644 --- a/opengl/libs/EGL/egl_cache.cpp +++ b/opengl/libs/EGL/egl_cache.cpp @@ -19,6 +19,11 @@ #include "egl_impl.h" #include "egldefs.h" +// Cache size limits. +static const size_t maxKeySize = 1024; +static const size_t maxValueSize = 4096; +static const size_t maxTotalSize = 64 * 1024; + // ---------------------------------------------------------------------------- namespace android { // ---------------------------------------------------------------------------- @@ -26,29 +31,27 @@ namespace android { #define BC_EXT_STR "EGL_ANDROID_blob_cache" // -// EGL_ANDROID_blob_cache types and functions -// -typedef khronos_ssize_t EGLsizei; - -typedef void (*EGLSetBlobFunc) (const void* key, EGLsizei keySize, - const void* value, EGLsizei valueSize); - -typedef EGLsizei (*EGLGetBlobFunc) (const void* key, EGLsizei keySize, - void* value, EGLsizei valueSize); - -typedef void (EGLAPIENTRYP PFNEGLSETBLOBCACHEFUNCSPROC) (EGLDisplay dpy, - EGLSetBlobFunc set, EGLGetBlobFunc get); - -// -// egl_cache_t definition +// Callback functions passed to EGL. // static void setBlob(const void* key, EGLsizei keySize, const void* value, EGLsizei valueSize) { + egl_cache_t::get()->setBlob(key, keySize, value, valueSize); } static EGLsizei getBlob(const void* key, EGLsizei keySize, void* value, EGLsizei valueSize) { - return 0; + return egl_cache_t::get()->getBlob(key, keySize, value, valueSize); +} + +// +// egl_cache_t definition +// +egl_cache_t::egl_cache_t() : + mInitialized(false), + mBlobCache(NULL) { +} + +egl_cache_t::~egl_cache_t() { } egl_cache_t* egl_cache_t::get() { @@ -57,6 +60,7 @@ egl_cache_t* egl_cache_t::get() { } void egl_cache_t::initialize(egl_display_t *display) { + Mutex::Autolock lock(mMutex); for (int i = 0; i < IMPL_NUM_IMPLEMENTATIONS; i++) { egl_connection_t* const cnx = &gEGLImpl[i]; if (cnx->dso && cnx->major >= 0 && cnx->minor >= 0) { @@ -79,7 +83,8 @@ void egl_cache_t::initialize(egl_display_t *display) { continue; } - eglSetBlobCacheFuncs(display->disp[i].dpy, setBlob, getBlob); + eglSetBlobCacheFuncs(display->disp[i].dpy, android::setBlob, + android::getBlob); EGLint err = cnx->egl.eglGetError(); if (err != EGL_SUCCESS) { LOGE("eglSetBlobCacheFuncs resulted in an error: %#x", @@ -88,6 +93,61 @@ void egl_cache_t::initialize(egl_display_t *display) { } } } + mInitialized = true; +} + +void egl_cache_t::terminate() { + Mutex::Autolock lock(mMutex); + if (mBlobCache != NULL) { + saveBlobCacheLocked(); + mBlobCache = NULL; + } + mInitialized = false; +} + +void egl_cache_t::setBlob(const void* key, EGLsizei keySize, const void* value, + EGLsizei valueSize) { + Mutex::Autolock lock(mMutex); + + if (keySize < 0 || valueSize < 0) { + LOGW("EGL_ANDROID_blob_cache set: negative sizes are not allowed"); + return; + } + + if (mInitialized) { + sp bc = getBlobCacheLocked(); + bc->set(key, keySize, value, valueSize); + } +} + +EGLsizei egl_cache_t::getBlob(const void* key, EGLsizei keySize, void* value, + EGLsizei valueSize) { + Mutex::Autolock lock(mMutex); + + if (keySize < 0 || valueSize < 0) { + LOGW("EGL_ANDROID_blob_cache set: negative sizes are not allowed"); + return 0; + } + + if (mInitialized) { + sp bc = getBlobCacheLocked(); + return bc->get(key, keySize, value, valueSize); + } + return 0; +} + +sp egl_cache_t::getBlobCacheLocked() { + if (mBlobCache == NULL) { + mBlobCache = new BlobCache(maxKeySize, maxValueSize, maxTotalSize); + loadBlobCacheLocked(); + } + return mBlobCache; +} + +void egl_cache_t::saveBlobCacheLocked() { +} + +void egl_cache_t::loadBlobCacheLocked() { } // ---------------------------------------------------------------------------- diff --git a/opengl/libs/EGL/egl_cache.h b/opengl/libs/EGL/egl_cache.h index 1fcfaccd8..70217528f 100644 --- a/opengl/libs/EGL/egl_cache.h +++ b/opengl/libs/EGL/egl_cache.h @@ -14,6 +14,15 @@ ** limitations under the License. */ +#ifndef ANDROID_EGL_CACHE_H +#define ANDROID_EGL_CACHE_H + +#include +#include + +#include +#include + // ---------------------------------------------------------------------------- namespace android { // ---------------------------------------------------------------------------- @@ -23,11 +32,77 @@ class egl_display_t; class egl_cache_t { public: + // get returns a pointer to the singleton egl_cache_t object. This + // singleton object will never be destroyed. static egl_cache_t* get(); + // initialize puts the egl_cache_t into an initialized state, such that it + // is able to insert and retrieve entries from the cache. This should be + // called when EGL is initialized. When not in the initialized state the + // getBlob and setBlob methods will return without performing any cache + // operations. void initialize(egl_display_t* display); + + // terminate puts the egl_cache_t back into the uninitialized state. When + // in this state the getBlob and setBlob methods will return without + // performing any cache operations. + void terminate(); + + // setBlob attempts to insert a new key/value blob pair into the cache. + // This will be called by the hardware vendor's EGL implementation via the + // EGL_ANDROID_blob_cache extension. + void setBlob(const void* key, EGLsizei keySize, const void* value, + EGLsizei valueSize); + + // getBlob attempts to retrieve the value blob associated with a given key + // blob from cache. This will be called by the hardware vendor's EGL + // implementation via the EGL_ANDROID_blob_cache extension. + EGLsizei getBlob(const void* key, EGLsizei keySize, void* value, + EGLsizei valueSize); + +private: + // Creation and (the lack of) destruction is handled internally. + egl_cache_t(); + ~egl_cache_t(); + + // Copying is disallowed. + egl_cache_t(const egl_cache_t&); // not implemented + void operator=(const egl_cache_t&); // not implemented + + // getBlobCacheLocked returns the BlobCache object being used to store the + // key/value blob pairs. If the BlobCache object has not yet been created, + // this will do so, loading the serialized cache contents from disk if + // possible. + sp getBlobCacheLocked(); + + // saveBlobCache attempts to save the current contents of mBlobCache to + // disk. + void saveBlobCacheLocked(); + + // loadBlobCache attempts to load the saved cache contents from disk into + // mBlobCache. + void loadBlobCacheLocked(); + + // mInitialized indicates whether the egl_cache_t is in the initialized + // state. It is initialized to false at construction time, and gets set to + // true when initialize is called. It is set back to false when terminate + // is called. When in this state, the cache behaves as normal. When not, + // the getBlob and setBlob methods will return without performing any cache + // operations. + bool mInitialized; + + // mBlobCache is the cache in which the key/value blob pairs are stored. It + // is initially NULL, and will be initialized by getBlobCacheLocked the + // first time it's needed. + sp mBlobCache; + + // mMutex is the mutex used to prevent concurrent access to the member + // variables. It must be locked whenever the member variables are accessed. + mutable Mutex mMutex; }; // ---------------------------------------------------------------------------- }; // namespace android // ---------------------------------------------------------------------------- + +#endif // ANDROID_EGL_CACHE_H diff --git a/opengl/libs/EGL/egl_display.cpp b/opengl/libs/EGL/egl_display.cpp index 0f9286463..558ca77ff 100644 --- a/opengl/libs/EGL/egl_display.cpp +++ b/opengl/libs/EGL/egl_display.cpp @@ -44,6 +44,7 @@ egl_display_t::egl_display_t() : egl_display_t::~egl_display_t() { magic = 0; + egl_cache_t::get()->terminate(); } egl_display_t* egl_display_t::get(EGLDisplay dpy) { From 98c6383580f94bb7ff9cc9a7cc24d8b8519e484a Mon Sep 17 00:00:00 2001 From: Jamie Gennis Date: Mon, 7 Nov 2011 17:03:54 -0800 Subject: [PATCH 2/2] EGL: implement loading and saving the cache This change adds support for saving and loading the contents of the EGL cache. It also adds some simple tests for the EGL cache. Change-Id: I18e5e789e0897a0783d29d1c1e64d26de2dd44c4 --- opengl/libs/EGL/egl_cache.cpp | 164 +++++++++++++++++++++++- opengl/libs/EGL/egl_cache.h | 17 ++- opengl/libs/EGL/egl_display.h | 3 +- opengl/tests/EGLTest/Android.mk | 4 + opengl/tests/EGLTest/egl_cache_test.cpp | 110 ++++++++++++++++ 5 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 opengl/tests/EGLTest/egl_cache_test.cpp diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp index adb2a13d6..aa40d5888 100644 --- a/opengl/libs/EGL/egl_cache.cpp +++ b/opengl/libs/EGL/egl_cache.cpp @@ -19,11 +19,21 @@ #include "egl_impl.h" #include "egldefs.h" +#include +#include +#include +#include +#include + // Cache size limits. static const size_t maxKeySize = 1024; static const size_t maxValueSize = 4096; static const size_t maxTotalSize = 64 * 1024; +// Cache file header +static const char* cacheFileMagic = "EGL$"; +static const size_t cacheFileHeaderSize = 8; + // ---------------------------------------------------------------------------- namespace android { // ---------------------------------------------------------------------------- @@ -54,9 +64,10 @@ egl_cache_t::egl_cache_t() : egl_cache_t::~egl_cache_t() { } +egl_cache_t egl_cache_t::sCache; + egl_cache_t* egl_cache_t::get() { - static egl_cache_t theCache; - return &theCache; + return &sCache; } void egl_cache_t::initialize(egl_display_t *display) { @@ -136,6 +147,11 @@ EGLsizei egl_cache_t::getBlob(const void* key, EGLsizei keySize, void* value, return 0; } +void egl_cache_t::setCacheFilename(const char* filename) { + Mutex::Autolock lock(mMutex); + mFilename = filename; +} + sp egl_cache_t::getBlobCacheLocked() { if (mBlobCache == NULL) { mBlobCache = new BlobCache(maxKeySize, maxValueSize, maxTotalSize); @@ -144,10 +160,154 @@ sp egl_cache_t::getBlobCacheLocked() { return mBlobCache; } +static uint32_t crc32c(const uint8_t* buf, size_t len) { + const uint32_t polyBits = 0x82F63B78; + uint32_t r = 0; + for (size_t i = 0; i < len; i++) { + r ^= buf[i]; + for (int j = 0; j < 8; j++) { + if (r & 1) { + r = (r >> 1) ^ polyBits; + } else { + r >>= 1; + } + } + } + return r; +} + void egl_cache_t::saveBlobCacheLocked() { + if (mFilename.length() > 0) { + size_t cacheSize = mBlobCache->getFlattenedSize(); + size_t headerSize = cacheFileHeaderSize; + const char* fname = mFilename.string(); + + // Try to create the file with no permissions so we can write it + // without anyone trying to read it. + int fd = open(fname, O_CREAT | O_EXCL | O_RDWR, 0); + if (fd == -1) { + if (errno == EEXIST) { + // The file exists, delete it and try again. + if (unlink(fname) == -1) { + // No point in retrying if the unlink failed. + LOGE("error unlinking cache file %s: %s (%d)", fname, + strerror(errno), errno); + return; + } + // Retry now that we've unlinked the file. + fd = open(fname, O_CREAT | O_EXCL | O_RDWR, 0); + } + if (fd == -1) { + LOGE("error creating cache file %s: %s (%d)", fname, + strerror(errno), errno); + return; + } + } + + size_t fileSize = headerSize + cacheSize; + if (ftruncate(fd, fileSize) == -1) { + LOGE("error setting cache file size: %s (%d)", strerror(errno), + errno); + close(fd); + unlink(fname); + return; + } + + uint8_t* buf = reinterpret_cast(mmap(NULL, fileSize, + PROT_WRITE, MAP_SHARED, fd, 0)); + if (buf == MAP_FAILED) { + LOGE("error mmaping cache file: %s (%d)", strerror(errno), + errno); + close(fd); + unlink(fname); + return; + } + + status_t err = mBlobCache->flatten(buf + headerSize, cacheSize, NULL, + 0); + if (err != OK) { + LOGE("error writing cache contents: %s (%d)", strerror(-err), + -err); + munmap(buf, fileSize); + close(fd); + unlink(fname); + return; + } + + // Write the file magic and CRC + memcpy(buf, cacheFileMagic, 4); + uint32_t* crc = reinterpret_cast(buf + 4); + *crc = crc32c(buf + headerSize, cacheSize); + + munmap(buf, fileSize); + fchmod(fd, S_IRUSR); + close(fd); + } } void egl_cache_t::loadBlobCacheLocked() { + if (mFilename.length() > 0) { + size_t headerSize = cacheFileHeaderSize; + + int fd = open(mFilename.string(), O_RDONLY, 0); + if (fd == -1) { + if (errno != ENOENT) { + LOGE("error opening cache file %s: %s (%d)", mFilename.string(), + strerror(errno), errno); + } + return; + } + + struct stat statBuf; + if (fstat(fd, &statBuf) == -1) { + LOGE("error stat'ing cache file: %s (%d)", strerror(errno), errno); + close(fd); + return; + } + + // Sanity check the size before trying to mmap it. + size_t fileSize = statBuf.st_size; + if (fileSize > maxTotalSize * 2) { + LOGE("cache file is too large: %#llx", statBuf.st_size); + close(fd); + return; + } + + uint8_t* buf = reinterpret_cast(mmap(NULL, fileSize, + PROT_READ, MAP_PRIVATE, fd, 0)); + if (buf == MAP_FAILED) { + LOGE("error mmaping cache file: %s (%d)", strerror(errno), + errno); + close(fd); + return; + } + + // Check the file magic and CRC + size_t cacheSize = fileSize - headerSize; + if (memcmp(buf, cacheFileMagic, 4) != 0) { + LOGE("cache file has bad mojo"); + close(fd); + return; + } + uint32_t* crc = reinterpret_cast(buf + 4); + if (crc32c(buf + headerSize, cacheSize) != *crc) { + LOGE("cache file failed CRC check"); + close(fd); + return; + } + + status_t err = mBlobCache->unflatten(buf + headerSize, cacheSize, NULL, 0); + if (err != OK) { + LOGE("error reading cache contents: %s (%d)", strerror(-err), + -err); + munmap(buf, fileSize); + close(fd); + return; + } + + munmap(buf, fileSize); + close(fd); + } } // ---------------------------------------------------------------------------- diff --git a/opengl/libs/EGL/egl_cache.h b/opengl/libs/EGL/egl_cache.h index 70217528f..05d587318 100644 --- a/opengl/libs/EGL/egl_cache.h +++ b/opengl/libs/EGL/egl_cache.h @@ -21,6 +21,7 @@ #include #include +#include #include // ---------------------------------------------------------------------------- @@ -29,7 +30,7 @@ namespace android { class egl_display_t; -class egl_cache_t { +class EGLAPI egl_cache_t { public: // get returns a pointer to the singleton egl_cache_t object. This @@ -60,6 +61,10 @@ public: EGLsizei getBlob(const void* key, EGLsizei keySize, void* value, EGLsizei valueSize); + // setCacheFilename sets the name of the file that should be used to store + // cache contents from one program invocation to another. + void setCacheFilename(const char* filename); + private: // Creation and (the lack of) destruction is handled internally. egl_cache_t(); @@ -96,9 +101,19 @@ private: // first time it's needed. sp mBlobCache; + // mFilename is the name of the file for storing cache contents in between + // program invocations. It is initialized to an empty string at + // construction time, and can be set with the setCacheFilename method. An + // empty string indicates that the cache should not be saved to or restored + // from disk. + String8 mFilename; + // mMutex is the mutex used to prevent concurrent access to the member // variables. It must be locked whenever the member variables are accessed. mutable Mutex mMutex; + + // sCache is the singleton egl_cache_t object. + static egl_cache_t sCache; }; // ---------------------------------------------------------------------------- diff --git a/opengl/libs/EGL/egl_display.h b/opengl/libs/EGL/egl_display.h index 113595f17..1c1092cd4 100644 --- a/opengl/libs/EGL/egl_display.h +++ b/opengl/libs/EGL/egl_display.h @@ -59,7 +59,7 @@ struct egl_config_t { // ---------------------------------------------------------------------------- -class egl_display_t { +class EGLAPI egl_display_t { // marked as EGLAPI for testing purposes static egl_display_t sDisplay[NUM_DISPLAYS]; EGLDisplay getDisplay(EGLNativeDisplayType display); @@ -141,4 +141,3 @@ EGLBoolean validate_display_surface(EGLDisplay dpy, EGLSurface surface); // ---------------------------------------------------------------------------- #endif // ANDROID_EGL_DISPLAY_H - diff --git a/opengl/tests/EGLTest/Android.mk b/opengl/tests/EGLTest/Android.mk index 92d7eb12a..14104d171 100644 --- a/opengl/tests/EGLTest/Android.mk +++ b/opengl/tests/EGLTest/Android.mk @@ -7,6 +7,7 @@ LOCAL_MODULE := EGL_test LOCAL_MODULE_TAGS := tests LOCAL_SRC_FILES := \ + egl_cache_test.cpp \ EGL_test.cpp \ LOCAL_SHARED_LIBRARIES := \ @@ -21,9 +22,12 @@ LOCAL_STATIC_LIBRARIES := \ LOCAL_C_INCLUDES := \ bionic \ + bionic/libc/private \ bionic/libstdc++/include \ external/gtest/include \ external/stlport/stlport \ + frameworks/base/opengl/libs \ + frameworks/base/opengl/libs/EGL \ include $(BUILD_EXECUTABLE) diff --git a/opengl/tests/EGLTest/egl_cache_test.cpp b/opengl/tests/EGLTest/egl_cache_test.cpp new file mode 100644 index 000000000..c7d9e3e20 --- /dev/null +++ b/opengl/tests/EGLTest/egl_cache_test.cpp @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2011 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_TAG "EGL_test" +//#define LOG_NDEBUG 0 + +#include + +#include + +#include "egl_cache.h" +#include "egl_display.h" + +namespace android { + +class EGLCacheTest : public ::testing::Test { +protected: + virtual void SetUp() { + mCache = egl_cache_t::get(); + } + + virtual void TearDown() { + mCache->setCacheFilename(""); + mCache->terminate(); + } + + egl_cache_t* mCache; +}; + +TEST_F(EGLCacheTest, UninitializedCacheAlwaysMisses) { + char buf[4] = { 0xee, 0xee, 0xee, 0xee }; + mCache->setBlob("abcd", 4, "efgh", 4); + ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf, 4)); + ASSERT_EQ(0xee, buf[0]); + ASSERT_EQ(0xee, buf[1]); + ASSERT_EQ(0xee, buf[2]); + ASSERT_EQ(0xee, buf[3]); +} + +TEST_F(EGLCacheTest, InitializedCacheAlwaysHits) { + char buf[4] = { 0xee, 0xee, 0xee, 0xee }; + mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); + mCache->setBlob("abcd", 4, "efgh", 4); + ASSERT_EQ(4, mCache->getBlob("abcd", 4, buf, 4)); + ASSERT_EQ('e', buf[0]); + ASSERT_EQ('f', buf[1]); + ASSERT_EQ('g', buf[2]); + ASSERT_EQ('h', buf[3]); +} + +TEST_F(EGLCacheTest, TerminatedCacheAlwaysMisses) { + char buf[4] = { 0xee, 0xee, 0xee, 0xee }; + mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); + mCache->setBlob("abcd", 4, "efgh", 4); + mCache->terminate(); + ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf, 4)); + ASSERT_EQ(0xee, buf[0]); + ASSERT_EQ(0xee, buf[1]); + ASSERT_EQ(0xee, buf[2]); + ASSERT_EQ(0xee, buf[3]); +} + +class EGLCacheSerializationTest : public EGLCacheTest { + +protected: + + virtual void SetUp() { + EGLCacheTest::SetUp(); + + char* tn = tempnam("/sdcard", "EGL_test-cache-"); + mFilename = tn; + free(tn); + } + + virtual void TearDown() { + unlink(mFilename.string()); + EGLCacheTest::TearDown(); + } + + String8 mFilename; +}; + +TEST_F(EGLCacheSerializationTest, ReinitializedCacheContainsValues) { + char buf[4] = { 0xee, 0xee, 0xee, 0xee }; + mCache->setCacheFilename(mFilename); + mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); + mCache->setBlob("abcd", 4, "efgh", 4); + mCache->terminate(); + mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY)); + ASSERT_EQ(4, mCache->getBlob("abcd", 4, buf, 4)); + ASSERT_EQ('e', buf[0]); + ASSERT_EQ('f', buf[1]); + ASSERT_EQ('g', buf[2]); + ASSERT_EQ('h', buf[3]); +} + +}