diff --git a/Makefile.common b/Makefile.common index 013fd9ce7d..0fab3ee1d8 100644 --- a/Makefile.common +++ b/Makefile.common @@ -1681,7 +1681,8 @@ endif ifeq ($(HAVE_ZLIB_COMMON), 1) OBJ += $(LIBRETRO_COMM_DIR)/file/archive_file_zlib.o \ - $(LIBRETRO_COMM_DIR)/streams/trans_stream_zlib.o + $(LIBRETRO_COMM_DIR)/streams/trans_stream_zlib.o \ + $(LIBRETRO_COMM_DIR)/streams/rzip_stream.o DEFINES += -DHAVE_ZLIB HAVE_COMPRESSION = 1 diff --git a/config.def.h b/config.def.h index a7b643efec..24c8e8866c 100644 --- a/config.def.h +++ b/config.def.h @@ -911,6 +911,10 @@ static const bool savestate_auto_load = false; static const bool savestate_thumbnail_enable = false; +/* When creating save state files, compress + * written data */ +#define DEFAULT_SAVESTATE_FILE_COMPRESSION false + /* Slowmotion ratio. */ #define DEFAULT_SLOWMOTION_RATIO 3.0 diff --git a/configuration.c b/configuration.c index d3e5a6585a..78b08b9d9e 100644 --- a/configuration.c +++ b/configuration.c @@ -1614,6 +1614,7 @@ static struct config_bool_setting *populate_settings_bool(settings_t *settings, SETTING_BOOL("savestate_auto_save", &settings->bools.savestate_auto_save, true, savestate_auto_save, false); SETTING_BOOL("savestate_auto_load", &settings->bools.savestate_auto_load, true, savestate_auto_load, false); SETTING_BOOL("savestate_thumbnail_enable", &settings->bools.savestate_thumbnail_enable, true, savestate_thumbnail_enable, false); + SETTING_BOOL("savestate_file_compression", &settings->bools.savestate_file_compression, true, DEFAULT_SAVESTATE_FILE_COMPRESSION, false); SETTING_BOOL("history_list_enable", &settings->bools.history_list_enable, true, DEFAULT_HISTORY_LIST_ENABLE, false); SETTING_BOOL("playlist_entry_rename", &settings->bools.playlist_entry_rename, true, DEFAULT_PLAYLIST_ENTRY_RENAME, false); SETTING_BOOL("game_specific_options", &settings->bools.game_specific_options, true, default_game_specific_options, false); diff --git a/configuration.h b/configuration.h index a86052f2f1..2f69dfd785 100644 --- a/configuration.h +++ b/configuration.h @@ -344,6 +344,7 @@ typedef struct settings bool savestate_auto_save; bool savestate_auto_load; bool savestate_thumbnail_enable; + bool savestate_file_compression; bool network_cmd_enable; bool stdin_cmd_enable; bool keymapper_enable; diff --git a/griffin/griffin.c b/griffin/griffin.c index b03ba37afb..b002383690 100644 --- a/griffin/griffin.c +++ b/griffin/griffin.c @@ -128,6 +128,7 @@ COMPRESSION #ifdef HAVE_ZLIB #include "../libretro-common/streams/trans_stream_zlib.c" +#include "../libretro-common/streams/rzip_stream.c" #endif /*============================================================ diff --git a/intl/msg_hash_lbl.h b/intl/msg_hash_lbl.h index 5f5a46425b..adf86be403 100644 --- a/intl/msg_hash_lbl.h +++ b/intl/msg_hash_lbl.h @@ -1200,6 +1200,8 @@ MSG_HASH(MENU_ENUM_LABEL_SAVESTATE_AUTO_LOAD, "savestate_auto_load") MSG_HASH(MENU_ENUM_LABEL_SAVESTATE_THUMBNAIL_ENABLE, "savestate_thumbnails") +MSG_HASH(MENU_ENUM_LABEL_SAVESTATE_FILE_COMPRESSION, + "savestate_file_compression") MSG_HASH(MENU_ENUM_LABEL_SAVESTATE_AUTO_SAVE, "savestate_auto_save") MSG_HASH(MENU_ENUM_LABEL_SAVESTATE_DIRECTORY, diff --git a/intl/msg_hash_us.h b/intl/msg_hash_us.h index 0dd655d831..22c5669ddb 100644 --- a/intl/msg_hash_us.h +++ b/intl/msg_hash_us.h @@ -2996,6 +2996,14 @@ MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVESTATE_THUMBNAIL_ENABLE, "Savestate Thumbnails" ) +MSG_HASH( + MENU_ENUM_LABEL_VALUE_SAVESTATE_FILE_COMPRESSION, + "Savestate Compression" + ) +MSG_HASH( + MENU_ENUM_SUBLABEL_SAVESTATE_FILE_COMPRESSION, + "Write save state files in an archived format. Dramatically reduces file size at the expense of increased saving/loading times." + ) MSG_HASH( MENU_ENUM_LABEL_VALUE_SAVE_CURRENT_CONFIG, "Save Current Configuration" diff --git a/libretro-common/include/streams/interface_stream.h b/libretro-common/include/streams/interface_stream.h index 858ec950fd..813b3551cf 100644 --- a/libretro-common/include/streams/interface_stream.h +++ b/libretro-common/include/streams/interface_stream.h @@ -36,7 +36,8 @@ enum intfstream_type { INTFSTREAM_FILE = 0, INTFSTREAM_MEMORY, - INTFSTREAM_CHD + INTFSTREAM_CHD, + INTFSTREAM_RZIP }; typedef struct intfstream_internal intfstream_internal_t, intfstream_t; @@ -112,6 +113,9 @@ intfstream_t *intfstream_open_writable_memory(void *data, intfstream_t *intfstream_open_chd_track(const char *path, unsigned mode, unsigned hints, int32_t track); +intfstream_t* intfstream_open_rzip_file(const char *path, + unsigned mode); + RETRO_END_DECLS #endif diff --git a/libretro-common/include/streams/rzip_stream.h b/libretro-common/include/streams/rzip_stream.h new file mode 100644 index 0000000000..8b53033516 --- /dev/null +++ b/libretro-common/include/streams/rzip_stream.h @@ -0,0 +1,122 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (rzip_stream.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef _LIBRETRO_SDK_FILE_RZIP_STREAM_H +#define _LIBRETRO_SDK_FILE_RZIP_STREAM_H + +#include +#include +#include + +#include + +RETRO_BEGIN_DECLS + +/* Rudimentary interface for streaming data to/from a + * zlib-compressed chunk-based RZIP archive file. + * + * This is somewhat less efficient than using regular + * gzip code, but this is by design - the intention here + * is to create an interface that integrates seamlessly + * with normal RetroArch functionality, using only + * standard/existing libretro-common routines. + * (Actual efficiency is pretty good, regardless: + * archived file size is almost identical to a solid + * zip file, and compression/decompression speed is + * not substantially worse than external archiving tools; + * it is certainly acceptable for use in real-time + * frontend applications) + * + * When reading existing files, uncompressed content + * is handled automatically. File type (compressed/ + * uncompressed) is detected via the RZIP header. + * + * ## RZIP file format: + * + * : 8 bytes + * - [#][R][Z][I][P][v][file format version][#] + * : 4 bytes, little endian order + * - nominal (maximum) size of each uncompressed + * chunk, in bytes + * : 8 bytes, little endian order + * : 4 bytes, little endian order + * - size on-disk of next compressed data + * chunk, in bytes + * : n bytes of zlib compressed data + * ... + * : repeated until end of file + * : + * + */ + +/* Prevent direct access to rzipstream_t members */ +typedef struct rzipstream rzipstream_t; + +/* File Open */ + +/* Opens a new or existing RZIP file + * > Supported 'mode' values are: + * - RETRO_VFS_FILE_ACCESS_READ + * - RETRO_VFS_FILE_ACCESS_WRITE + * > When reading, 'path' may reference compressed + * or uncompressed data + * Returns NULL if arguments are invalid, file + * is invalid or an IO error occurs */ +rzipstream_t* rzipstream_open(const char *path, unsigned mode); + +/* File Read */ + +/* Reads (a maximum of) 'len' bytes from an RZIP file. + * Returns actual number of bytes read, or -1 in + * the event of an error */ +int64_t rzipstream_read(rzipstream_t *stream, void *data, int64_t len); + +/* File Write */ + +/* Writes 'len' bytes to an RZIP file. + * Returns actual number of bytes written, or -1 + * in the event of an error */ +int64_t rzipstream_write(rzipstream_t *stream, const void *data, int64_t len); + +/* File Status */ + +/* Returns total size (in bytes) of the *uncompressed* + * data in an RZIP file. + * (If reading an uncompressed file, this corresponds + * to the 'physical' file size in bytes) + * Returns -1 in the event of a error. */ +int64_t rzipstream_get_size(rzipstream_t *stream); + +/* Returns EOF when no further *uncompressed* data + * can be read from an RZIP file. */ +int rzipstream_eof(rzipstream_t *stream); + +/* File Close */ + +/* Closes RZIP file. If file is open for writing, + * flushes any remaining buffered data to disk. + * Returns -1 in the event of a error. */ +int rzipstream_close(rzipstream_t *stream); + +RETRO_END_DECLS + +#endif diff --git a/libretro-common/streams/interface_stream.c b/libretro-common/streams/interface_stream.c index b31f047db9..c2ad9edba8 100644 --- a/libretro-common/streams/interface_stream.c +++ b/libretro-common/streams/interface_stream.c @@ -28,6 +28,9 @@ #ifdef HAVE_CHD #include #endif +#if defined(HAVE_ZLIB) +#include +#endif struct intfstream_internal { @@ -55,6 +58,12 @@ struct intfstream_internal chdstream_t *fp; } chd; #endif +#if defined(HAVE_ZLIB) + struct + { + rzipstream_t *fp; + } rzip; +#endif }; int64_t intfstream_get_size(intfstream_internal_t *intf) @@ -73,6 +82,12 @@ int64_t intfstream_get_size(intfstream_internal_t *intf) return chdstream_get_size(intf->chd.fp); #else break; +#endif + case INTFSTREAM_RZIP: +#if defined(HAVE_ZLIB) + return rzipstream_get_size(intf->rzip.fp); +#else + break; #endif } @@ -99,6 +114,9 @@ bool intfstream_resize(intfstream_internal_t *intf, intfstream_info_t *info) #ifdef HAVE_CHD #endif break; + case INTFSTREAM_RZIP: + /* Unsupported */ + return false; } return true; @@ -130,6 +148,15 @@ bool intfstream_open(intfstream_internal_t *intf, const char *path, break; #else return false; +#endif + case INTFSTREAM_RZIP: +#if defined(HAVE_ZLIB) + intf->rzip.fp = rzipstream_open(path, mode); + if (!intf->rzip.fp) + return false; + break; +#else + return false; #endif } @@ -147,6 +174,7 @@ int intfstream_flush(intfstream_internal_t *intf) return filestream_flush(intf->file.fp); case INTFSTREAM_MEMORY: case INTFSTREAM_CHD: + case INTFSTREAM_RZIP: /* Should we stub this for these interfaces? */ break; } @@ -173,6 +201,12 @@ int intfstream_close(intfstream_internal_t *intf) #ifdef HAVE_CHD if (intf->chd.fp) chdstream_close(intf->chd.fp); +#endif + return 0; + case INTFSTREAM_RZIP: +#if defined(HAVE_ZLIB) + if (intf->rzip.fp) + return rzipstream_close(intf->rzip.fp); #endif return 0; } @@ -209,6 +243,8 @@ void *intfstream_init(intfstream_info_t *info) #else goto error; #endif + case INTFSTREAM_RZIP: + break; } return intf; @@ -252,6 +288,9 @@ int64_t intfstream_seek(intfstream_internal_t *intf, int64_t offset, int whence) #else break; #endif + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } return -1; @@ -273,6 +312,12 @@ int64_t intfstream_read(intfstream_internal_t *intf, void *s, uint64_t len) return chdstream_read(intf->chd.fp, s, len); #else break; +#endif + case INTFSTREAM_RZIP: +#if defined(HAVE_ZLIB) + return rzipstream_read(intf->rzip.fp, s, len); +#else + break; #endif } @@ -293,6 +338,12 @@ int64_t intfstream_write(intfstream_internal_t *intf, return memstream_write(intf->memory.fp, s, len); case INTFSTREAM_CHD: return -1; + case INTFSTREAM_RZIP: +#if defined(HAVE_ZLIB) + return rzipstream_write(intf->rzip.fp, s, len); +#else + return -1; +#endif } return 0; @@ -311,6 +362,8 @@ int64_t intfstream_get_ptr(intfstream_internal_t* intf) return memstream_get_ptr(intf->memory.fp); case INTFSTREAM_CHD: return -1; + case INTFSTREAM_RZIP: + return -1; } return 0; @@ -336,6 +389,9 @@ char *intfstream_gets(intfstream_internal_t *intf, #else break; #endif + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } return NULL; @@ -358,6 +414,9 @@ int intfstream_getc(intfstream_internal_t *intf) #else break; #endif + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } return -1; @@ -380,6 +439,9 @@ int64_t intfstream_tell(intfstream_internal_t *intf) #else break; #endif + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } return -1; @@ -400,6 +462,9 @@ void intfstream_rewind(intfstream_internal_t *intf) chdstream_rewind(intf->chd.fp); #endif break; + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } } @@ -418,6 +483,9 @@ void intfstream_putc(intfstream_internal_t *intf, int c) break; case INTFSTREAM_CHD: break; + case INTFSTREAM_RZIP: + /* Unsupported */ + break; } } @@ -531,8 +599,6 @@ error: return NULL; } - - intfstream_t *intfstream_open_chd_track(const char *path, unsigned mode, unsigned hints, int32_t track) { @@ -560,3 +626,29 @@ error: } return NULL; } + +intfstream_t* intfstream_open_rzip_file(const char *path, + unsigned mode) +{ + intfstream_info_t info; + intfstream_t *fd = NULL; + + info.type = INTFSTREAM_RZIP; + fd = (intfstream_t*)intfstream_init(&info); + + if (!fd) + return NULL; + + if (!intfstream_open(fd, path, mode, RETRO_VFS_FILE_ACCESS_HINT_NONE)) + goto error; + + return fd; + +error: + if (fd) + { + intfstream_close(fd); + free(fd); + } + return NULL; +} diff --git a/libretro-common/streams/rzip_stream.c b/libretro-common/streams/rzip_stream.c new file mode 100644 index 0000000000..94fa121caf --- /dev/null +++ b/libretro-common/streams/rzip_stream.c @@ -0,0 +1,742 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (rzip_stream.c). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include +#include + +#include + +/* Current RZIP file format version */ +#define RZIP_VERSION 1 + +/* Compression level + * > zlib default of 6 provides the best + * balance between file size and + * compression speed */ +#define RZIP_COMPRESSION_LEVEL 6 + +/* Default chunk size: 128kb */ +#define RZIP_DEFAULT_CHUNK_SIZE 131072 + +/* Header sizes (in bytes) */ +#define RZIP_HEADER_SIZE 20 +#define RZIP_CHUNK_HEADER_SIZE 4 + +/* Holds all metadata for an RZIP file stream */ +struct rzipstream +{ + bool is_compressed; + bool is_writing; + uint64_t size; + uint32_t chunk_size; + /* virtual_ptr: Used to track how much + * uncompressed data has been read */ + uint64_t virtual_ptr; + RFILE* file; + const struct trans_stream_backend *deflate_backend; + void *deflate_stream; + const struct trans_stream_backend *inflate_backend; + void *inflate_stream; + uint8_t *in_buf; + uint32_t in_buf_size; + uint32_t in_buf_ptr; + uint8_t *out_buf; + uint32_t out_buf_size; + uint32_t out_buf_ptr; + uint32_t out_buf_occupancy; +}; + +/* Header Functions */ + +/* Reads header information from RZIP file + * > Detects whether file is compressed or + * uncompressed data + * > If compressed, extracts uncompressed + * file/chunk sizes */ +static bool rzipstream_read_file_header(rzipstream_t *stream) +{ + uint8_t header_bytes[RZIP_HEADER_SIZE] = {0}; + int64_t length; + + if (!stream) + return false; + + /* Attempt to read header bytes */ + length = filestream_read(stream->file, header_bytes, sizeof(header_bytes)); + if (length <= 0) + return false; + + /* If file length is less than header size + * then assume this is uncompressed data */ + if (length < RZIP_HEADER_SIZE) + goto file_uncompressed; + + /* Check 'magic numbers' - first 8 bytes + * of header */ + if ((header_bytes[0] != 35) || /* # */ + (header_bytes[1] != 82) || /* R */ + (header_bytes[2] != 90) || /* Z */ + (header_bytes[3] != 73) || /* I */ + (header_bytes[4] != 80) || /* P */ + (header_bytes[5] != 118) || /* v */ + (header_bytes[6] != RZIP_VERSION) || /* file format version number */ + (header_bytes[7] != 35)) /* # */ + goto file_uncompressed; + + /* Get uncompressed chunk size - next 4 bytes */ + stream->chunk_size = ((uint32_t)header_bytes[11] << 24) | + ((uint32_t)header_bytes[10] << 16) | + ((uint32_t)header_bytes[9] << 8) | + (uint32_t)header_bytes[8]; + if (stream->chunk_size == 0) + return false; + + /* Get total uncompressed data size - next 8 bytes */ + stream->size = ((uint64_t)header_bytes[19] << 56) | + ((uint64_t)header_bytes[18] << 48) | + ((uint64_t)header_bytes[17] << 40) | + ((uint64_t)header_bytes[16] << 32) | + ((uint64_t)header_bytes[15] << 24) | + ((uint64_t)header_bytes[14] << 16) | + ((uint64_t)header_bytes[13] << 8) | + (uint64_t)header_bytes[12]; + if (stream->size == 0) + return false; + + stream->is_compressed = true; + return true; + +file_uncompressed: + + /* Reset file to start */ + filestream_seek(stream->file, 0, SEEK_SET); + + /* Get 'raw' file size */ + stream->size = filestream_get_size(stream->file); + + stream->is_compressed = false; + return true; +} + +/* Writes header information to RZIP file + * > ID 'magic numbers' + uncompressed + * file/chunk sizes */ +static bool rzipstream_write_file_header(rzipstream_t *stream) +{ + uint8_t header_bytes[RZIP_HEADER_SIZE] = {0}; + int64_t length; + + if (!stream) + return false; + + /* Populate header array */ + + /* > 'Magic numbers' - first 8 bytes */ + header_bytes[0] = 35; /* # */ + header_bytes[1] = 82; /* R */ + header_bytes[2] = 90; /* Z */ + header_bytes[3] = 73; /* I */ + header_bytes[4] = 80; /* P */ + header_bytes[5] = 118; /* v */ + header_bytes[6] = RZIP_VERSION; /* file format version number */ + header_bytes[7] = 35; /* # */ + + /* > Uncompressed chunk size - next 4 bytes */ + header_bytes[11] = (stream->chunk_size >> 24) & 0xFF; + header_bytes[10] = (stream->chunk_size >> 16) & 0xFF; + header_bytes[9] = (stream->chunk_size >> 8) & 0xFF; + header_bytes[8] = stream->chunk_size & 0xFF; + + /* > Total uncompressed data size - next 8 bytes */ + header_bytes[19] = (stream->size >> 56) & 0xFF; + header_bytes[18] = (stream->size >> 48) & 0xFF; + header_bytes[17] = (stream->size >> 40) & 0xFF; + header_bytes[16] = (stream->size >> 32) & 0xFF; + header_bytes[15] = (stream->size >> 24) & 0xFF; + header_bytes[14] = (stream->size >> 16) & 0xFF; + header_bytes[13] = (stream->size >> 8) & 0xFF; + header_bytes[12] = stream->size & 0xFF; + + /* Reset file to start */ + filestream_seek(stream->file, 0, SEEK_SET); + + /* Write header bytes */ + length = filestream_write(stream->file, header_bytes, sizeof(header_bytes)); + if (length != RZIP_HEADER_SIZE) + return false; + + return true; +} + +/* Stream Initialisation/De-initialisation */ + +/* Initialises all members of an rzipstream_t struct, + * reading config from existing file header if available */ +static bool rzipstream_init_stream( + rzipstream_t *stream, const char *path, bool is_writing) +{ + unsigned file_mode; + + if (!stream) + return false; + + /* Ensure stream has valid initial values */ + stream->size = 0; + stream->chunk_size = RZIP_DEFAULT_CHUNK_SIZE; + stream->file = NULL; + stream->deflate_backend = NULL; + stream->deflate_stream = NULL; + stream->inflate_backend = NULL; + stream->inflate_stream = NULL; + stream->in_buf = NULL; + stream->in_buf_size = 0; + stream->in_buf_ptr = 0; + stream->out_buf = NULL; + stream->out_buf_size = 0; + stream->out_buf_ptr = 0; + stream->out_buf_occupancy = 0; + + /* Check whether this is a read or write stream */ + stream->is_writing = is_writing; + if (stream->is_writing) + { + /* Written files are always compressed */ + stream->is_compressed = true; + file_mode = RETRO_VFS_FILE_ACCESS_WRITE; + } + /* For read files, must get compression status + * from file itself... */ + else + file_mode = RETRO_VFS_FILE_ACCESS_READ; + + /* Open file */ + stream->file = filestream_open( + path, file_mode, RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (!stream->file) + return false; + + /* If file is open for writing, output header + * (Size component cannot be written until + * file is closed...) */ + if (stream->is_writing) + { + /* Note: could just write zeros here, but + * still want to identify this as an RZIP + * file if writing fails partway through */ + if (!rzipstream_write_file_header(stream)) + return false; + } + /* If file is open for reading, parse any existing + * header */ + else if (!rzipstream_read_file_header(stream)) + return false; + + /* Initialise appropriate transform stream + * and determine associated buffer sizes */ + if (stream->is_writing) + { + /* Compression */ + stream->deflate_backend = trans_stream_get_zlib_deflate_backend(); + if (!stream->deflate_backend) + return false; + + stream->deflate_stream = stream->deflate_backend->stream_new(); + if (!stream->deflate_stream) + return false; + + /* Set compression level */ + if (!stream->deflate_backend->define( + stream->deflate_stream, "level", RZIP_COMPRESSION_LEVEL)) + return false; + + /* Buffers + * > Input: uncompressed + * > Output: compressed */ + stream->in_buf_size = stream->chunk_size; + stream->out_buf_size = stream->chunk_size * 2; + /* > Account for minimum zlib overhead + * of 11 bytes... */ + stream->out_buf_size = + (stream->out_buf_size < (stream->in_buf_size + 11)) ? + stream->out_buf_size + 11 : + stream->out_buf_size; + + /* Redundant safety check */ + if ((stream->in_buf_size == 0) || + (stream->out_buf_size == 0)) + return false; + } + /* When reading, don't need an inflate transform + * stream (or buffers) if source file is uncompressed */ + else if (stream->is_compressed) + { + /* Decompression */ + stream->inflate_backend = trans_stream_get_zlib_inflate_backend(); + if (!stream->inflate_backend) + return false; + + stream->inflate_stream = stream->inflate_backend->stream_new(); + if (!stream->inflate_stream) + return false; + + /* Buffers + * > Input: compressed + * > Output: uncompressed + * Note 1: Actual compressed chunk sizes are read + * from the file - just allocate a sensible + * default to minimise memory reallocations + * Note 2: If file header is valid, output buffer + * should have a size of exactly stream->chunk_size. + * Allocate some additional space, just for + * redundant safety... */ + stream->in_buf_size = stream->chunk_size * 2; + stream->out_buf_size = stream->chunk_size + (stream->chunk_size >> 2); + + /* Redundant safety check */ + if ((stream->in_buf_size == 0) || + (stream->out_buf_size == 0)) + return false; + } + + /* Allocate buffers */ + if (stream->in_buf_size > 0) + { + stream->in_buf = (uint8_t *)calloc(stream->in_buf_size, 1); + if (!stream->in_buf) + return false; + } + + if (stream->out_buf_size > 0) + { + stream->out_buf = (uint8_t *)calloc(stream->out_buf_size, 1); + if (!stream->out_buf) + return false; + } + + return true; +} + +/* free()'s all members of an rzipstream_t struct + * > Also closes associated file, if currently open */ +static int rzipstream_free_stream(rzipstream_t *stream) +{ + int ret = 0; + + if (!stream) + return -1; + + /* Free transform streams */ + if (stream->deflate_stream && stream->deflate_backend) + stream->deflate_backend->stream_free(stream->deflate_stream); + + stream->deflate_stream = NULL; + stream->deflate_backend = NULL; + + if (stream->inflate_stream && stream->inflate_backend) + stream->inflate_backend->stream_free(stream->inflate_stream); + + stream->inflate_stream = NULL; + stream->inflate_backend = NULL; + + /* Free buffers */ + if (stream->in_buf) + free(stream->in_buf); + stream->in_buf = NULL; + + if (stream->out_buf) + free(stream->out_buf); + stream->out_buf = NULL; + + /* Close file */ + if (stream->file) + ret = filestream_close(stream->file); + stream->file = NULL; + + free(stream); + + return ret; +} + +/* File Open */ + +/* Opens a new or existing RZIP file + * > Supported 'mode' values are: + * - RETRO_VFS_FILE_ACCESS_READ + * - RETRO_VFS_FILE_ACCESS_WRITE + * > When reading, 'path' may reference compressed + * or uncompressed data + * Returns NULL if arguments are invalid, file + * is invalid or an IO error occurs */ +rzipstream_t* rzipstream_open(const char *path, unsigned mode) +{ + rzipstream_t *stream = NULL; + + /* Sanity check + * > Only RETRO_VFS_FILE_ACCESS_READ and + * RETRO_VFS_FILE_ACCESS_WRITE are supported */ + if (string_is_empty(path) || + ((mode != RETRO_VFS_FILE_ACCESS_READ) && + (mode != RETRO_VFS_FILE_ACCESS_WRITE))) + return NULL; + + /* If opening in read mode, ensure file exists */ + if ((mode == RETRO_VFS_FILE_ACCESS_READ) && + !path_is_valid(path)) + return NULL; + + /* Allocate stream object */ + stream = (rzipstream_t*)calloc(1, sizeof(*stream)); + if (!stream) + return NULL; + + /* Initialise stream */ + if (!rzipstream_init_stream( + stream, path, + (mode == RETRO_VFS_FILE_ACCESS_WRITE))) + { + rzipstream_free_stream(stream); + return NULL; + } + + return stream; +} + +/* File Read */ + +/* Reads and decompresses the next chunk of data + * in the RZIP file */ +static bool rzipstream_read_chunk(rzipstream_t *stream) +{ + uint8_t chunk_header_bytes[RZIP_CHUNK_HEADER_SIZE] = {0}; + uint32_t compressed_chunk_size; + uint32_t inflate_read; + uint32_t inflate_written; + int64_t length; + + if (!stream || !stream->inflate_backend || !stream->inflate_stream) + return false; + + /* Attempt to read chunk header bytes */ + length = filestream_read( + stream->file, chunk_header_bytes, sizeof(chunk_header_bytes)); + if (length != RZIP_CHUNK_HEADER_SIZE) + return false; + + /* Get size of next compressed chunk */ + compressed_chunk_size = ((uint32_t)chunk_header_bytes[3] << 24) | + ((uint32_t)chunk_header_bytes[2] << 16) | + ((uint32_t)chunk_header_bytes[1] << 8) | + (uint32_t)chunk_header_bytes[0]; + if (compressed_chunk_size == 0) + return false; + + /* Resize input buffer, if required */ + if (compressed_chunk_size > stream->in_buf_size) + { + free(stream->in_buf); + stream->in_buf = NULL; + + stream->in_buf_size = compressed_chunk_size; + stream->in_buf = (uint8_t *)calloc(stream->in_buf_size, 1); + if (!stream->in_buf) + return false; + + /* Note: Uncompressed data size is fixed, and read + * from the file header - we therefore don't attempt + * to resize the output buffer (if it's too small, then + * that's an error condition) */ + } + + /* Read compressed chunk from file */ + length = filestream_read( + stream->file, stream->in_buf, compressed_chunk_size); + if (length != compressed_chunk_size) + return false; + + /* Decompress chunk data */ + stream->inflate_backend->set_in( + stream->inflate_stream, + stream->in_buf, compressed_chunk_size); + + stream->inflate_backend->set_out( + stream->inflate_stream, + stream->out_buf, stream->out_buf_size); + + /* Note: We have to set 'flush == true' here, otherwise we + * can't guarantee that the entire chunk will be written + * to the output buffer - this is inefficient, but not + * much we can do... */ + if (!stream->inflate_backend->trans( + stream->inflate_stream, true, + &inflate_read, &inflate_written, NULL)) + return false; + + /* Error checking */ + if (inflate_read != compressed_chunk_size) + return false; + + if ((inflate_written == 0) || + (inflate_written > stream->out_buf_size)) + return false; + + /* Record current output buffer occupancy + * and reset pointer */ + stream->out_buf_occupancy = inflate_written; + stream->out_buf_ptr = 0; + + return true; +} + +/* Reads (a maximum of) 'len' bytes from an RZIP file. + * Returns actual number of bytes read, or -1 in + * the event of an error */ +int64_t rzipstream_read(rzipstream_t *stream, void *data, int64_t len) +{ + int64_t data_len = len; + uint8_t *data_ptr = (uint8_t *)data; + int64_t data_read = 0; + + if (!stream || stream->is_writing) + return -1; + + /* If we are reading uncompressed data, simply + * 'pass on' the direct file access request */ + if (!stream->is_compressed) + return filestream_read(stream->file, data, len); + + /* Process input data */ + while (data_len > 0) + { + uint32_t read_size = 0; + + /* Check whether we have reached the end + * of the file */ + if (stream->virtual_ptr >= stream->size) + return data_read; + + /* If everything in the output buffer has already + * been read, grab and extract the next chunk + * from disk */ + if (stream->out_buf_ptr >= stream->out_buf_occupancy) + if (!rzipstream_read_chunk(stream)) + return -1; + + /* Get amount of data to 'read out' this loop + * > i.e. minimum of remaining output buffer + * occupancy and remaining 'read data' size */ + read_size = stream->out_buf_occupancy - stream->out_buf_ptr; + read_size = (read_size > data_len) ? data_len : read_size; + + /* Copy as much cached data as possible into + * the read buffer */ + memcpy(data_ptr, stream->out_buf + stream->out_buf_ptr, read_size); + + /* Increment pointers and remaining length */ + stream->out_buf_ptr += read_size; + data_ptr += read_size; + data_len -= read_size; + + stream->virtual_ptr += read_size; + + data_read += read_size; + } + + return data_read; +} + +/* File Write */ + +/* Compresses currently cached data and writes it + * as the next RZIP file chunk */ +static bool rzipstream_write_chunk(rzipstream_t *stream) +{ + uint8_t chunk_header_bytes[RZIP_CHUNK_HEADER_SIZE] = {0}; + uint32_t deflate_read; + uint32_t deflate_written; + int64_t length; + + if (!stream || !stream->deflate_backend || !stream->deflate_stream) + return false; + + /* Compress data currently held in input buffer */ + stream->deflate_backend->set_in( + stream->deflate_stream, + stream->in_buf, stream->in_buf_ptr); + + stream->deflate_backend->set_out( + stream->deflate_stream, + stream->out_buf, stream->out_buf_size); + + /* Note: We have to set 'flush == true' here, otherwise we + * can't guarantee that the entire chunk will be written + * to the output buffer - this is inefficient, but not + * much we can do... */ + if (!stream->deflate_backend->trans( + stream->deflate_stream, true, + &deflate_read, &deflate_written, NULL)) + return false; + + /* Error checking */ + if (deflate_read != stream->in_buf_ptr) + return false; + + if ((deflate_written == 0) || + (deflate_written > stream->out_buf_size)) + return false; + + /* Write compressed chunk size to file */ + chunk_header_bytes[3] = (deflate_written >> 24) & 0xFF; + chunk_header_bytes[2] = (deflate_written >> 16) & 0xFF; + chunk_header_bytes[1] = (deflate_written >> 8) & 0xFF; + chunk_header_bytes[0] = deflate_written & 0xFF; + + length = filestream_write( + stream->file, chunk_header_bytes, sizeof(chunk_header_bytes)); + if (length != RZIP_CHUNK_HEADER_SIZE) + return false; + + /* Write compressed data to file */ + length = filestream_write( + stream->file, stream->out_buf, deflate_written); + + if (length != deflate_written) + return false; + + /* Reset input buffer pointer */ + stream->in_buf_ptr = 0; + + return true; +} + +/* Writes 'len' bytes to an RZIP file. + * Returns actual number of bytes written, or -1 + * in the event of an error */ +int64_t rzipstream_write(rzipstream_t *stream, const void *data, int64_t len) +{ + int64_t data_len = len; + const uint8_t *data_ptr = (const uint8_t *)data; + + if (!stream || !stream->is_writing) + return -1; + + /* Process input data */ + while (data_len > 0) + { + uint32_t cache_size = 0; + + /* If input buffer is full, compress and write to disk */ + if (stream->in_buf_ptr >= stream->in_buf_size) + if (!rzipstream_write_chunk(stream)) + return -1; + + /* Get amount of data to cache during this loop + * > i.e. minimum of space remaining in input buffer + * and remaining 'write data' size */ + cache_size = stream->in_buf_size - stream->in_buf_ptr; + cache_size = (cache_size > data_len) ? data_len : cache_size; + + /* Copy as much data as possible into + * the input buffer */ + memcpy(stream->in_buf + stream->in_buf_ptr, data_ptr, cache_size); + + /* Increment pointers and remaining length */ + stream->in_buf_ptr += cache_size; + data_ptr += cache_size; + data_len -= cache_size; + + stream->size += cache_size; + stream->virtual_ptr += cache_size; + } + + /* We always write the specified number of bytes + * (unless rzipstream_write_chunk() fails, in + * which we register a complete failure...) */ + return len; +} + +/* File Status */ + +/* Returns total size (in bytes) of the *uncompressed* + * data in an RZIP file. + * (If reading an uncompressed file, this corresponds + * to the 'physical' file size in bytes) + * Returns -1 in the event of a error. */ +int64_t rzipstream_get_size(rzipstream_t *stream) +{ + if (!stream) + return -1; + + if (stream->is_compressed) + return stream->size; + else + return filestream_get_size(stream->file); +} + +/* Returns EOF when no further *uncompressed* data + * can be read from an RZIP file. */ +int rzipstream_eof(rzipstream_t *stream) +{ + if (!stream) + return -1; + + if (stream->is_compressed) + return (stream->virtual_ptr >= stream->size) ? + EOF : 0; + else + return filestream_eof(stream->file); +} + +/* File Close */ + +/* Closes RZIP file. If file is open for writing, + * flushes any remaining buffered data to disk. + * Returns -1 in the event of a error. */ +int rzipstream_close(rzipstream_t *stream) +{ + if (!stream) + return -1; + + /* If we are writing, ensure that any + * remaining uncompressed data is flushed to + * disk and update file header */ + if (stream->is_writing) + { + if (stream->in_buf_ptr > 0) + if (!rzipstream_write_chunk(stream)) + goto error; + + if (!rzipstream_write_file_header(stream)) + goto error; + } + + /* Free stream + * > This also closes the file */ + return rzipstream_free_stream(stream); + +error: + /* Stream must be free()'d regardless */ + rzipstream_free_stream(stream); + return -1; +} diff --git a/menu/cbs/menu_cbs_sublabel.c b/menu/cbs/menu_cbs_sublabel.c index 82e6cb51f6..f798140732 100644 --- a/menu/cbs/menu_cbs_sublabel.c +++ b/menu/cbs/menu_cbs_sublabel.c @@ -381,6 +381,7 @@ default_sublabel_macro(action_bind_sublabel_perfcnt_enable, MENU_ default_sublabel_macro(action_bind_sublabel_savestate_auto_save, MENU_ENUM_SUBLABEL_SAVESTATE_AUTO_SAVE) default_sublabel_macro(action_bind_sublabel_savestate_auto_load, MENU_ENUM_SUBLABEL_SAVESTATE_AUTO_LOAD) default_sublabel_macro(action_bind_sublabel_savestate_thumbnail_enable, MENU_ENUM_SUBLABEL_SAVESTATE_THUMBNAIL_ENABLE) +default_sublabel_macro(action_bind_sublabel_savestate_file_compression, MENU_ENUM_SUBLABEL_SAVESTATE_FILE_COMPRESSION) default_sublabel_macro(action_bind_sublabel_autosave_interval, MENU_ENUM_SUBLABEL_AUTOSAVE_INTERVAL) default_sublabel_macro(action_bind_sublabel_input_remap_binds_enable, MENU_ENUM_SUBLABEL_INPUT_REMAP_BINDS_ENABLE) default_sublabel_macro(action_bind_sublabel_input_autodetect_enable, MENU_ENUM_SUBLABEL_INPUT_AUTODETECT_ENABLE) @@ -2327,6 +2328,9 @@ int menu_cbs_init_bind_sublabel(menu_file_list_cbs_t *cbs, case MENU_ENUM_LABEL_SAVESTATE_THUMBNAIL_ENABLE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_savestate_thumbnail_enable); break; + case MENU_ENUM_LABEL_SAVESTATE_FILE_COMPRESSION: + BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_savestate_file_compression); + break; case MENU_ENUM_LABEL_SAVESTATE_AUTO_SAVE: BIND_ACTION_SUBLABEL(cbs, action_bind_sublabel_savestate_auto_save); break; diff --git a/menu/menu_displaylist.c b/menu/menu_displaylist.c index 1a394f8e94..e6098c9720 100644 --- a/menu/menu_displaylist.c +++ b/menu/menu_displaylist.c @@ -6832,6 +6832,7 @@ unsigned menu_displaylist_build_list( {MENU_ENUM_LABEL_SAVESTATE_AUTO_SAVE, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SAVESTATE_AUTO_LOAD, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SAVESTATE_THUMBNAIL_ENABLE, PARSE_ONLY_BOOL}, + {MENU_ENUM_LABEL_SAVESTATE_FILE_COMPRESSION, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SAVEFILES_IN_CONTENT_DIR_ENABLE, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SAVESTATES_IN_CONTENT_DIR_ENABLE, PARSE_ONLY_BOOL}, {MENU_ENUM_LABEL_SYSTEMFILES_IN_CONTENT_DIR_ENABLE, PARSE_ONLY_BOOL}, diff --git a/menu/menu_setting.c b/menu/menu_setting.c index bc4c099ac9..ff78f75837 100644 --- a/menu/menu_setting.c +++ b/menu/menu_setting.c @@ -8950,6 +8950,24 @@ static bool setting_append_list( general_read_handler, SD_FLAG_NONE); +#if defined(HAVE_ZLIB) + CONFIG_BOOL( + list, list_info, + &settings->bools.savestate_file_compression, + MENU_ENUM_LABEL_SAVESTATE_FILE_COMPRESSION, + MENU_ENUM_LABEL_VALUE_SAVESTATE_FILE_COMPRESSION, + DEFAULT_SAVESTATE_FILE_COMPRESSION, + MENU_ENUM_LABEL_VALUE_OFF, + MENU_ENUM_LABEL_VALUE_ON, + &group_info, + &subgroup_info, + parent_group, + general_write_handler, + general_read_handler, + SD_FLAG_NONE); +#endif + + /* TODO/FIXME: This is in the wrong group... */ CONFIG_BOOL( list, list_info, &settings->bools.scan_without_core_match, diff --git a/msg_hash.h b/msg_hash.h index 99060cf68c..061a01134d 100644 --- a/msg_hash.h +++ b/msg_hash.h @@ -1607,6 +1607,7 @@ enum msg_hash_enums MENU_LABEL(SAVESTATE_AUTO_SAVE), MENU_LABEL(SAVESTATE_AUTO_LOAD), MENU_LABEL(SAVESTATE_THUMBNAIL_ENABLE), + MENU_LABEL(SAVESTATE_FILE_COMPRESSION), MENU_LABEL(SUSPEND_SCREENSAVER_ENABLE), MENU_ENUM_LABEL_VOLUME_UP, diff --git a/retroarch.c b/retroarch.c index a7ecd685fc..46e0060656 100644 --- a/retroarch.c +++ b/retroarch.c @@ -7818,7 +7818,14 @@ static void command_event_undo_save_state(char *s, size_t len) } if (!content_undo_save_state()) + { + strlcpy(s, + msg_hash_to_str(MSG_FAILED_TO_UNDO_SAVE_STATE), len); return; + } + + strlcpy(s, + msg_hash_to_str(MSG_UNDOING_SAVE_STATE), len); } static void command_event_undo_load_state(char *s, size_t len) diff --git a/tasks/task_save.c b/tasks/task_save.c index d5804f1386..d11c133ca7 100644 --- a/tasks/task_save.c +++ b/tasks/task_save.c @@ -101,6 +101,7 @@ typedef struct int state_slot; bool thumbnail_enable; bool has_valid_framebuffer; + bool compress_files; } save_task_state_t; typedef save_task_state_t load_task_data_t; @@ -560,7 +561,12 @@ static void *get_serialized_data(const char *path, size_t serial_size) if (!serial_size) return NULL; - data = malloc(serial_size); + /* Ensure buffer is initialised to zero + * > Prevents inconsistent compressed state file + * sizes when core requests a larger buffer + * than it needs (and leaves the excess + * as uninitialised garbage) */ + data = calloc(serial_size, 1); if (!data) return NULL; @@ -597,9 +603,13 @@ static void task_save_handler(retro_task_t *task) if (!state->file) { - state->file = intfstream_open_file( - state->path, RETRO_VFS_FILE_ACCESS_WRITE, - RETRO_VFS_FILE_ACCESS_HINT_NONE); + if (state->compress_files) + state->file = intfstream_open_rzip_file( + state->path, RETRO_VFS_FILE_ACCESS_WRITE); + else + state->file = intfstream_open_file( + state->path, RETRO_VFS_FILE_ACCESS_WRITE, + RETRO_VFS_FILE_ACCESS_HINT_NONE); if (!state->file) return; @@ -694,6 +704,11 @@ static bool task_push_undo_save_state(const char *path, void *data, size_t size) retro_task_t *task = task_init(); save_task_state_t *state = (save_task_state_t*)calloc(1, sizeof(*state)); settings_t *settings = config_get_ptr(); +#if defined(HAVE_ZLIB) + bool compress_files = settings->bools.savestate_file_compression; +#else + bool compress_files = false; +#endif if (!task || !state) goto error; @@ -704,6 +719,7 @@ static bool task_push_undo_save_state(const char *path, void *data, size_t size) state->undo_save = true; state->state_slot = settings->ints.state_slot; state->has_valid_framebuffer = video_driver_cached_frame_has_valid_framebuffer(); + state->compress_files = compress_files; task->type = TASK_TYPE_BLOCKING; task->state = state; @@ -787,23 +803,26 @@ static void task_load_handler(retro_task_t *task) if (!state->file) { +#if defined(HAVE_ZLIB) + /* Always use RZIP interface when reading state + * files - this will automatically handle uncompressed + * data */ + state->file = intfstream_open_rzip_file(state->path, + RETRO_VFS_FILE_ACCESS_READ); +#else state->file = intfstream_open_file(state->path, RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE); +#endif if (!state->file) goto error; - if (intfstream_seek(state->file, 0, SEEK_END) != 0) - goto error; - - state->size = intfstream_tell(state->file); + state->size = intfstream_get_size(state->file); if (state->size < 0) goto error; - intfstream_rewind(state->file); - state->data = malloc(state->size + 1); if (!state->data) @@ -1076,6 +1095,11 @@ static void task_push_save_state(const char *path, void *data, size_t size, bool settings_t *settings = config_get_ptr(); bool savestate_thumbnail_enable = settings->bools.savestate_thumbnail_enable; int state_slot = settings->ints.state_slot; +#if defined(HAVE_ZLIB) + bool compress_files = settings->bools.savestate_file_compression; +#else + bool compress_files = false; +#endif if (!task || !state) goto error; @@ -1088,6 +1112,7 @@ static void task_push_save_state(const char *path, void *data, size_t size, bool state->thumbnail_enable = savestate_thumbnail_enable; state->state_slot = state_slot; state->has_valid_framebuffer = video_driver_cached_frame_has_valid_framebuffer(); + state->compress_files = compress_files; task->type = TASK_TYPE_BLOCKING; task->state = state; @@ -1161,6 +1186,11 @@ static void task_push_load_and_save_state(const char *path, void *data, retro_task_t *task = NULL; settings_t *settings = config_get_ptr(); int state_slot = settings->ints.state_slot; +#if defined(HAVE_ZLIB) + bool compress_files = settings->bools.savestate_file_compression; +#else + bool compress_files = false; +#endif save_task_state_t *state = (save_task_state_t*) calloc(1, sizeof(*state)); @@ -1188,6 +1218,7 @@ static void task_push_load_and_save_state(const char *path, void *data, state->state_slot = state_slot; state->has_valid_framebuffer = video_driver_cached_frame_has_valid_framebuffer(); + state->compress_files = compress_files; task->state = state; task->type = TASK_TYPE_BLOCKING; @@ -1318,6 +1349,11 @@ bool content_load_state(const char *path, save_task_state_t *state = (save_task_state_t*)calloc(1, sizeof(*state)); settings_t *settings = config_get_ptr(); int state_slot = settings->ints.state_slot; +#if defined(HAVE_ZLIB) + bool compress_files = settings->bools.savestate_file_compression; +#else + bool compress_files = false; +#endif if (!task || !state) goto error; @@ -1328,6 +1364,7 @@ bool content_load_state(const char *path, state->state_slot = state_slot; state->has_valid_framebuffer = video_driver_cached_frame_has_valid_framebuffer(); + state->compress_files = compress_files; task->type = TASK_TYPE_BLOCKING; task->state = state;