/* Cairo - a vector graphics library with display and print output
 *
 * Copyright © 2007 Chris Wilson
 * Copyright © 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it either under the terms of the GNU Lesser General Public
 * License version 2.1 as published by the Free Software Foundation
 * (the "LGPL") or, at your option, under the terms of the Mozilla
 * Public License Version 1.1 (the "MPL"). If you do not alter this
 * notice, a recipient may use your version of this file under either
 * the MPL or the LGPL.
 *
 * You should have received a copy of the LGPL along with this library
 * in the file COPYING-LGPL-2.1; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA
 * You should have received a copy of the MPL along with this library
 * in the file COPYING-MPL-1.1
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (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.mozilla.org/MPL/
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY
 * OF ANY KIND, either express or implied. See the LGPL or the MPL for
 * the specific language governing rights and limitations.
 *
 * The Original Code is the cairo graphics library.
 *
 * The Initial Developer of the Original Code is Red Hat, Inc.
 *
 * Contributors(s):
 *	Chris Wilson <chris@chris-wilson.co.uk>
 */

#include "cairoint.h"

#if CAIRO_HAS_XCB_SHM_FUNCTIONS

#include "cairo-xcb-private.h"
#include "cairo-list-inline.h"
#include "cairo-mempool-private.h"

#include <xcb/shm.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>

#define CAIRO_MAX_SHM_MEMORY (16*1024*1024)

/* a simple buddy allocator for memory pools
 * XXX fragmentation? use Doug Lea's malloc?
 */

typedef struct _cairo_xcb_shm_mem_block cairo_xcb_shm_mem_block_t;

typedef enum {
    PENDING_WAIT,
    PENDING_POLL
} shm_wait_type_t;

struct _cairo_xcb_shm_mem_pool {
    int shmid;
    uint32_t shmseg;
    void *shm;

    cairo_mempool_t mem;

    cairo_list_t link;
};

static void
_cairo_xcb_shm_mem_pool_destroy (cairo_xcb_shm_mem_pool_t *pool)
{
    cairo_list_del (&pool->link);

    shmdt (pool->shm);
    _cairo_mempool_fini (&pool->mem);

    free (pool);
}

static void
_cairo_xcb_shm_info_finalize (cairo_xcb_shm_info_t *shm_info)
{
    cairo_xcb_connection_t *connection = shm_info->connection;

    assert (CAIRO_MUTEX_IS_LOCKED (connection->shm_mutex));

    _cairo_mempool_free (&shm_info->pool->mem, shm_info->mem);
    _cairo_freepool_free (&connection->shm_info_freelist, shm_info);

    /* scan for old, unused pools - hold at least one in reserve */
    if (! cairo_list_is_singular (&connection->shm_pools))
    {
	cairo_xcb_shm_mem_pool_t *pool, *next;
	cairo_list_t head;

	cairo_list_init (&head);
	cairo_list_move (connection->shm_pools.next, &head);

	cairo_list_foreach_entry_safe (pool, next, cairo_xcb_shm_mem_pool_t,
				       &connection->shm_pools, link)
	{
	    if (pool->mem.free_bytes == pool->mem.max_bytes) {
		_cairo_xcb_connection_shm_detach (connection, pool->shmseg);
		_cairo_xcb_shm_mem_pool_destroy (pool);
	    }
	}

	cairo_list_move (head.next, &connection->shm_pools);
    }
}

static void
_cairo_xcb_shm_process_pending (cairo_xcb_connection_t *connection, shm_wait_type_t wait)
{
    cairo_xcb_shm_info_t *info, *next;
    xcb_get_input_focus_reply_t *reply;

    assert (CAIRO_MUTEX_IS_LOCKED (connection->shm_mutex));
    cairo_list_foreach_entry_safe (info, next, cairo_xcb_shm_info_t,
				   &connection->shm_pending, pending)
    {
	switch (wait) {
	case PENDING_WAIT:
	     reply = xcb_wait_for_reply (connection->xcb_connection,
					 info->sync.sequence, NULL);
	     break;
	case PENDING_POLL:
	    if (! xcb_poll_for_reply (connection->xcb_connection,
				      info->sync.sequence,
				      (void **) &reply, NULL))
		/* We cannot be sure the server finished with this image yet, so
		 * try again later. All other shm info are guaranteed to have a
		 * larger sequence number and thus don't have to be checked. */
		return;
	    break;
	default:
	    /* silence Clang static analyzer warning */
	    ASSERT_NOT_REACHED;
	    reply = NULL;
	}

	free (reply);
	cairo_list_del (&info->pending);
	_cairo_xcb_shm_info_finalize (info);
    }
}

cairo_int_status_t
_cairo_xcb_connection_allocate_shm_info (cairo_xcb_connection_t *connection,
					 size_t size,
					 cairo_bool_t might_reuse,
					 cairo_xcb_shm_info_t **shm_info_out)
{
    cairo_xcb_shm_info_t *shm_info;
    cairo_xcb_shm_mem_pool_t *pool, *next;
    size_t bytes, maxbits = 16, minbits = 8;
    size_t shm_allocated = 0;
    void *mem = NULL;
    cairo_status_t status;

    assert (connection->flags & CAIRO_XCB_HAS_SHM);

    CAIRO_MUTEX_LOCK (connection->shm_mutex);
    _cairo_xcb_shm_process_pending (connection, PENDING_POLL);

    if (might_reuse) {
	cairo_list_foreach_entry (shm_info, cairo_xcb_shm_info_t,
		&connection->shm_pending, pending) {
	    if (shm_info->size >= size) {
		cairo_list_del (&shm_info->pending);
		CAIRO_MUTEX_UNLOCK (connection->shm_mutex);

		xcb_discard_reply (connection->xcb_connection, shm_info->sync.sequence);
		shm_info->sync.sequence = XCB_NONE;

		*shm_info_out = shm_info;
		return CAIRO_STATUS_SUCCESS;
	    }
	}
    }

    cairo_list_foreach_entry_safe (pool, next, cairo_xcb_shm_mem_pool_t,
				   &connection->shm_pools, link)
    {
	if (pool->mem.free_bytes > size) {
	    mem = _cairo_mempool_alloc (&pool->mem, size);
	    if (mem != NULL) {
		/* keep the active pools towards the front */
		cairo_list_move (&pool->link, &connection->shm_pools);
		goto allocate_shm_info;
	    }
	}
	/* scan for old, unused pools */
	if (pool->mem.free_bytes == pool->mem.max_bytes) {
	    _cairo_xcb_connection_shm_detach (connection,
					      pool->shmseg);
	    _cairo_xcb_shm_mem_pool_destroy (pool);
	} else {
	    shm_allocated += pool->mem.max_bytes;
	}
    }

    if (unlikely (shm_allocated >= CAIRO_MAX_SHM_MEMORY)) {
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return _cairo_error (CAIRO_STATUS_NO_MEMORY);
    }

    pool = malloc (sizeof (cairo_xcb_shm_mem_pool_t));
    if (unlikely (pool == NULL)) {
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return _cairo_error (CAIRO_STATUS_NO_MEMORY);
    }

    bytes = 1 << maxbits;
    while (bytes <= size)
	bytes <<= 1, maxbits++;
    bytes <<= 3;

    do {
	pool->shmid = shmget (IPC_PRIVATE, bytes, IPC_CREAT | 0600);
	if (pool->shmid != -1)
	    break;

	/* If the allocation failed because we asked for too much memory, we try
	 * again with a smaller request, as long as our allocation still fits. */
	bytes >>= 1;
	if (errno != EINVAL || bytes < size)
	    break;
    } while (TRUE);
    if (pool->shmid == -1) {
	int err = errno;
	if (! (err == EINVAL || err == ENOMEM))
	    connection->flags &= ~CAIRO_XCB_HAS_SHM;
	free (pool);
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return CAIRO_INT_STATUS_UNSUPPORTED;
    }

    pool->shm = shmat (pool->shmid, NULL, 0);
    if (unlikely (pool->shm == (char *) -1)) {
	shmctl (pool->shmid, IPC_RMID, NULL);
	free (pool);
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return _cairo_error (CAIRO_STATUS_NO_MEMORY);
    }

    status = _cairo_mempool_init (&pool->mem, pool->shm, bytes,
				  minbits, maxbits - minbits + 1);
    if (unlikely (status)) {
	shmdt (pool->shm);
	free (pool);
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return status;
    }

    pool->shmseg = _cairo_xcb_connection_shm_attach (connection, pool->shmid, FALSE);
    shmctl (pool->shmid, IPC_RMID, NULL);

    cairo_list_add (&pool->link, &connection->shm_pools);
    mem = _cairo_mempool_alloc (&pool->mem, size);

  allocate_shm_info:
    shm_info = _cairo_freepool_alloc (&connection->shm_info_freelist);
    if (unlikely (shm_info == NULL)) {
	_cairo_mempool_free (&pool->mem, mem);
	CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
	return _cairo_error (CAIRO_STATUS_NO_MEMORY);
    }

    shm_info->connection = connection;
    shm_info->pool = pool;
    shm_info->shm = pool->shmseg;
    shm_info->size = size;
    shm_info->offset = (char *) mem - (char *) pool->shm;
    shm_info->mem = mem;
    shm_info->sync.sequence = XCB_NONE;

    /* scan for old, unused pools */
    cairo_list_foreach_entry_safe (pool, next, cairo_xcb_shm_mem_pool_t,
				   &connection->shm_pools, link)
    {
	if (pool->mem.free_bytes == pool->mem.max_bytes) {
	    _cairo_xcb_connection_shm_detach (connection,
					      pool->shmseg);
	    _cairo_xcb_shm_mem_pool_destroy (pool);
	}
    }
    CAIRO_MUTEX_UNLOCK (connection->shm_mutex);

    *shm_info_out = shm_info;
    return CAIRO_STATUS_SUCCESS;
}

void
_cairo_xcb_shm_info_destroy (cairo_xcb_shm_info_t *shm_info)
{
    cairo_xcb_connection_t *connection = shm_info->connection;

    /* We can only return shm_info->mem to the allocator when we can be sure
     * that the X server no longer reads from it. Since the X server processes
     * requests in order, we send a GetInputFocus here.
     * _cairo_xcb_shm_process_pending () will later check if the reply for that
     * request was received and then actually mark this memory area as free. */

    CAIRO_MUTEX_LOCK (connection->shm_mutex);
    assert (shm_info->sync.sequence == XCB_NONE);
    shm_info->sync = xcb_get_input_focus (connection->xcb_connection);

    cairo_list_init (&shm_info->pending);
    cairo_list_add_tail (&shm_info->pending, &connection->shm_pending);
    CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
}

void
_cairo_xcb_connection_shm_mem_pools_flush (cairo_xcb_connection_t *connection)
{
    CAIRO_MUTEX_LOCK (connection->shm_mutex);
    _cairo_xcb_shm_process_pending (connection, PENDING_WAIT);
    CAIRO_MUTEX_UNLOCK (connection->shm_mutex);
}

void
_cairo_xcb_connection_shm_mem_pools_fini (cairo_xcb_connection_t *connection)
{
    assert (cairo_list_is_empty (&connection->shm_pending));
    while (! cairo_list_is_empty (&connection->shm_pools)) {
	_cairo_xcb_shm_mem_pool_destroy (cairo_list_first_entry (&connection->shm_pools,
								 cairo_xcb_shm_mem_pool_t,
								 link));
    }
}

#endif /* CAIRO_HAS_XCB_SHM_FUNCTIONS */
