From 1224807f503cf6aaef33388af682f754dcbbc3df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Ekl=C3=B6f?= Date: Sun, 22 Mar 2020 20:06:44 +0100 Subject: [PATCH] shm: new function, shm_scroll() This function "scrolls" the buffer by the specified number of (pixel) rows. The idea is move the image offset by re-sizing the underlying memfd object. I.e. to scroll forward, increase the size of the memfd file, and move the pixman image offset forward (and the Wayland SHM buffer as well). Only increasing the file size would, obviously, cause the memfd file to grow indefinitely. To deal with this, we "punch" a whole from the beginning of the file to the new offset. This frees the associated memory. Thus, while we have a memfd file whose size is (as seen by e.g. fstat()) is ever growing, the actual file size is always the original buffer size. Some notes: * FALLOC_FL_PUNCH_HOLE can be quite slow when the number of used pages to drop is large. * all normal fallocate() usages have been replaced with ftruncate(), as this is *much* faster. fallocate() guarantees subsequent writes wont fail. I.e. it actually reserves (disk) space. While it doesn't allocate on-disk blocks for on-disk files, it *does* zero-initialize the in-memory blocks. And this is slow. ftruncate() doesn't do this. TODO: implement reverse scrolling (i.e. a negative row count). --- shm.c | 267 +++++++++++++++++++++++++++++++++++++++++++--------------- shm.h | 15 +++- 2 files changed, 210 insertions(+), 72 deletions(-) diff --git a/shm.c b/shm.c index a4fc8797..a96bda4f 100644 --- a/shm.c +++ b/shm.c @@ -1,11 +1,13 @@ #include "shm.h" #include +#include #include #include #include #include +#include #include #include #include @@ -19,6 +21,8 @@ #define LOG_ENABLE_DBG 0 #include "log.h" +#define TIME_SCROLL 0 + static tll(struct buffer) buffers; static void @@ -26,14 +30,19 @@ buffer_destroy(struct buffer *buf) { if (buf->pix != NULL) pixman_image_unref(buf->pix); - wl_buffer_destroy(buf->wl_buf); - munmap(buf->mmapped, buf->size); + if (buf->wl_buf != NULL) + wl_buffer_destroy(buf->wl_buf); + if (buf->real_mmapped != MAP_FAILED) + munmap(buf->real_mmapped, buf->mmap_size); + if (buf->fd >= 0) + close(buf->fd); } static void buffer_release(void *data, struct wl_buffer *wl_buffer) { struct buffer *buffer = data; + LOG_DBG("release: cookie=%lx (buf=%p)", buffer->cookie, buffer); assert(buffer->wl_buf == wl_buffer); assert(buffer->busy); buffer->busy = false; @@ -43,6 +52,99 @@ static const struct wl_buffer_listener buffer_listener = { .release = &buffer_release, }; +static bool +instantiate_offset(struct wl_shm *shm, struct buffer *buf, size_t new_offset) +{ + assert(buf->fd >= 0); + assert(buf->mmapped == NULL); + assert(buf->real_mmapped == NULL); + assert(buf->wl_buf == NULL); + assert(buf->pix == NULL); + + assert(new_offset >= buf->offset); + + static size_t page_size = 0; + if (page_size == 0) { + page_size = sysconf(_SC_PAGE_SIZE); + if (page_size < 0) { + LOG_ERRNO("failed to get page size"); + page_size = 4096; + } + } + assert(page_size > 0); + + void *real_mmapped = MAP_FAILED; + void *mmapped = MAP_FAILED; + struct wl_shm_pool *pool = NULL; + struct wl_buffer *wl_buf = NULL; + pixman_image_t *pix = NULL; + + /* mmap offset must be page aligned */ + size_t aligned_offset = new_offset & ~(page_size - 1); + size_t page_offset = new_offset & (page_size - 1); + size_t mmap_size = buf->size + page_offset; + + assert(aligned_offset <= new_offset); + assert(mmap_size >= buf->size); + + LOG_DBG("size=%zx, offset=%zx, size-aligned=%zx, offset-aligned=%zx", + buf->size, buf->offset, mmap_size, aligned_offset); + + real_mmapped = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_UNINITIALIZED, buf->fd, aligned_offset); + if (real_mmapped == MAP_FAILED) { + LOG_ERRNO("failed to mmap SHM backing memory file"); + goto err; + } + mmapped = real_mmapped + page_offset; + + pool = wl_shm_create_pool(shm, buf->fd, new_offset + buf->size); + if (pool == NULL) { + LOG_ERR("failed to create SHM pool"); + goto err; + } + + wl_buf = wl_shm_pool_create_buffer( + pool, new_offset, buf->width, buf->height, buf->stride, WL_SHM_FORMAT_ARGB8888); + if (wl_buf == NULL) { + LOG_ERR("failed to create SHM buffer"); + goto err; + } + + /* We use the entire pool for our single buffer */ + wl_shm_pool_destroy(pool); pool = NULL; + + /* One pixman image for each worker thread (do we really need multiple?) */ + pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, buf->width, buf->height, (uint32_t *)mmapped, buf->stride); + if (pix == NULL) { + LOG_ERR("failed to create pixman image"); + goto err; + } + + buf->offset = new_offset; + buf->real_mmapped = real_mmapped; + buf->mmapped = mmapped; + buf->mmap_size = mmap_size; + buf->wl_buf = wl_buf; + buf->pix = pix; + + wl_buffer_add_listener(wl_buf, &buffer_listener, buf); + return true; + +err: + if (pix != NULL) + pixman_image_unref(pix); + if (wl_buf != NULL) + wl_buffer_destroy(wl_buf); + if (pool != NULL) + wl_shm_pool_destroy(pool); + if (real_mmapped != MAP_FAILED) + munmap(real_mmapped, mmap_size); + + abort(); + return false; +} + struct buffer * shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) { @@ -73,7 +175,8 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) continue; if (!it->item.busy) { - LOG_DBG("cookie=%lx: re-using buffer from cache", cookie); + LOG_DBG("cookie=%lx: re-using buffer from cache (buf=%p)", + cookie, &it->item); it->item.busy = true; it->item.purge = false; return &it->item; @@ -106,13 +209,10 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) */ int pool_fd = -1; - void *mmapped = MAP_FAILED; - size_t size = 0; + const int stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); + const size_t size = stride * height; - struct wl_shm_pool *pool = NULL; - struct wl_buffer *buf = NULL; - - pixman_image_t *pix = NULL; + LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); /* Backing memory for SHM */ pool_fd = memfd_create("foot-wayland-shm-buffer-pool", MFD_CLOEXEC); @@ -121,56 +221,11 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) goto err; } - const int stride = stride_for_format_and_width(PIXMAN_a8r8g8b8, width); - - /* Total size */ - size = stride * height; - LOG_DBG("cookie=%lx: allocating new buffer: %zu KB", cookie, size / 1024); - - int err = EINTR; - while (err == EINTR) - err = posix_fallocate(pool_fd, 0, size); - if (err != 0) { - static bool failure_logged = false; - - if (!failure_logged) { - failure_logged = true; - LOG_ERRNO_P("failed to fallocate %zu bytes", err, size); - } - - if (ftruncate(pool_fd, size) == -1) { - LOG_ERRNO("failed to truncate SHM pool"); - goto err; - } - } - - mmapped = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_UNINITIALIZED, pool_fd, 0); - if (mmapped == MAP_FAILED) { - LOG_ERRNO("failed to mmap SHM backing memory file"); + if (ftruncate(pool_fd, size) == -1) { + LOG_ERRNO("failed to truncate SHM pool"); goto err; } - pool = wl_shm_create_pool(shm, pool_fd, size); - if (pool == NULL) { - LOG_ERR("failed to create SHM pool"); - goto err; - } - - buf = wl_shm_pool_create_buffer( - pool, 0, width, height, stride, WL_SHM_FORMAT_ARGB8888); - if (buf == NULL) { - LOG_ERR("failed to create SHM buffer"); - goto err; - } - - /* We use the entire pool for our single buffer */ - wl_shm_pool_destroy(pool); pool = NULL; - close(pool_fd); pool_fd = -1; - - /* One pixman image for each worker thread (do we really need multiple?) */ - pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, width, height, (uint32_t *)mmapped, stride); - /* Push to list of available buffers, but marked as 'busy' */ tll_push_back( buffers, @@ -181,27 +236,20 @@ shm_get_buffer(struct wl_shm *shm, int width, int height, unsigned long cookie) .stride = stride, .busy = true, .size = size, - .mmapped = mmapped, - .wl_buf = buf, - .pix = pix} + .fd = pool_fd, + .mmap_size = size, + .offset = 0} ) ); struct buffer *ret = &tll_back(buffers); - wl_buffer_add_listener(ret->wl_buf, &buffer_listener, ret); + if (!instantiate_offset(shm, ret, 0)) + goto err; return ret; err: - if (pix != NULL) - pixman_image_unref(pix); - if (buf != NULL) - wl_buffer_destroy(buf); - if (pool != NULL) - wl_shm_pool_destroy(pool); if (pool_fd != -1) close(pool_fd); - if (mmapped != MAP_FAILED) - munmap(mmapped, size); /* We don't handle this */ abort(); @@ -217,6 +265,89 @@ shm_fini(void) } } +bool +shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows) +{ + assert(buf->busy); + assert(buf->pix); + assert(buf->wl_buf); + assert(buf->real_mmapped); + assert(buf->fd >= 0); + + LOG_DBG("scrolling %d rows (%d bytes)", rows, rows * buf->stride); + + assert(rows > 0); + assert(rows * buf->stride < buf->size); + const size_t new_offset = buf->offset + rows * buf->stride; + +#if TIME_SCROLL + struct timeval time0; + gettimeofday(&time0, NULL); +#endif + + /* Increase file size */ + if (ftruncate(buf->fd, new_offset + buf->size) < 0) { + LOG_ERRNO("failed increase memfd size from %zu -> %zu", + buf->offset + buf->size, new_offset + buf->size); + + /* Instantiate a new buffer and copy over our content */ + struct buffer *fresh_buf = shm_get_buffer(shm, buf->width, buf->height, buf->cookie); + + memcpy(fresh_buf->mmapped, + (const uint8_t *)buf->mmapped + rows * buf->stride, + buf->size - rows * buf->stride); + + buffer_destroy(buf); + *buf = *fresh_buf; + + assert(false); + + /* Mark copied buffer for deletion, but make sure we don't + * free any of its resources */ + fresh_buf->pix = NULL; + fresh_buf->wl_buf = NULL; + fresh_buf->real_mmapped = MAP_FAILED; + fresh_buf->fd = -1; + fresh_buf->purge = true; + return true; + } + +#if TIME_SCROLL + struct timeval time1; + gettimeofday(&time1, NULL); + + struct timeval tot; + timersub(&time1, &time0, &tot); + LOG_INFO("fallocate: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + /* Destroy old objects (they point to the old offset) */ + pixman_image_unref(buf->pix); + wl_buffer_destroy(buf->wl_buf); + munmap(buf->real_mmapped, buf->mmap_size); + + buf->pix = NULL; + buf->wl_buf = NULL; + buf->real_mmapped = buf->mmapped = NULL; + + /* Free unused memory */ + if (fallocate(buf->fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, new_offset) < 0) { + LOG_ERRNO( + "fallocate(FALLOC_FL_PUNCH_HOLE) not " + "supported: expect lower performance"); + abort(); + } + +#if TIME_SCROLL + struct timeval time2; + gettimeofday(&time2, NULL); + timersub(&time2, &time1, &tot); + LOG_INFO("PUNCH HOLE: %lds %ldus", tot.tv_sec, tot.tv_usec); +#endif + + return instantiate_offset(shm, buf, new_offset); +} + void shm_purge(struct wl_shm *shm, unsigned long cookie) { diff --git a/shm.h b/shm.h index eb9987ed..2b3a9b19 100644 --- a/shm.h +++ b/shm.h @@ -13,20 +13,27 @@ struct buffer { int height; int stride; - bool purge; - bool busy; - size_t size; - void *mmapped; + size_t size; /* Buffer size */ + void *mmapped; /* Raw data */ struct wl_buffer *wl_buf; pixman_image_t *pix; + + /* Internal */ + int fd; /* memfd */ + void *real_mmapped; /* Address returned from mmap */ + size_t mmap_size; /* Size of mmap (>= size) */ + size_t offset; /* Offset into memfd where data begins */ + bool purge; /* True if this buffer should be destroyed */ }; struct buffer *shm_get_buffer( struct wl_shm *shm, int width, int height, unsigned long cookie); void shm_fini(void); +bool shm_scroll(struct wl_shm *shm, struct buffer *buf, int rows); + void shm_purge(struct wl_shm *shm, unsigned long cookie); struct terminal;