diff --git a/src/core/fullscreen_ui.cpp b/src/core/fullscreen_ui.cpp index d8b3b4f55..ea1f96e78 100644 --- a/src/core/fullscreen_ui.cpp +++ b/src/core/fullscreen_ui.cpp @@ -5184,9 +5184,14 @@ void FullscreenUI::DrawAdvancedSettingsPage() FSUI_CSTR("Renames existing save states when saving to a backup file."), "Main", "CreateSaveStateBackups", false); DrawToggleSetting( - bsi, FSUI_ICONSTR(ICON_FA_GAMEPAD, "Load Devices From Save States"), + bsi, FSUI_CSTR("Load Devices From Save States"), FSUI_CSTR("When enabled, memory cards and controllers will be overwritten when save states are loaded."), "Main", "LoadDevicesFromSaveStates", false); + DrawEnumSetting(bsi, FSUI_CSTR("Save State Compression"), + FSUI_CSTR("Reduces the size of save states by compressing the data before saving."), "Main", + "SaveStateCompression", Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE, + &Settings::ParseSaveStateCompressionModeName, &Settings::GetSaveStateCompressionModeName, + &Settings::GetSaveStateCompressionModeDisplayName, SaveStateCompressionMode::Count); MenuHeading(FSUI_CSTR("Display Settings")); DrawToggleSetting(bsi, FSUI_CSTR("Show Status Indicators"), @@ -7618,6 +7623,7 @@ TRANSLATE_NOOP("FullscreenUI", "Reduces \"wobbly\" polygons by attempting to pre TRANSLATE_NOOP("FullscreenUI", "Reduces hitches in emulation by reading/decompressing CD data asynchronously on a worker thread."); TRANSLATE_NOOP("FullscreenUI", "Reduces input latency by delaying the start of frame until closer to the presentation time."); TRANSLATE_NOOP("FullscreenUI", "Reduces polygon Z-fighting through depth testing. Low compatibility with games."); +TRANSLATE_NOOP("FullscreenUI", "Reduces the size of save states by compressing the data before saving."); TRANSLATE_NOOP("FullscreenUI", "Region"); TRANSLATE_NOOP("FullscreenUI", "Region: "); TRANSLATE_NOOP("FullscreenUI", "Release Date: %s"); @@ -7661,6 +7667,7 @@ TRANSLATE_NOOP("FullscreenUI", "SDL DualShock 4 / DualSense Enhanced Mode"); TRANSLATE_NOOP("FullscreenUI", "Save Profile"); TRANSLATE_NOOP("FullscreenUI", "Save Screenshot"); TRANSLATE_NOOP("FullscreenUI", "Save State"); +TRANSLATE_NOOP("FullscreenUI", "Save State Compression"); TRANSLATE_NOOP("FullscreenUI", "Save State On Exit"); TRANSLATE_NOOP("FullscreenUI", "Saved {:%c}"); TRANSLATE_NOOP("FullscreenUI", "Saves state periodically so you can rewind any mistakes while playing."); diff --git a/src/core/save_state_version.h b/src/core/save_state_version.h index 3a6d46d67..53480f7d5 100644 --- a/src/core/save_state_version.h +++ b/src/core/save_state_version.h @@ -11,13 +11,6 @@ static constexpr u32 SAVE_STATE_MINIMUM_VERSION = 42; static_assert(SAVE_STATE_VERSION >= SAVE_STATE_MINIMUM_VERSION); -enum class SaveStateCompression : u32 -{ - None = 0, - ZLib = 1, - ZStd = 2, -}; - #pragma pack(push, 4) struct SAVE_STATE_HEADER { @@ -28,6 +21,13 @@ struct SAVE_STATE_HEADER MAX_SAVE_STATE_SIZE = 32 * 1024 * 1024, }; + enum class CompressionType : u32 + { + None = 0, + Deflate = 1, + Zstandard = 2, + }; + u32 magic; u32 version; char title[MAX_TITLE_LENGTH]; diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 805a04c66..93f5387f3 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -147,7 +147,6 @@ void Settings::Load(SettingsInterface& si) pause_on_controller_disconnection = si.GetBoolValue("Main", "PauseOnControllerDisconnection", false); save_state_on_exit = si.GetBoolValue("Main", "SaveStateOnExit", true); create_save_state_backups = si.GetBoolValue("Main", "CreateSaveStateBackups", DEFAULT_SAVE_STATE_BACKUPS); - compress_save_states = si.GetBoolValue("Main", "CompressSaveStates", DEFAULT_SAVE_STATE_COMPRESSION); confim_power_off = si.GetBoolValue("Main", "ConfirmPowerOff", true); load_devices_from_save_states = si.GetBoolValue("Main", "LoadDevicesFromSaveStates", false); apply_compatibility_settings = si.GetBoolValue("Main", "ApplyCompatibilitySettings", true); @@ -311,6 +310,12 @@ void Settings::Load(SettingsInterface& si) display_stretch_vertically = si.GetBoolValue("Display", "StretchVertically", false); display_osd_scale = si.GetFloatValue("Display", "OSDScale", DEFAULT_OSD_SCALE); + save_state_compression = ParseSaveStateCompressionModeName( + si.GetStringValue("Main", "SaveStateCompression", + GetSaveStateCompressionModeName(DEFAULT_SAVE_STATE_COMPRESSION_MODE)) + .c_str()) + .value_or(DEFAULT_SAVE_STATE_COMPRESSION_MODE); + cdrom_readahead_sectors = static_cast(si.GetIntValue("CDROM", "ReadaheadSectors", DEFAULT_CDROM_READAHEAD_SECTORS)); cdrom_mechacon_version = @@ -462,7 +467,7 @@ void Settings::Save(SettingsInterface& si, bool ignore_base) const si.SetBoolValue("Main", "PauseOnControllerDisconnection", pause_on_controller_disconnection); si.SetBoolValue("Main", "SaveStateOnExit", save_state_on_exit); si.SetBoolValue("Main", "CreateSaveStateBackups", create_save_state_backups); - si.SetBoolValue("Main", "CompressSaveStates", compress_save_states); + si.SetStringValue("Main", "SaveStateCompression", GetSaveStateCompressionModeName(save_state_compression)); si.SetBoolValue("Main", "ConfirmPowerOff", confim_power_off); si.SetBoolValue("Main", "EnableDiscordPresence", enable_discord_presence); } @@ -1764,6 +1769,43 @@ const char* Settings::GetCDROMMechVersionDisplayName(CDROMMechaconVersion mode) return s_mechacon_version_display_names[static_cast(mode)]; } +static constexpr const std::array s_save_state_compression_mode_names = { + "Uncompressed", "DeflateLow", "DeflateDefault", "DeflateHigh", "ZstLow", "ZstDefault", "ZstHigh", +}; +static constexpr const std::array s_save_state_compression_mode_display_names = { + TRANSLATE_NOOP("Settings", "Uncompressed"), TRANSLATE_NOOP("Settings", "Deflate (Low)"), + TRANSLATE_NOOP("Settings", "Deflate (Default)"), TRANSLATE_NOOP("Settings", "Deflate (High)"), + TRANSLATE_NOOP("Settings", "Zstandard (Low)"), TRANSLATE_NOOP("Settings", "Zstandard (Default)"), + TRANSLATE_NOOP("Settings", "Zstandard (High)"), +}; +static_assert(s_save_state_compression_mode_names.size() == static_cast(SaveStateCompressionMode::Count)); +static_assert(s_save_state_compression_mode_display_names.size() == + static_cast(SaveStateCompressionMode::Count)); + +std::optional Settings::ParseSaveStateCompressionModeName(const char* str) +{ + u32 index = 0; + for (const char* name : s_save_state_compression_mode_names) + { + if (StringUtil::Strcasecmp(name, str) == 0) + return static_cast(index); + + index++; + } + + return std::nullopt; +} + +const char* Settings::GetSaveStateCompressionModeName(SaveStateCompressionMode mode) +{ + return s_save_state_compression_mode_names[static_cast(mode)]; +} + +const char* Settings::GetSaveStateCompressionModeDisplayName(SaveStateCompressionMode mode) +{ + return Host::TranslateToCString("Settings", s_save_state_compression_mode_display_names[static_cast(mode)]); +} + std::string EmuFolders::AppRoot; std::string EmuFolders::DataRoot; std::string EmuFolders::Bios; diff --git a/src/core/settings.h b/src/core/settings.h index 01b163c81..df673bda4 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -63,14 +63,14 @@ struct Settings ConsoleRegion region = DEFAULT_CONSOLE_REGION; CPUExecutionMode cpu_execution_mode = DEFAULT_CPU_EXECUTION_MODE; - u32 cpu_overclock_numerator = 1; - u32 cpu_overclock_denominator = 1; + CPUFastmemMode cpu_fastmem_mode = DEFAULT_CPU_FASTMEM_MODE; bool cpu_overclock_enable : 1 = false; bool cpu_overclock_active : 1 = false; bool cpu_recompiler_memory_exceptions : 1 = false; bool cpu_recompiler_block_linking : 1 = true; bool cpu_recompiler_icache : 1 = false; - CPUFastmemMode cpu_fastmem_mode = DEFAULT_CPU_FASTMEM_MODE; + u32 cpu_overclock_numerator = 1; + u32 cpu_overclock_denominator = 1; float emulation_speed = 1.0f; float fast_forward_speed = 0.0f; @@ -84,7 +84,6 @@ struct Settings bool pause_on_controller_disconnection : 1 = false; bool save_state_on_exit : 1 = true; bool create_save_state_backups : 1 = DEFAULT_SAVE_STATE_BACKUPS; - bool compress_save_states : 1 = DEFAULT_SAVE_STATE_COMPRESSION; bool confim_power_off : 1 = true; bool load_devices_from_save_states : 1 = false; bool apply_compatibility_settings : 1 = true; @@ -180,6 +179,8 @@ struct Settings float gpu_pgxp_tolerance = -1.0f; float gpu_pgxp_depth_clear_threshold = DEFAULT_GPU_PGXP_DEPTH_THRESHOLD / GPU_PGXP_DEPTH_THRESHOLD_SCALE; + SaveStateCompressionMode save_state_compression = DEFAULT_SAVE_STATE_COMPRESSION_MODE; + u8 cdrom_readahead_sectors = DEFAULT_CDROM_READAHEAD_SECTORS; CDROMMechaconVersion cdrom_mechacon_version = DEFAULT_CDROM_MECHACON_VERSION; bool cdrom_region_check : 1 = false; @@ -459,6 +460,10 @@ struct Settings static const char* GetCDROMMechVersionName(CDROMMechaconVersion mode); static const char* GetCDROMMechVersionDisplayName(CDROMMechaconVersion mode); + static std::optional ParseSaveStateCompressionModeName(const char* str); + static const char* GetSaveStateCompressionModeName(SaveStateCompressionMode mode); + static const char* GetSaveStateCompressionModeDisplayName(SaveStateCompressionMode mode); + static constexpr GPURenderer DEFAULT_GPU_RENDERER = GPURenderer::Automatic; static constexpr GPUTextureFilter DEFAULT_GPU_TEXTURE_FILTER = GPUTextureFilter::Nearest; static constexpr GPULineDetectMode DEFAULT_GPU_LINE_DETECT_MODE = GPULineDetectMode::Disabled; @@ -510,7 +515,7 @@ struct Settings static constexpr LOGLEVEL DEFAULT_LOG_LEVEL = LOGLEVEL_INFO; - static constexpr bool DEFAULT_SAVE_STATE_COMPRESSION = true; + static constexpr SaveStateCompressionMode DEFAULT_SAVE_STATE_COMPRESSION_MODE = SaveStateCompressionMode::ZstDefault; // Enable console logging by default on Linux platforms. #if defined(__linux__) && !defined(__ANDROID__) diff --git a/src/core/system.cpp b/src/core/system.cpp index 873cde483..f7ea10949 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -190,12 +190,12 @@ static bool LoadStateFromBuffer(const SaveStateBuffer& buffer, Error* error, boo static bool LoadStateBufferFromFile(SaveStateBuffer* buffer, std::FILE* fp, Error* error, bool read_title, bool read_media_path, bool read_screenshot, bool read_data); static bool ReadAndDecompressStateData(std::FILE* fp, std::span dst, u32 file_offset, u32 compressed_size, - SaveStateCompression method, Error* error); + SAVE_STATE_HEADER::CompressionType method, Error* error); static bool SaveStateToBuffer(SaveStateBuffer* buffer, Error* error, u32 screenshot_size = 256); static bool SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, Error* error, - SaveStateCompression screenshot_compression = SaveStateCompression::ZLib, - SaveStateCompression data_compression = SaveStateCompression::ZStd); -static u32 CompressAndWriteStateData(std::FILE* fp, std::span src, SaveStateCompression method, Error* error); + SaveStateCompressionMode compression_mode); +static u32 CompressAndWriteStateData(std::FILE* fp, std::span src, SaveStateCompressionMode method, + u32* header_type, Error* error); static bool DoState(StateWrapper& sw, GPUTexture** host_texture, bool update_display, bool is_memory_state); static void SetRewinding(bool enabled); @@ -2699,9 +2699,9 @@ bool System::LoadStateBufferFromFile(SaveStateBuffer* buffer, std::FILE* fp, Err buffer->screenshot.SetSize(header.screenshot_width, header.screenshot_height); const u32 uncompressed_size = buffer->screenshot.GetPitch() * buffer->screenshot.GetHeight(); const u32 compressed_size = (header.version >= 69) ? header.screenshot_compressed_size : uncompressed_size; - const SaveStateCompression compression_type = - (header.version >= 69) ? static_cast(header.screenshot_compression_type) : - SaveStateCompression::None; + const SAVE_STATE_HEADER::CompressionType compression_type = + (header.version >= 69) ? static_cast(header.screenshot_compression_type) : + SAVE_STATE_HEADER::CompressionType::None; if (!ReadAndDecompressStateData( fp, std::span(reinterpret_cast(buffer->screenshot.GetPixels()), uncompressed_size), header.offset_to_screenshot, compressed_size, compression_type, error)) [[unlikely]] @@ -2716,8 +2716,8 @@ bool System::LoadStateBufferFromFile(SaveStateBuffer* buffer, std::FILE* fp, Err buffer->state_data.resize(header.data_uncompressed_size); buffer->state_size = header.data_uncompressed_size; if (!ReadAndDecompressStateData(fp, buffer->state_data.span(), header.offset_to_data, header.data_compressed_size, - static_cast(header.data_compression_type), error)) - [[unlikely]] + static_cast(header.data_compression_type), + error)) [[unlikely]] { return false; } @@ -2727,12 +2727,12 @@ bool System::LoadStateBufferFromFile(SaveStateBuffer* buffer, std::FILE* fp, Err } bool System::ReadAndDecompressStateData(std::FILE* fp, std::span dst, u32 file_offset, u32 compressed_size, - SaveStateCompression method, Error* error) + SAVE_STATE_HEADER::CompressionType method, Error* error) { if (!FileSystem::FSeek64(fp, file_offset, SEEK_SET, error)) return false; - if (method == SaveStateCompression::None) + if (method == SAVE_STATE_HEADER::CompressionType::None) { // Feed through. if (std::fread(dst.data(), dst.size(), 1, fp) != 1) [[unlikely]] @@ -2751,7 +2751,7 @@ bool System::ReadAndDecompressStateData(std::FILE* fp, std::span dst, u32 fi return false; } - if (method == SaveStateCompression::ZLib) + if (method == SAVE_STATE_HEADER::CompressionType::Deflate) { uLong source_len = compressed_size; uLong dest_len = static_cast(dst.size()); @@ -2772,7 +2772,7 @@ bool System::ReadAndDecompressStateData(std::FILE* fp, std::span dst, u32 fi return true; } - else if (method == SaveStateCompression::ZStd) + else if (method == SAVE_STATE_HEADER::CompressionType::Zstandard) { const size_t result = ZSTD_decompress(dst.data(), dst.size(), compressed_data.data(), compressed_size); if (ZSTD_isError(result)) [[unlikely]] @@ -2832,9 +2832,7 @@ bool System::SaveState(const char* path, Error* error, bool backup_existing_save INFO_LOG("Saving state to '{}'...", path); - const SaveStateCompression compression = - g_settings.compress_save_states ? SaveStateCompression::ZStd : SaveStateCompression::None; - if (!SaveStateBufferToFile(buffer, fp.get(), error, compression, compression)) + if (!SaveStateBufferToFile(buffer, fp.get(), error, g_settings.save_state_compression)) { FileSystem::DiscardAtomicRenamedFile(fp); return false; @@ -2927,7 +2925,7 @@ bool System::SaveStateToBuffer(SaveStateBuffer* buffer, Error* error, u32 screen } bool System::SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, Error* error, - SaveStateCompression screenshot_compression, SaveStateCompression data_compression) + SaveStateCompressionMode compression) { // Header gets rewritten below. SAVE_STATE_HEADER header = {}; @@ -2961,7 +2959,6 @@ bool System::SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, if (buffer.screenshot.IsValid()) { DebugAssert(FileSystem::FTell64(fp) == static_cast(file_position)); - header.screenshot_compression_type = static_cast(screenshot_compression); header.screenshot_width = buffer.screenshot.GetWidth(); header.screenshot_height = buffer.screenshot.GetHeight(); header.offset_to_screenshot = file_position; @@ -2969,7 +2966,7 @@ bool System::SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, CompressAndWriteStateData(fp, std::span(reinterpret_cast(buffer.screenshot.GetPixels()), buffer.screenshot.GetPitch() * buffer.screenshot.GetHeight()), - screenshot_compression, error); + compression, &header.screenshot_compression_type, error); if (header.screenshot_compressed_size == 0) return false; file_position += header.screenshot_compressed_size; @@ -2977,10 +2974,9 @@ bool System::SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, DebugAssert(buffer.state_size > 0); header.offset_to_data = file_position; - header.data_compression_type = static_cast(data_compression); header.data_uncompressed_size = static_cast(buffer.state_size); - header.data_compressed_size = - CompressAndWriteStateData(fp, buffer.state_data.cspan(0, buffer.state_size), data_compression, error); + header.data_compressed_size = CompressAndWriteStateData(fp, buffer.state_data.cspan(0, buffer.state_size), + compression, &header.data_compression_type, error); if (header.data_compressed_size == 0) return false; @@ -3001,9 +2997,10 @@ bool System::SaveStateBufferToFile(const SaveStateBuffer& buffer, std::FILE* fp, return true; } -u32 System::CompressAndWriteStateData(std::FILE* fp, std::span src, SaveStateCompression method, Error* error) +u32 System::CompressAndWriteStateData(std::FILE* fp, std::span src, SaveStateCompressionMode method, + u32* header_type, Error* error) { - if (method == SaveStateCompression::None) + if (method == SaveStateCompressionMode::Uncompressed) { if (std::fwrite(src.data(), src.size(), 1, fp) != 1) [[unlikely]] { @@ -3011,33 +3008,40 @@ u32 System::CompressAndWriteStateData(std::FILE* fp, std::span src, Sa return 0; } + *header_type = static_cast(SAVE_STATE_HEADER::CompressionType::None); return static_cast(src.size()); } DynamicHeapArray buffer; u32 write_size; - if (method == SaveStateCompression::ZLib) + if (method >= SaveStateCompressionMode::DeflateLow && method <= SaveStateCompressionMode::DeflateHigh) { const size_t buffer_size = compressBound(static_cast(src.size())); buffer.resize(buffer_size); uLongf compressed_size = static_cast(buffer_size); - const int err = - compress2(buffer.data(), &compressed_size, src.data(), static_cast(src.size()), Z_DEFAULT_COMPRESSION); + const int level = + ((method == SaveStateCompressionMode::DeflateLow) ? + Z_BEST_SPEED : + ((method == SaveStateCompressionMode::DeflateHigh) ? Z_BEST_COMPRESSION : Z_DEFAULT_COMPRESSION)); + const int err = compress2(buffer.data(), &compressed_size, src.data(), static_cast(src.size()), level); if (err != Z_OK) [[unlikely]] { Error::SetStringFmt(error, "compress2() failed: {}", err); return 0; } + *header_type = static_cast(SAVE_STATE_HEADER::CompressionType::Deflate); write_size = static_cast(compressed_size); } - else if (method == SaveStateCompression::ZStd) + else if (method >= SaveStateCompressionMode::ZstLow && method <= SaveStateCompressionMode::ZstHigh) { const size_t buffer_size = ZSTD_compressBound(src.size()); buffer.resize(buffer_size); - const size_t compressed_size = ZSTD_compress(buffer.data(), buffer_size, src.data(), src.size(), 0); + const int level = + ((method == SaveStateCompressionMode::ZstLow) ? 1 : ((method == SaveStateCompressionMode::ZstHigh) ? 19 : 0)); + const size_t compressed_size = ZSTD_compress(buffer.data(), buffer_size, src.data(), src.size(), level); if (ZSTD_isError(compressed_size)) [[unlikely]] { const char* errstr = ZSTD_getErrorString(ZSTD_getErrorCode(compressed_size)); @@ -3045,6 +3049,7 @@ u32 System::CompressAndWriteStateData(std::FILE* fp, std::span src, Sa return 0; } + *header_type = static_cast(SAVE_STATE_HEADER::CompressionType::Zstandard); write_size = static_cast(compressed_size); } else [[unlikely]] diff --git a/src/core/types.h b/src/core/types.h index ac679e744..793d71f96 100644 --- a/src/core/types.h +++ b/src/core/types.h @@ -235,7 +235,7 @@ enum : u32 NUM_MULTITAPS = 2 }; -enum class CPUFastmemMode +enum class CPUFastmemMode : u8 { Disabled, MMap, @@ -261,3 +261,16 @@ enum class CDROMMechaconVersion : u8 Count, }; + +enum class SaveStateCompressionMode : u8 +{ + Uncompressed, + DeflateLow, + DeflateDefault, + DeflateHigh, + ZstLow, + ZstDefault, + ZstHigh, + + Count, +}; diff --git a/src/duckstation-qt/advancedsettingswidget.cpp b/src/duckstation-qt/advancedsettingswidget.cpp index 472d8cd7a..c457b2f96 100644 --- a/src/duckstation-qt/advancedsettingswidget.cpp +++ b/src/duckstation-qt/advancedsettingswidget.cpp @@ -222,8 +222,11 @@ void AdvancedSettingsWidget::addTweakOptions() "IncreaseTimerResolution", true); addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Load Devices From Save States"), "Main", "LoadDevicesFromSaveStates", false); - addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Compress Save States"), "Main", "CompressSaveStates", - Settings::DEFAULT_SAVE_STATE_COMPRESSION); + addChoiceTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Save State Compression"), "Main", "SaveStateCompression", + &Settings::ParseSaveStateCompressionModeName, &Settings::GetSaveStateCompressionModeName, + &Settings::GetSaveStateCompressionModeDisplayName, + static_cast(SaveStateCompressionMode::Count), + Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE); if (m_dialog->isPerGameSettings()) { @@ -284,7 +287,8 @@ void AdvancedSettingsWidget::onResetToDefaultClicked() setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Apply compatibility settings setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Increase Timer Resolution setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Load Devices From Save States - setBooleanTweakOption(m_ui.tweakOptionTable, i++, Settings::DEFAULT_SAVE_STATE_COMPRESSION); // Compress Save States + setChoiceTweakOption(m_ui.tweakOptionTable, i++, + Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE); // Save State Compression setIntRangeTweakOption(m_ui.tweakOptionTable, i++, static_cast(Settings::DEFAULT_DMA_MAX_SLICE_TICKS)); // DMA max slice ticks setIntRangeTweakOption(m_ui.tweakOptionTable, i++,