diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c275102 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,46 @@ +# Normalize every text file to LF in the repository. +# The build runs under Linux/MinGW containers, so LF is also used in the +# working tree; Windows editors and toolchains handle LF fine. +* text=auto eol=lf + +# Windows-only files that must keep CRLF in the working tree. +*.bat text eol=crlf +*.cmd text eol=crlf +*.sln text eol=crlf +*.vcproj text eol=crlf +*.vcxproj text eol=crlf +*.props text eol=crlf +*.filters text eol=crlf + +# Files that must keep LF even if a Windows editor rewrites them. +*.sh text eol=lf +*.in text eol=lf +*.cmake text eol=lf +*.mk text eol=lf +Makefile text eol=lf +Dockerfile text eol=lf + +# Binary files - never touch the contents. +*.bin binary +*.ico binary +*.ttf binary +*.otf binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.bmp binary +*.zip binary +*.7z binary +*.gz binary +*.dll binary +*.exe binary +*.lib binary +*.a binary +*.o binary +*.obj binary +*.pdb binary + +# Vendored code is stored and checked out byte-for-byte as upstream ships it, +# so re-importing a library never produces line-ending-only diffs. +src/spice2x/external/** -text diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d610b0..2762ea0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,49 +1,49 @@ -name: Draft Release - -on: - workflow_dispatch: - -permissions: - contents: write - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - release: - name: Build and Draft Release - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./src/spice2x - steps: - - uses: actions/checkout@v5 - with: - ref: main - fetch-depth: 0 - - name: Clean leftover build artifacts - run: | - rm -rf .ccache dist bin cmake-build-* - - name: Compile - run: ./build_docker.sh - - name: Determine release name from dist filename - run: | - dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')") - # strip the ".zip" to get the base name, e.g. spice2x-26-06-28 - name="${dist%.zip}" - # the tag is the date portion, e.g. 26-06-28 - tag="${name#spice2x-}" - echo "RELEASE_NAME=$name" >> $GITHUB_ENV - echo "RELEASE_TAG=$tag" >> $GITHUB_ENV - - name: Create draft release - uses: softprops/action-gh-release@v3 - with: - draft: true - prerelease: true - tag_name: ${{ env.RELEASE_TAG }} - name: ${{ env.RELEASE_NAME }} - target_commitish: main - generate_release_notes: true - files: | - src/spice2x/dist/spice2x-*.zip +name: Draft Release + +on: + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + release: + name: Build and Draft Release + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./src/spice2x + steps: + - uses: actions/checkout@v5 + with: + ref: main + fetch-depth: 0 + - name: Clean leftover build artifacts + run: | + rm -rf .ccache dist bin cmake-build-* + - name: Compile + run: ./build_docker.sh + - name: Determine release name from dist filename + run: | + dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')") + # strip the ".zip" to get the base name, e.g. spice2x-26-06-28 + name="${dist%.zip}" + # the tag is the date portion, e.g. 26-06-28 + tag="${name#spice2x-}" + echo "RELEASE_NAME=$name" >> $GITHUB_ENV + echo "RELEASE_TAG=$tag" >> $GITHUB_ENV + - name: Create draft release + uses: softprops/action-gh-release@v3 + with: + draft: true + prerelease: true + tag_name: ${{ env.RELEASE_TAG }} + name: ${{ env.RELEASE_NAME }} + target_commitish: main + generate_release_notes: true + files: | + src/spice2x/dist/spice2x-*.zip diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index cb30b7a..d4fb10f 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -254,6 +254,57 @@ add_subdirectory(external/imgui EXCLUDE_FROM_ALL) add_subdirectory(external/minhook EXCLUDE_FROM_ALL) add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL) +# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own +# sysroot and cannot see it, so those targets build without JPEG support. +add_library(spice_jpeg INTERFACE) +if(NOT SPICE_XP) + # search static archives only: the mingw package also ships an import library, + # and linking that one would pull in a libjpeg DLL at runtime + set(SPICE_JPEG_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX}) + find_package(JPEG) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_JPEG_SUFFIXES}) + + if(JPEG_FOUND) + target_link_libraries(spice_jpeg INTERFACE JPEG::JPEG) + target_compile_definitions(spice_jpeg INTERFACE SPICE_JPEG=1) + else() + message(WARNING + "libjpeg-turbo not found: screen capture over the API is disabled") + endif() +endif() + +# x264 for the API H.264 video stream, installed into the mingw sysroots by the +# deps image. The WinXP toolchains deliberately go without it and serve MJPEG only. +add_library(spice_x264 INTERFACE) +if(NOT SPICE_XP) + # search static archives only: the mingw package also ships an import library, + # and linking that one would pull in a libx264 DLL at runtime + set(SPICE_X264_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES}) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX}) + find_library(X264_LIBRARY NAMES x264 libx264) + set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_X264_SUFFIXES}) + + find_path(X264_INCLUDE_DIR NAMES x264.h) + if(X264_LIBRARY AND X264_INCLUDE_DIR) + target_include_directories(spice_x264 INTERFACE "${X264_INCLUDE_DIR}") + target_link_libraries(spice_x264 INTERFACE "${X264_LIBRARY}") + target_compile_definitions(spice_x264 INTERFACE SPICE_H264=1) + else() + message(WARNING + "x264 not found: the api video stream will only offer MJPEG") + endif() +endif() + +# fpng's SIMD needs the whole unit built for SSE4.1, which its runtime CPU check +# cannot undo, so keep it scalar rather than raising the CPU baseline +set_source_files_properties(external/fpng/fpng.cpp PROPERTIES + COMPILE_DEFINITIONS "FPNG_NO_SSE=1") +if(NOT MSVC) + set_source_files_properties(external/fpng/fpng.cpp PROPERTIES + COMPILE_OPTIONS "-fno-strict-aliasing") +endif() + # set link time optimizations (disabled for Debug builds for speed, disabled # for RelWithDebInfo builds due to "lto1: error: two or more sections for" # errors) @@ -328,6 +379,10 @@ set(SOURCE_FILES ${SOURCE_FILES} # api api/controller.cpp api/websocket.cpp + api/capture_pump.cpp + api/h264_stream.cpp + api/stream_format.cpp + api/stream_server.cpp api/request.cpp api/response.cpp api/module.cpp @@ -391,7 +446,7 @@ set(SOURCE_FILES ${SOURCE_FILES} external/tinyxml2/tinyxml2.cpp external/http-parser/http_parser.c external/usbhidusage/usb-hid-usage.c - external/toojpeg/toojpeg.cpp + external/fpng/fpng.cpp external/scard/scard.cpp # games @@ -548,10 +603,12 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/devicehook.cpp hooks/graphics/graphics.cpp hooks/graphics/graphics_windowed.cpp + hooks/graphics/jpeg_encoder.cpp hooks/graphics/nvapi_impl.cpp hooks/graphics/nvapi_hook.cpp hooks/graphics/nvenc_hook.cpp hooks/graphics/backends/d3d9/d3d9_backend.cpp + hooks/graphics/backends/d3d9/d3d9_readback.cpp hooks/graphics/backends/d3d9/d3d9_screenshot.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp hooks/graphics/backends/d3d9/d3d9_gfdm.cpp @@ -594,6 +651,7 @@ set(SOURCE_FILES ${SOURCE_FILES} misc/device.cpp misc/eamuse.cpp misc/extdev.cpp + misc/hotkeys.cpp misc/sciunit.cpp misc/sde.cpp misc/wintouchemu.cpp @@ -755,7 +813,7 @@ endfunction() add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES}) target_link_libraries(spicetools_spice_objs PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) target_link_libraries(spicetools_spice_objs PUBLIC winscard) if(NOT MSVC) @@ -795,7 +853,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES}) target_link_libraries(spicetools_spice_linux PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux") target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1) @@ -813,8 +871,8 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES}) # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break target_link_libraries(spicetools_spice64 PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) - + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) +target_link_libraries(spicetools_spice64 PUBLIC winscard) set_target_properties(spicetools_spice64 PROPERTIES PREFIX "") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) @@ -836,7 +894,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES}) # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break target_link_libraries(spicetools_spice64_linux PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 - PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) + PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264) set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) @@ -855,6 +913,7 @@ endif() set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) +# the configurator serves neither the API nor the video stream, so it needs no codecs target_link_libraries(spicetools_cfg PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) diff --git a/src/spice2x/README.md b/src/spice2x/README.md index e6eae30..11f5bfd 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -291,6 +291,42 @@ which also means that your hex edits are applicable directly. - image_resize_set_scene(scene: int) - sets the active scene for image resize state; set to 0 to disable resize +## Video Stream + +Separate from the JSON API, spice can serve the mirrored screen as a video +stream over plain HTTP. Enable it with `-apistream`. It listens on the API port +plus two, in the same way the WebSocket server uses the API port plus one, so +`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too. + +Two formats are served: + + http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace + http://host:1339/stream.h264 H.264 annex-b, no container + +All accept the same optional query parameters: + +- `screen` - which screen to mirror, 0-3. Defaults to the subscreen when the + game has one, otherwise the main screen. +- `fps` - frames per second, 1-60. Default 30. +- `q` - quality, 1-100. Default 70. This is the JPEG quality for `stream.mjpg` + and is mapped onto the H.264 rate factor for `stream.h264`, so the same number + does not mean the same thing for both. + +For example: + + http://host:1339/stream.h264?screen=1&fps=30&q=70 + +See the wiki for format tradeoffs, latency tuning, testing commands and client +notes. + +The stream is view only. Touch and other input still go through the JSON API, +so a companion app needs both. There is no authentication on the stream port - +anyone who can reach it can watch the screen. + +WinXP builds have no video stream. Neither encoder is compiled in, so every +endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for +the same reason. + ## Native wrapper libraries Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python. Python is the only one that is fully spec compliant. diff --git a/src/spice2x/api/capture_pump.cpp b/src/spice2x/api/capture_pump.cpp new file mode 100644 index 0000000..fe75e56 --- /dev/null +++ b/src/spice2x/api/capture_pump.cpp @@ -0,0 +1,58 @@ +#include "capture_pump.h" + +#include +#include + +#include "hooks/graphics/graphics.h" + +namespace api::capture_pump { + + namespace { + + std::array CONSUMER_M; + + std::mutex CLAIMED_M; + std::array CLAIMED {}; + + bool valid_screen(int screen) { + return 0 <= screen && screen < static_cast(GRAPHICS_CAPTURE_SCREEN_NO); + } + } + + bool capture_direct(int screen, std::shared_ptr &out, int divide, + uint64_t *timestamp, int *width, int *height) { + + if (!valid_screen(screen)) { + return false; + } + + std::lock_guard lock(CONSUMER_M[screen]); + graphics_capture_trigger(screen); + return graphics_capture_receive_raw( + screen, out, divide, timestamp, width, height); + } + + bool claim_screen(int screen) { + if (!valid_screen(screen)) { + return false; + } + + std::lock_guard lock(CLAIMED_M); + + if (CLAIMED[screen]) { + return false; + } + + CLAIMED[screen] = true; + return true; + } + + void release_screen(int screen) { + if (!valid_screen(screen)) { + return; + } + + std::lock_guard lock(CLAIMED_M); + CLAIMED[screen] = false; + } +} diff --git a/src/spice2x/api/capture_pump.h b/src/spice2x/api/capture_pump.h new file mode 100644 index 0000000..b8ae9d0 --- /dev/null +++ b/src/spice2x/api/capture_pump.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +namespace api::capture_pump { + + struct Frame { + // packed 24bpp RGB, width * height * 3 bytes + std::shared_ptr pixels; + uint64_t timestamp = 0; + int width = 0; + int height = 0; + }; + + // the graphics layer has one capture slot per screen, so concurrent waiters would steal + // each other's frames; everything that captures goes through here to keep it serialized + bool capture_direct(int screen, std::shared_ptr &out, int divide, + uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr); + + // a screen carries one stream at a time; false when another connection already holds it + bool claim_screen(int screen); + void release_screen(int screen); +} diff --git a/src/spice2x/api/h264_stream.cpp b/src/spice2x/api/h264_stream.cpp new file mode 100644 index 0000000..7774220 --- /dev/null +++ b/src/spice2x/api/h264_stream.cpp @@ -0,0 +1,224 @@ +#include "h264_stream.h" + +#ifdef SPICE_H264 + +#include + +#include + +#include "util/logging.h" + +namespace api { + + namespace { + + // BT.601 limited range, the range every decoder assumes for H.264 without + // explicit colour metadata + inline uint8_t rgb_to_y(int r, int g, int b) { + return static_cast(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16); + } + + inline uint8_t rgb_to_u(int r, int g, int b) { + return static_cast(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128); + } + + inline uint8_t rgb_to_v(int r, int g, int b) { + return static_cast(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128); + } + + // a bare annex-b elementary stream, one encoder per connection so every client + // starts on its own keyframe. no container, so nothing here keeps a media clock + class H264Writer : public StreamWriter { + public: + + H264Writer(int quality, int fps) : quality(quality), fps(fps) {} + + ~H264Writer() override { + this->close(); + } + + std::string content_type() const override { + return "video/h264"; + } + + bool write(const StreamSend &send, const capture_pump::Frame &frame) override { + + // I420 needs even dimensions + const int width = frame.width & ~1; + const int height = frame.height & ~1; + if (width <= 0 || height <= 0) { + return true; + } + + if (this->encoder == nullptr) { + if (!this->open(width, height)) { + return false; + } + } else if (width != this->width || height != this->height) { + // the encoder is fixed at the size it opened with; let the client reconnect + log_info("api::stream", "capture size changed, ending H.264 client"); + return false; + } + + this->convert(frame.pixels.get(), frame.width); + + this->picture.i_pts = this->frame_index; + + x264_nal_t *nals = nullptr; + int nal_count = 0; + x264_picture_t picture_out; + const int size = x264_encoder_encode( + this->encoder, &nals, &nal_count, &this->picture, &picture_out); + + if (size < 0) { + log_warning("api::stream", "H.264 encode failed"); + return false; + } + + this->frame_index++; + + if (size == 0) { + return true; + } + + // x264 lays every NAL of the frame out back to back. an SEI or delimiter + // carries no picture, so only the parameter sets and the slice go through + this->annexb.clear(); + for (int i = 0; i < nal_count; i++) { + switch (nals[i].i_type) { + case NAL_SEI: + case NAL_AUD: + case NAL_FILLER: + continue; + default: + break; + } + this->annexb.insert(this->annexb.end(), + nals[i].p_payload, nals[i].p_payload + nals[i].i_payload); + } + + if (this->annexb.empty()) { + return true; + } + + return send(this->annexb.data(), this->annexb.size()); + } + + private: + + bool open(int width, int height) { + + x264_param_t param; + if (x264_param_default_preset(¶m, "ultrafast", "zerolatency") < 0) { + return false; + } + + param.i_csp = X264_CSP_I420; + param.i_width = width; + param.i_height = height; + param.i_fps_num = this->fps; + param.i_fps_den = 1; + param.i_threads = 1; + param.b_annexb = 1; + // SPS/PPS ahead of every IDR, so a client can start decoding cold + param.b_repeat_headers = 1; + // a keyframe every two seconds bounds how long a new client waits + param.i_keyint_max = this->fps * 2; + param.i_log_level = X264_LOG_NONE; + param.rc.i_rc_method = X264_RC_CRF; + param.rc.f_rf_constant = 40.0f - (this->quality * 0.25f); + + // baseline keeps hardware decode available on the widest range of phones + if (x264_param_apply_profile(¶m, "baseline") < 0) { + return false; + } + + this->encoder = x264_encoder_open(¶m); + if (this->encoder == nullptr) { + log_warning("api::stream", "could not open the H.264 encoder"); + return false; + } + + if (x264_picture_alloc(&this->picture, X264_CSP_I420, width, height) < 0) { + this->close(); + return false; + } + this->picture_ready = true; + + this->width = width; + this->height = height; + return true; + } + + void close() { + if (this->picture_ready) { + x264_picture_clean(&this->picture); + this->picture_ready = false; + } + if (this->encoder != nullptr) { + x264_encoder_close(this->encoder); + this->encoder = nullptr; + } + } + + // packed 24bpp RGB to I420, averaging each 2x2 block for the chroma planes + void convert(const uint8_t *rgb, int source_width) { + + uint8_t *plane_y = this->picture.img.plane[0]; + uint8_t *plane_u = this->picture.img.plane[1]; + uint8_t *plane_v = this->picture.img.plane[2]; + const int stride_y = this->picture.img.i_stride[0]; + const int stride_u = this->picture.img.i_stride[1]; + const int stride_v = this->picture.img.i_stride[2]; + + for (int y = 0; y < this->height; y++) { + const uint8_t *row = rgb + static_cast(y) * source_width * 3; + uint8_t *out_y = plane_y + static_cast(y) * stride_y; + + for (int x = 0; x < this->width; x++) { + const uint8_t *pixel = row + x * 3; + out_y[x] = rgb_to_y(pixel[0], pixel[1], pixel[2]); + } + } + + for (int y = 0; y < this->height / 2; y++) { + const uint8_t *row0 = rgb + static_cast(y * 2) * source_width * 3; + const uint8_t *row1 = row0 + static_cast(source_width) * 3; + uint8_t *out_u = plane_u + static_cast(y) * stride_u; + uint8_t *out_v = plane_v + static_cast(y) * stride_v; + + for (int x = 0; x < this->width / 2; x++) { + const uint8_t *p00 = row0 + (x * 2) * 3; + const uint8_t *p01 = p00 + 3; + const uint8_t *p10 = row1 + (x * 2) * 3; + const uint8_t *p11 = p10 + 3; + + const int r = (p00[0] + p01[0] + p10[0] + p11[0] + 2) / 4; + const int g = (p00[1] + p01[1] + p10[1] + p11[1] + 2) / 4; + const int b = (p00[2] + p01[2] + p10[2] + p11[2] + 2) / 4; + + out_u[x] = rgb_to_u(r, g, b); + out_v[x] = rgb_to_v(r, g, b); + } + } + } + + int quality; + int fps; + int width = 0; + int height = 0; + int64_t frame_index = 0; + std::vector annexb; + + x264_t *encoder = nullptr; + x264_picture_t picture {}; + bool picture_ready = false; + }; + } + + std::unique_ptr make_h264_writer(int quality, int fps) { + return std::make_unique(quality, fps); + } +} + +#endif // SPICE_H264 diff --git a/src/spice2x/api/h264_stream.h b/src/spice2x/api/h264_stream.h new file mode 100644 index 0000000..494b0a5 --- /dev/null +++ b/src/spice2x/api/h264_stream.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "stream_format.h" + +namespace api { + + // bare annex-b H.264; null when the build has no encoder + std::unique_ptr make_h264_writer(int quality, int fps); +} diff --git a/src/spice2x/api/modules/capture.cpp b/src/spice2x/api/modules/capture.cpp index 7d393f1..b8ab2b1 100644 --- a/src/spice2x/api/modules/capture.cpp +++ b/src/spice2x/api/modules/capture.cpp @@ -2,8 +2,10 @@ #include #include #include +#include "api/capture_pump.h" #include "external/rapidjson/document.h" #include "hooks/graphics/graphics.h" +#include "hooks/graphics/jpeg_encoder.h" #include "util/crypt.h" using namespace std::placeholders; @@ -93,8 +95,6 @@ namespace api::modules { * reduce: uint for dividing image size */ void Capture::get_jpg(Request &req, Response &res) { - CAPTURE_BUFFER.clear(); - CAPTURE_BUFFER.reserve(1024 * 128); // settings int screen = 0; @@ -120,10 +120,16 @@ namespace api::modules { uint64_t timestamp = 0; int width = 0; int height = 0; - graphics_capture_trigger(screen); - bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) { - CAPTURE_BUFFER.push_back(byte); - }, true, quality, true, divide, ×tamp, &width, &height); + + std::shared_ptr pixels; + bool success = capture_pump::capture_direct( + screen, pixels, divide, ×tamp, &width, &height); + + if (success) { + CAPTURE_BUFFER.clear(); + success = jpeg_encoder::encode( + CAPTURE_BUFFER, pixels.get(), width, height, quality); + } if (success) { add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res); diff --git a/src/spice2x/api/modules/ddr.cpp b/src/spice2x/api/modules/ddr.cpp index 6b4171f..2c3f3fe 100644 --- a/src/spice2x/api/modules/ddr.cpp +++ b/src/spice2x/api/modules/ddr.cpp @@ -1,54 +1,54 @@ -#include "ddr.h" -#include -#include "external/rapidjson/document.h" -#include "games/ddr/ddr.h" - -using namespace std::placeholders; -using namespace rapidjson; - -namespace api::modules { - - DDR::DDR() : Module("ddr") { - functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2); - } - - /** - * Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI - */ - void DDR::tapeled_get(Request &req, Response &res) { - static const char* device_names[11] = { - "p1_foot_up", - "p1_foot_right", - "p1_foot_left", - "p1_foot_down", - "p2_foot_up", - "p2_foot_right", - "p2_foot_left", - "p2_foot_down", - "top_panel", - "monitor_left", - "monitor_right" - }; - - Value response_object(kObjectType); - - // Iterate through each device and dump its lights data into the response - for (size_t device = 0; device < 11; device++) { - size_t num_leds = 25; - if (device > 7) - num_leds = 50; - - Value light_state(kArrayType); - light_state.Reserve(num_leds * 3, res.doc()->GetAllocator()); - for (size_t led = 0; led < num_leds; led++) { - light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator()); - light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator()); - light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator()); - } - - response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator()); - } - - res.add_data(response_object); - } -} +#include "ddr.h" +#include +#include "external/rapidjson/document.h" +#include "games/ddr/ddr.h" + +using namespace std::placeholders; +using namespace rapidjson; + +namespace api::modules { + + DDR::DDR() : Module("ddr") { + functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2); + } + + /** + * Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI + */ + void DDR::tapeled_get(Request &req, Response &res) { + static const char* device_names[11] = { + "p1_foot_up", + "p1_foot_right", + "p1_foot_left", + "p1_foot_down", + "p2_foot_up", + "p2_foot_right", + "p2_foot_left", + "p2_foot_down", + "top_panel", + "monitor_left", + "monitor_right" + }; + + Value response_object(kObjectType); + + // Iterate through each device and dump its lights data into the response + for (size_t device = 0; device < 11; device++) { + size_t num_leds = 25; + if (device > 7) + num_leds = 50; + + Value light_state(kArrayType); + light_state.Reserve(num_leds * 3, res.doc()->GetAllocator()); + for (size_t led = 0; led < num_leds; led++) { + light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator()); + light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator()); + light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator()); + } + + response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator()); + } + + res.add_data(response_object); + } +} diff --git a/src/spice2x/api/modules/ddr.h b/src/spice2x/api/modules/ddr.h index 13a8fc5..95ed300 100644 --- a/src/spice2x/api/modules/ddr.h +++ b/src/spice2x/api/modules/ddr.h @@ -1,17 +1,17 @@ -#pragma once - -#include -#include "api/module.h" -#include "api/request.h" - -namespace api::modules { - - class DDR : public Module { - public: - DDR(); - - private: - // function definitions - void tapeled_get(Request &req, Response &res); - }; -} +#pragma once + +#include +#include "api/module.h" +#include "api/request.h" + +namespace api::modules { + + class DDR : public Module { + public: + DDR(); + + private: + // function definitions + void tapeled_get(Request &req, Response &res); + }; +} diff --git a/src/spice2x/api/modules/touch.cpp b/src/spice2x/api/modules/touch.cpp index 3986651..217d710 100644 --- a/src/spice2x/api/modules/touch.cpp +++ b/src/spice2x/api/modules/touch.cpp @@ -12,7 +12,9 @@ #include "touch/touch.h" #include "touch/native/inject.h" #include "touch/native/nativetouchhook.h" +#include "touch/native/transform.h" #include "util/utils.h" +#include "games/gitadora/gitadora.h" #include "games/iidx/iidx.h" using namespace std::placeholders; @@ -40,6 +42,35 @@ namespace api::modules { return nativetouch::inject::inject_synthetic_touch(position, true); } + // map API coordinates onto the touch space SDVX reads, which depends on how it is displayed + static void sdvx_touch_errata( + int &x, int &y, bool use_native, int canvas_w, int canvas_h) { + + // windowed coordinates already match the sub screen window they land on + if (GRAPHICS_WINDOWED) { + return; + } + + // landscape mode: native injection hands the game these coordinates + // unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead + // rotates them later through the subscreen overlay + if (GRAPHICS_FS_ORIENTATION_SWAP) { + if (use_native) { + POINT position { x, y }; + if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) { + x = position.x; + y = position.y; + } + } + return; + } + + // rotate into the portrait touch space + const int x_raw = x; + x = canvas_w - y; + y = x_raw; + } + Touch::Touch() : Module("touch") { is_sdvx = avs::game::is_model("KFC"); @@ -55,8 +86,8 @@ namespace api::modules { native_canvas_w = 0; native_canvas_h = 0; if (is_sdvx) { - // windowed and landscape API coordinates already match the primary screen orientation; - // fullscreen portrait coordinates are rotated by apply_touch_errata + // windowed API coordinates land on the sub screen window as-is; fullscreen + // coordinates are rotated into the game's touch space by apply_touch_errata const bool landscape_coordinates = GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP; native_canvas_w = landscape_coordinates ? 1920 : 1080; @@ -69,6 +100,10 @@ namespace api::modules { // pop'n music API touch surface native_canvas_w = 1280; native_canvas_h = 800; + } else if (games::gitadora::is_arena_model()) { + // GITADORA arena SMALL subscreen, either in its own window or in the overlay + native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH; + native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT; } functions["read"] = std::bind(&Touch::read, this, _1, _2); @@ -212,19 +247,14 @@ namespace api::modules { } void Touch::apply_touch_errata(int &x, int &y) { - int x_raw = x; - int y_raw = y; - if (is_tdj_fhd) { // deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p) // we don't know what screen is being shown on the companion and the API doesn't specify // the target of the touch events so just assume it's the sub screen - x = x_raw * 1920 / 1280; - y = y_raw * 1080 / 720; - } else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) { - // rotate API coordinates into SDVX's portrait touch space - x = 1080 - y_raw; - y = x_raw; + x = x * 1920 / 1280; + y = y * 1080 / 720; + } else if (is_sdvx) { + sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h); } } } diff --git a/src/spice2x/api/stream_format.cpp b/src/spice2x/api/stream_format.cpp new file mode 100644 index 0000000..0fce3ce --- /dev/null +++ b/src/spice2x/api/stream_format.cpp @@ -0,0 +1,70 @@ +#include "stream_format.h" + +#include + +#include "h264_stream.h" +#include "hooks/graphics/jpeg_encoder.h" + +namespace api { + + namespace { + +#ifdef SPICE_JPEG + constexpr const char *MJPEG_BOUNDARY = "spice2xframe"; + + // multipart/x-mixed-replace: every frame is a standalone JPEG, no inter-frame state + class MjpegWriter : public StreamWriter { + public: + + explicit MjpegWriter(int quality) : quality(quality) {} + + std::string content_type() const override { + return std::string("multipart/x-mixed-replace; boundary=") + MJPEG_BOUNDARY; + } + + bool write(const StreamSend &send, const capture_pump::Frame &frame) override { + this->jpeg.clear(); + if (!jpeg_encoder::encode( + this->jpeg, frame.pixels.get(), + frame.width, frame.height, this->quality)) { + // a frame the encoder rejects is not worth dropping the client over + return true; + } + + const std::string part = + "--" + std::string(MJPEG_BOUNDARY) + "\r\n" + "Content-Type: image/jpeg\r\n" + "Content-Length: " + std::to_string(this->jpeg.size()) + "\r\n" + "\r\n"; + + return send(part.data(), part.size()) + && send(this->jpeg.data(), this->jpeg.size()) + && send("\r\n", 2); + } + + private: + int quality; + std::vector jpeg; + }; +#endif + } + + // both parameters go unused on toolchains that compile in neither format + std::unique_ptr make_stream_writer( + const std::string &path, [[maybe_unused]] int quality, [[maybe_unused]] int fps) { + +#ifdef SPICE_JPEG + if (path == "/stream.mjpg") { + return std::make_unique(quality); + } +#endif + +#ifdef SPICE_H264 + if (path == "/stream.h264") { + return make_h264_writer(quality, fps); + } +#endif + + return nullptr; + } +} diff --git a/src/spice2x/api/stream_format.h b/src/spice2x/api/stream_format.h new file mode 100644 index 0000000..ed04fb9 --- /dev/null +++ b/src/spice2x/api/stream_format.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +#include "capture_pump.h" + +namespace api { + + // writes bytes to the client; false once the connection is gone + using StreamSend = std::function; + + // one wire format, instantiated per connection so it can keep encoder state across frames + class StreamWriter { + public: + virtual ~StreamWriter() = default; + + StreamWriter(const StreamWriter &) = delete; + StreamWriter &operator=(const StreamWriter &) = delete; + + // value for the HTTP Content-Type response header + virtual std::string content_type() const = 0; + + // for formats that open with an init segment; runs once before any frame + virtual bool begin(const StreamSend &send) { return true; } + + virtual bool write(const StreamSend &send, const capture_pump::Frame &frame) = 0; + + protected: + StreamWriter() = default; + }; + + // null when the path does not name a format this build supports + std::unique_ptr make_stream_writer( + const std::string &path, int quality, int fps); +} diff --git a/src/spice2x/api/stream_server.cpp b/src/spice2x/api/stream_server.cpp new file mode 100644 index 0000000..99d720d --- /dev/null +++ b/src/spice2x/api/stream_server.cpp @@ -0,0 +1,476 @@ +#include +#include + +#include "stream_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "capture_pump.h" +#include "hooks/graphics/graphics.h" +#include "stream_format.h" +#include "util/logging.h" +#include "util/utils.h" + +namespace api { + + namespace { + + struct HttpRequest { + std::string method; + std::string path; + std::map query; + }; + + bool send_all(SOCKET socket, const void *data, size_t size) { + auto cursor = reinterpret_cast(data); + size_t remaining = size; + + while (remaining > 0) { + const int sent = send(socket, cursor, static_cast(remaining), 0); + if (sent <= 0) { + return false; + } + cursor += sent; + remaining -= static_cast(sent); + } + + return true; + } + + bool send_all(SOCKET socket, const std::string &text) { + return send_all(socket, text.data(), text.size()); + } + + // a viewer leaving is normally noticed by a failing send, so a stream with no frame + // to push has to ask the socket instead + bool client_gone(SOCKET socket) { + fd_set read_set; + FD_ZERO(&read_set); + FD_SET(socket, &read_set); + + // the socket is blocking with a receive timeout, so poll before touching it + timeval immediately {}; + const int ready = select(0, &read_set, nullptr, nullptr, &immediately); + if (ready == 0) { + return false; + } + if (ready < 0) { + return true; + } + + // consumed rather than peeked: a stray byte would otherwise sit in front of the + // FIN and keep hiding it for as long as the stream runs + char discard[256]; + return recv(socket, discard, sizeof(discard), 0) <= 0; + } + + std::string url_decode(const std::string &input) { + std::string out; + out.reserve(input.size()); + + for (size_t i = 0; i < input.size(); i++) { + if (input[i] == '+') { + out.push_back(' '); + } else if (input[i] == '%' && i + 2 < input.size() + && isxdigit(static_cast(input[i + 1])) + && isxdigit(static_cast(input[i + 2]))) { + out.push_back(static_cast( + std::stoi(input.substr(i + 1, 2), nullptr, 16))); + i += 2; + } else { + out.push_back(input[i]); + } + } + + return out; + } + + void parse_query(const std::string &query, HttpRequest &request) { + size_t pos = 0; + + while (pos < query.size()) { + auto end = query.find('&', pos); + if (end == std::string::npos) { + end = query.size(); + } + + const auto pair = query.substr(pos, end - pos); + const auto split = pair.find('='); + if (split != std::string::npos && split > 0) { + request.query[url_decode(pair.substr(0, split))] = + url_decode(pair.substr(split + 1)); + } + + pos = end + 1; + } + } + + // reads the request head only; anything oversized or malformed is refused + bool read_request(SOCKET socket, size_t size_limit, HttpRequest &request) { + std::string head; + char buffer[1024]; + + while (head.find("\r\n\r\n") == std::string::npos) { + if (head.size() >= size_limit) { + return false; + } + + // read no further than the limit, so the head cannot overshoot it + const size_t budget = std::min(sizeof(buffer), size_limit - head.size()); + const int received = recv(socket, buffer, static_cast(budget), 0); + if (received <= 0) { + return false; + } + + head.append(buffer, static_cast(received)); + } + + const auto line_end = head.find("\r\n"); + const auto line = head.substr(0, line_end); + + const auto method_end = line.find(' '); + if (method_end == std::string::npos) { + return false; + } + + const auto target_end = line.find(' ', method_end + 1); + if (target_end == std::string::npos) { + return false; + } + + request.method = line.substr(0, method_end); + auto target = line.substr(method_end + 1, target_end - method_end - 1); + + const auto query_start = target.find('?'); + if (query_start != std::string::npos) { + parse_query(target.substr(query_start + 1), request); + target = target.substr(0, query_start); + } + + request.path = url_decode(target); + return true; + } + + int query_int(const HttpRequest &request, const std::string &name, int fallback, + int min, int max) { + + const auto pos = request.query.find(name); + if (pos == request.query.end()) { + return fallback; + } + + try { + return std::clamp(std::stoi(pos->second), min, max); + } catch (const std::exception &) { + return fallback; + } + } + + // an can show a cross-origin stream without this, but a browser client that + // decodes the bytes itself has to fetch() them, and fetch is subject to CORS. errors + // carry it too, or the client sees an opaque failure instead of the status. + constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n"; + + void send_error(SOCKET socket, const char *status) { + const std::string response = + std::string("HTTP/1.0 ") + status + "\r\n" + + cors_header + + "Content-Length: 0\r\n" + "Connection: close\r\n" + "\r\n"; + send_all(socket, response); + } + } + + StreamServer::StreamServer(unsigned short port) + : port(port) + { + if (!this->open_listener()) { + // the stream was asked for explicitly, so say plainly that it is not there + log_warning("api::stream", + "the video stream is not available on port {}", this->port); + return; + } + + this->running = true; + this->acceptor = std::thread([this] { + this->accept_worker(); + }); + + // deliberately not logging a full URL; local IPs would leak into shared logs + log_info("api::stream", "video stream is listening on port: {}", this->port); + log_warning("api::stream", + "the video stream is unauthenticated - anyone who can reach port {} can watch " + "the game screen", this->port); + } + + bool StreamServer::open_listener() { + WSADATA wsa_data; + const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (error != 0) { + log_warning("api::stream", "WSAStartup() returned {}", error); + return false; + } + this->wsa_started = true; + + this->listener = socket(AF_INET, SOCK_STREAM, 0); + if (this->listener == INVALID_SOCKET) { + log_warning("api::stream", "could not create listener socket: {}", + get_last_error_string()); + return false; + } + + int opt_enable = 1; + if (setsockopt(this->listener, SOL_SOCKET, SO_REUSEADDR, + reinterpret_cast(&opt_enable), sizeof(int)) == -1) { + log_warning("api::stream", "could not set socket option SO_REUSEADDR: {}", + get_last_error_string()); + } + + sockaddr_in server_address {}; + server_address.sin_family = AF_INET; + server_address.sin_port = htons(this->port); + server_address.sin_addr.s_addr = INADDR_ANY; + + if (bind(this->listener, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) { + log_warning("api::stream", "could not bind socket on port {}: {}", + this->port, get_last_error_string()); + closesocket(this->listener); + this->listener = INVALID_SOCKET; + return false; + } + + if (listen(this->listener, server_backlog) == -1) { + log_warning("api::stream", "could not listen on port {}: {}", + this->port, get_last_error_string()); + closesocket(this->listener); + this->listener = INVALID_SOCKET; + return false; + } + + return true; + } + + StreamServer::~StreamServer() { + + this->running = false; + + if (this->listener != INVALID_SOCKET) { + closesocket(this->listener); + this->listener = INVALID_SOCKET; + } + + // drops the client threads out of their blocking send/recv + { + std::lock_guard lock(this->clients_m); + for (auto &client : this->clients) { + if (client.socket != INVALID_SOCKET) { + ::shutdown(client.socket, SD_BOTH); + } + } + } + + if (this->acceptor.joinable()) { + this->acceptor.join(); + } + + // joining is what guarantees no client thread outlives this object + for (auto &client : this->clients) { + if (client.thread.joinable()) { + client.thread.join(); + } + } + + if (this->wsa_started) { + WSACleanup(); + } + } + + void StreamServer::accept_worker() { + + while (this->running) { + sockaddr_in client_address {}; + int client_address_size = sizeof(sockaddr_in); + + const SOCKET client = accept( + this->listener, (sockaddr *) &client_address, &client_address_size); + if (client == INVALID_SOCKET) { + // on shutdown the listener is closed under us; otherwise do not spin + if (this->running) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + continue; + } + + if (!this->running) { + closesocket(client); + break; + } + + char address_data[INET_ADDRSTRLEN] {}; + inet_ntop(AF_INET, &client_address.sin_addr, address_data, INET_ADDRSTRLEN); + std::string address(address_data); + + // every client costs an encode and real bandwidth, so the cap protects the game + int slot = -1; + { + std::lock_guard lock(this->clients_m); + for (size_t i = 0; i < this->clients.size(); i++) { + if (!this->clients[i].active) { + this->clients[i].active = true; + this->clients[i].socket = client; + slot = static_cast(i); + break; + } + } + } + + if (slot < 0) { + log_warning("api::stream", "client limit of {} hit", client_limit); + send_error(client, "503 Service Unavailable"); + closesocket(client); + continue; + } + + // this thread is the only one that touches the thread objects, so the slot's + // previous occupant gets reaped here rather than being detached + if (this->clients[slot].thread.joinable()) { + this->clients[slot].thread.join(); + } + + this->clients[slot].thread = std::thread([this, slot, client, address] { + this->client_worker(slot, client, address); + }); + } + } + + void StreamServer::client_worker(int slot, SOCKET socket, std::string address) { + + DWORD timeout = request_timeout_ms; + setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); + + timeout = send_timeout_ms; + setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); + + int opt_enable = 1; + setsockopt(socket, IPPROTO_TCP, TCP_NODELAY, + reinterpret_cast(&opt_enable), sizeof(int)); + + // whatever sits in the send buffer is already stale, and the default holds about a + // third of a second of H.264 because the bitrate is so low. keeping it small makes a + // slow reader block the sender, which then skips to the newest frame instead of + // handing over a backlog + int send_buffer = send_buffer_bytes; + setsockopt(socket, SOL_SOCKET, SO_SNDBUF, + reinterpret_cast(&send_buffer), sizeof(send_buffer)); + + HttpRequest request; + if (read_request(socket, request_size_limit, request)) { + if (request.method != "GET") { + send_error(socket, "405 Method Not Allowed"); + } else { + const int fps = query_int(request, "fps", 30, 1, fps_limit); + const int quality = query_int(request, "q", 70, 1, 100); + + auto writer = make_stream_writer(request.path, quality, fps); + if (!writer) { + send_error(socket, "404 Not Found"); + } else { + std::vector screens; + graphics_screens_get(screens); + + // registration takes a raw swapchain index and never bounds it, so the + // capture range has to be enforced here rather than assumed + const auto streamable = [&screens](int screen) { + return screen < static_cast(GRAPHICS_CAPTURE_SCREEN_NO) + && std::find(screens.begin(), screens.end(), screen) + != screens.end(); + }; + + // screen 1 is the subscreen in every game that has one; single-screen games + // only ever register screen 0, so resolve the default against what exists. + // left unclamped so a nonsense screen is reported as what was asked for + int screen = query_int(request, "screen", -1, 0, + std::numeric_limits::max()); + if (screen < 0) { + screen = streamable(1) ? 1 : 0; + } + + // the default always lands on a screen that exists, so this is only ever + // an explicit request for one that cannot be captured + if (!streamable(screen)) { + log_warning("api::stream", + "screen {} is not available, refusing {}", screen, address); + send_error(socket, "404 Not Found"); + } else if (!capture_pump::claim_screen(screen)) { + log_warning("api::stream", + "screen {} is already being streamed, refusing {}", + screen, address); + send_error(socket, "503 Service Unavailable"); + } else { + log_info("api::stream", + "client connected: {} ({}, screen={}, fps={}, quality={})", + address, request.path, screen, fps, quality); + + const std::string header = + "HTTP/1.0 200 OK\r\n" + "Connection: close\r\n" + + std::string(cors_header) + + "Cache-Control: no-store, no-cache, must-revalidate\r\n" + "Pragma: no-cache\r\n" + "Content-Type: " + writer->content_type() + "\r\n" + "\r\n"; + + const StreamSend stream_send = [socket](const void *data, size_t size) { + return send_all(socket, data, size); + }; + + if (send_all(socket, header) && writer->begin(stream_send)) { + const auto interval = std::chrono::microseconds(1000000 / fps); + + while (this->running) { + const auto started = std::chrono::steady_clock::now(); + + capture_pump::Frame frame; + const bool ok = capture_pump::capture_direct( + screen, frame.pixels, 1, + &frame.timestamp, &frame.width, &frame.height); + + if (ok && frame.pixels) { + if (!writer->write(stream_send, frame)) { + break; + } + } else if (client_gone(socket)) { + break; + } + + // a failed capture still paces, or a stalled game spins this + std::this_thread::sleep_until(started + interval); + } + } + + capture_pump::release_screen(screen); + log_info("api::stream", "client disconnected: {}", address); + } + } + } + } + + { + std::lock_guard lock(this->clients_m); + this->clients[slot].socket = INVALID_SOCKET; + this->clients[slot].active = false; + } + + closesocket(socket); + } +} diff --git a/src/spice2x/api/stream_server.h b/src/spice2x/api/stream_server.h new file mode 100644 index 0000000..0e2646e --- /dev/null +++ b/src/spice2x/api/stream_server.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +namespace api { + + class StreamServer { + public: + + explicit StreamServer(unsigned short port); + ~StreamServer(); + + StreamServer(const StreamServer &) = delete; + StreamServer &operator=(const StreamServer &) = delete; + + private: + + // configuration + static constexpr int server_backlog = 4; + static constexpr int client_limit = 4; + static constexpr int request_size_limit = 8 * 1024; + static constexpr int request_timeout_ms = 5000; + static constexpr int send_timeout_ms = 5000; + // small enough that a low bitrate stream cannot hide a backlog of stale frames in it + static constexpr int send_buffer_bytes = 16 * 1024; + static constexpr int fps_limit = 60; + + struct Client { + std::thread thread; + SOCKET socket = INVALID_SOCKET; + bool active = false; + }; + + void accept_worker(); + bool open_listener(); + void client_worker(int slot, SOCKET socket, std::string address); + + unsigned short port; + SOCKET listener = INVALID_SOCKET; + bool wsa_started = false; + std::atomic_bool running { false }; + std::thread acceptor; + std::mutex clients_m; + // socket and active are guarded by clients_m; only the acceptor touches thread + std::array clients; + }; +} diff --git a/src/spice2x/api/websocket.cpp b/src/spice2x/api/websocket.cpp index eaab384..18dec87 100644 --- a/src/spice2x/api/websocket.cpp +++ b/src/spice2x/api/websocket.cpp @@ -12,6 +12,20 @@ using namespace headsocket; namespace api { + namespace { + + // how long a single handshake read may stall before the connection is dropped; + // headsocket reads the request a byte at a time, so this is an idle timeout between + // bytes rather than a deadline for the whole handshake + constexpr int handshake_timeout_ms = 5000; + + void set_recv_timeout(connection &conn, int milliseconds) { + DWORD timeout = static_cast(milliseconds); + setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO, + reinterpret_cast(&timeout), sizeof(timeout)); + } + } + /* * Client class declaration */ @@ -37,6 +51,21 @@ namespace api { HEADSOCKET_SERVER(WebSocketServer, web_socket_server); public: WebSocketController *websocket; + + protected: + bool handshake(connection &conn) override { + + // headsocket runs the handshake on its single accept thread with a blocking + // recv, so a peer that connects and then says nothing would park that thread and + // leave every later connection sitting unaccepted in the backlog + set_recv_timeout(conn, handshake_timeout_ms); + const bool accepted = base_t::handshake(conn); + + // from here the client thread owns the socket and wants to block on reads + set_recv_timeout(conn, 0); + + return accepted; + } }; void api::WebSocketServer::init() {} diff --git a/src/spice2x/cmake/check_no_static_dll_imports.cmake b/src/spice2x/cmake/check_no_static_dll_imports.cmake index 130b0fc..232946b 100644 --- a/src/spice2x/cmake/check_no_static_dll_imports.cmake +++ b/src/spice2x/cmake/check_no_static_dll_imports.cmake @@ -1,72 +1,72 @@ -# fails the build if a PE binary statically imports a forbidden DLL. -# -# some DLLs must never end up in spice's static import table, for two reasons: -# -# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy -# into the modules directory to replace the system one. a static import -# forces the loader to load the SYSTEM copy at process startup - before the -# modules directory is added to the DLL search path and before the game DLL -# loads - so the user-supplied override never takes effect (see issue #779). -# -# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/ -# mfreadwrite): a static import loads them eagerly and breaks Unity games. -# -# in both cases the DLL must instead be loaded dynamically (libutils::try_library -# / GetProcAddress / delay load) so it is only pulled in when actually needed. -# -# invoked via `cmake -P` from a POST_BUILD step. required -D variables: -# OBJDUMP - path to objdump (CMAKE_OBJDUMP) -# TARGET_FILE - path to the PE binary to inspect -# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject - -if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}") - message(WARNING - "check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}") - return() -endif() - -execute_process( - COMMAND "${OBJDUMP}" -p "${TARGET_FILE}" - OUTPUT_VARIABLE dump_output - RESULT_VARIABLE dump_result - ERROR_VARIABLE dump_error) - -if(NOT dump_result EQUAL 0) - message(WARNING - "check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}") - return() -endif() - -# both GNU objdump and llvm-objdump print one "DLL Name: " line per -# statically imported DLL in their PE private-header dump. -string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}") - -set(violations "") -foreach(line IN LISTS dll_lines) - string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}") - string(STRIP "${dll_name}" dll_name) - string(TOLOWER "${dll_name}" dll_name_lower) - if(dll_name_lower IN_LIST FORBIDDEN) - list(APPEND violations "${dll_name}") - endif() -endforeach() - -if(violations) - list(REMOVE_DUPLICATES violations) - string(REPLACE ";" ", " violations_str "${violations}") - message(FATAL_ERROR - "static DLL import check FAILED for ${TARGET_FILE}\n" - " forbidden static imports found: ${violations_str}\n" - "\n" - " these DLLs must never be statically imported by spice:\n" - " * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n" - " system copy at startup and preempts the modules override (issue #779).\n" - " * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n" - " Unity games.\n" - "\n" - " fix: load the DLL dynamically instead - replace the direct API call with a\n" - " libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n" - " call through the resolved function pointer.") -endif() - -message(STATUS "static DLL import check passed for ${TARGET_FILE}") +# fails the build if a PE binary statically imports a forbidden DLL. +# +# some DLLs must never end up in spice's static import table, for two reasons: +# +# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy +# into the modules directory to replace the system one. a static import +# forces the loader to load the SYSTEM copy at process startup - before the +# modules directory is added to the DLL search path and before the game DLL +# loads - so the user-supplied override never takes effect (see issue #779). +# +# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/ +# mfreadwrite): a static import loads them eagerly and breaks Unity games. +# +# in both cases the DLL must instead be loaded dynamically (libutils::try_library +# / GetProcAddress / delay load) so it is only pulled in when actually needed. +# +# invoked via `cmake -P` from a POST_BUILD step. required -D variables: +# OBJDUMP - path to objdump (CMAKE_OBJDUMP) +# TARGET_FILE - path to the PE binary to inspect +# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject + +if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}") + message(WARNING + "check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}") + return() +endif() + +execute_process( + COMMAND "${OBJDUMP}" -p "${TARGET_FILE}" + OUTPUT_VARIABLE dump_output + RESULT_VARIABLE dump_result + ERROR_VARIABLE dump_error) + +if(NOT dump_result EQUAL 0) + message(WARNING + "check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}") + return() +endif() + +# both GNU objdump and llvm-objdump print one "DLL Name: " line per +# statically imported DLL in their PE private-header dump. +string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}") + +set(violations "") +foreach(line IN LISTS dll_lines) + string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}") + string(STRIP "${dll_name}" dll_name) + string(TOLOWER "${dll_name}" dll_name_lower) + if(dll_name_lower IN_LIST FORBIDDEN) + list(APPEND violations "${dll_name}") + endif() +endforeach() + +if(violations) + list(REMOVE_DUPLICATES violations) + string(REPLACE ";" ", " violations_str "${violations}") + message(FATAL_ERROR + "static DLL import check FAILED for ${TARGET_FILE}\n" + " forbidden static imports found: ${violations_str}\n" + "\n" + " these DLLs must never be statically imported by spice:\n" + " * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n" + " system copy at startup and preempts the modules override (issue #779).\n" + " * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n" + " Unity games.\n" + "\n" + " fix: load the DLL dynamically instead - replace the direct API call with a\n" + " libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n" + " call through the resolved function pointer.") +endif() + +message(STATUS "static DLL import check passed for ${TARGET_FILE}") diff --git a/src/spice2x/external/docker/Dockerfile b/src/spice2x/external/docker/Dockerfile index a010f38..fa01867 100644 --- a/src/spice2x/external/docker/Dockerfile +++ b/src/spice2x/external/docker/Dockerfile @@ -7,7 +7,7 @@ RUN pacman --noconfirm -Syu git \ ninja \ cmake \ unzip \ - wget \ + nasm \ mingw-w64-crt \ mingw-w64-winpthreads \ mingw-w64-gcc \ @@ -24,3 +24,12 @@ ENV PATH="$PATH:/opt/llvm-mingw-xp/bin" RUN curl -fsSL "https://github.com/mon/windows-dll-compat-checker/releases/download/v1.3/windows_dll_compat_checker-linux-x86_64.tar.xz" \ | tar -xJ -C /usr/local/bin + +# Stock makepkg.conf builds serially; this makes the AUR compiles below parallel. +RUN printf '%s\n' 'MAKEFLAGS="-j$(nproc)"' > /home/user/.makepkg.conf \ + && chown user: /home/user/.makepkg.conf + +# libjpeg-turbo for JPEG encoding, x264 for the API H.264 video stream. Only the +# mingw-w64 toolchains get these; the WinXP targets build without JPEG support +# and without the stream encoder. +RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo mingw-w64-x264" diff --git a/src/spice2x/external/dseg/DSEG-LICENSE.txt b/src/spice2x/external/dseg/DSEG-LICENSE.txt index ad4c0bf..a6c8ffd 100644 --- a/src/spice2x/external/dseg/DSEG-LICENSE.txt +++ b/src/spice2x/external/dseg/DSEG-LICENSE.txt @@ -1,95 +1,95 @@ -Copyright (c) 2017, keshikan (http://www.keshikan.net), -with Reserved Font Name "DSEG". - - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. +Copyright (c) 2017, keshikan (http://www.keshikan.net), +with Reserved Font Name "DSEG". + + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/spice2x/external/dseg/DSEG14Classic-Italic.sfd b/src/spice2x/external/dseg/DSEG14Classic-Italic.sfd index 2b814de..733018d 100644 --- a/src/spice2x/external/dseg/DSEG14Classic-Italic.sfd +++ b/src/spice2x/external/dseg/DSEG14Classic-Italic.sfd @@ -1,11023 +1,11023 @@ -SplineFontDB: 3.2 -FontName: DSEG14Classic-Italic -FullName: DSEG14 Classic-Italic -FamilyName: DSEG14 Classic -Weight: Regular -Copyright: Created by Keshikan(https://twitter.com/keshinomi_88pro)\nwith FontForge 2.0 (http://fontforge.sf.net) -UComments: "2014-8-31: Created." -Version: 0.46 -ItalicAngle: -5 -UnderlinePosition: -100 -UnderlineWidth: 50 -Ascent: 1000 -Descent: 0 -InvalidEm: 0 -LayerCount: 2 -Layer: 0 0 "+gMyXYgAA" 1 -Layer: 1 0 "+Uk2XYgAA" 0 -XUID: [1021 682 390630330 14528854] -FSType: 8 -OS2Version: 0 -OS2_WeightWidthSlopeOnly: 0 -OS2_UseTypoMetrics: 1 -CreationTime: 1409488158 -ModificationTime: 1681891359 -PfmFamily: 17 -TTFWeight: 400 -TTFWidth: 5 -LineGap: 90 -VLineGap: 0 -OS2TypoAscent: 0 -OS2TypoAOffset: 1 -OS2TypoDescent: 0 -OS2TypoDOffset: 1 -OS2TypoLinegap: 90 -OS2WinAscent: 0 -OS2WinAOffset: 1 -OS2WinDescent: 0 -OS2WinDOffset: 1 -HheadAscent: 0 -HheadAOffset: 1 -HheadDescent: 0 -HheadDOffset: 1 -OS2Vendor: 'PfEd' -MarkAttachClasses: 1 -DEI: 91125 -LangName: 1033 "Created by Keshikan+AAoA-with FontForge 2.0 (http://fontforge.sf.net)" "" "" "" "" "Version 0.46" "" "" "" "Keshikan(Twitter:@keshinomi_88pro)" "" "" "http://www.keshikan.net" "Copyright (c) 2018, keshikan (http://www.keshikan.net),+AAoA-with Reserved Font Name +ACIA-DSEG+ACIA.+AAoACgAA-This Font Software is licensed under the SIL Open Font License, Version 1.1.+AAoA-This license is copied below, and is also available with a FAQ at:+AAoA-http://scripts.sil.org/OFL+AAoACgAK------------------------------------------------------------+AAoA-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007+AAoA------------------------------------------------------------+AAoACgAA-PREAMBLE+AAoA-The goals of the Open Font License (OFL) are to stimulate worldwide+AAoA-development of collaborative font projects, to support the font creation+AAoA-efforts of academic and linguistic communities, and to provide a free and+AAoA-open framework in which fonts may be shared and improved in partnership+AAoA-with others.+AAoACgAA-The OFL allows the licensed fonts to be used, studied, modified and+AAoA-redistributed freely as long as they are not sold by themselves. The+AAoA-fonts, including any derivative works, can be bundled, embedded, +AAoA-redistributed and/or sold with any software provided that any reserved+AAoA-names are not used by derivative works. The fonts and derivatives,+AAoA-however, cannot be released under any other type of license. The+AAoA-requirement for fonts to remain under this license does not apply+AAoA-to any document created using the fonts or their derivatives.+AAoACgAA-DEFINITIONS+AAoAIgAA-Font Software+ACIA refers to the set of files released by the Copyright+AAoA-Holder(s) under this license and clearly marked as such. This may+AAoA-include source files, build scripts and documentation.+AAoACgAi-Reserved Font Name+ACIA refers to any names specified as such after the+AAoA-copyright statement(s).+AAoACgAi-Original Version+ACIA refers to the collection of Font Software components as+AAoA-distributed by the Copyright Holder(s).+AAoACgAi-Modified Version+ACIA refers to any derivative made by adding to, deleting,+AAoA-or substituting -- in part or in whole -- any of the components of the+AAoA-Original Version, by changing formats or by porting the Font Software to a+AAoA-new environment.+AAoACgAi-Author+ACIA refers to any designer, engineer, programmer, technical+AAoA-writer or other person who contributed to the Font Software.+AAoACgAA-PERMISSION & CONDITIONS+AAoA-Permission is hereby granted, free of charge, to any person obtaining+AAoA-a copy of the Font Software, to use, study, copy, merge, embed, modify,+AAoA-redistribute, and sell modified and unmodified copies of the Font+AAoA-Software, subject to the following conditions:+AAoACgAA-1) Neither the Font Software nor any of its individual components,+AAoA-in Original or Modified Versions, may be sold by itself.+AAoACgAA-2) Original or Modified Versions of the Font Software may be bundled,+AAoA-redistributed and/or sold with any software, provided that each copy+AAoA-contains the above copyright notice and this license. These can be+AAoA-included either as stand-alone text files, human-readable headers or+AAoA-in the appropriate machine-readable metadata fields within text or+AAoA-binary files as long as those fields can be easily viewed by the user.+AAoACgAA-3) No Modified Version of the Font Software may use the Reserved Font+AAoA-Name(s) unless explicit written permission is granted by the corresponding+AAoA-Copyright Holder. This restriction only applies to the primary font name as+AAoA-presented to the users.+AAoACgAA-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font+AAoA-Software shall not be used to promote, endorse or advertise any+AAoA-Modified Version, except to acknowledge the contribution(s) of the+AAoA-Copyright Holder(s) and the Author(s) or with their explicit written+AAoA-permission.+AAoACgAA-5) The Font Software, modified or unmodified, in part or in whole,+AAoA-must be distributed entirely under this license, and must not be+AAoA-distributed under any other license. The requirement for fonts to+AAoA-remain under this license does not apply to any document created+AAoA-using the Font Software.+AAoACgAA-TERMINATION+AAoA-This license becomes null and void if any of the above conditions are+AAoA-not met.+AAoACgAA-DISCLAIMER+AAoA-THE FONT SOFTWARE IS PROVIDED +ACIA-AS IS+ACIA, WITHOUT WARRANTY OF ANY KIND,+AAoA-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF+AAoA-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT+AAoA-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE+AAoA-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+AAoA-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL+AAoA-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+AAoA-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM+AAoA-OTHER DEALINGS IN THE FONT SOFTWARE." "http://scripts.sil.org/OFL" "" "" "" "" "DSEG14 12:34" -Encoding: UnicodeFull -UnicodeInterp: none -NameList: Adobe Glyph List -DisplaySize: -48 -AntiAlias: 1 -FitToEm: 1 -WinInfo: 0 32 9 -BeginPrivate: 0 -EndPrivate -TeXData: 1 0 0 209715 104857 69905 930087 1048576 69905 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 -BeginChars: 1114112 814 - -StartChar: uEE00 -Encoding: 60928 60928 0 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --631 969 m 1 - -597 1000 l 1 - -131 1000 l 1 - -103 969 l 1 - -170 907 l 1 - -190 907 l 1 - -326 907 l 1 - -418 907 l 1 - -554 907 l 1 - -574 907 l 1 - -631 969 l 1 -EndSplineSet -EndChar - -StartChar: uEE01 -Encoding: 60929 60929 1 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --90 955 m 1 - -62 924 l 1 - -98 510 l 1 - -125 510 l 1 - -130 514 l 1 - -185 575 l 1 - -183 607 l 1 - -167 782 l 1 - -158 887 l 1 - -158 893 l 1 - -90 955 l 1 -EndSplineSet -EndChar - -StartChar: uEE02 -Encoding: 60930 60930 2 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --132 485 m 1 - -127 489 l 1 - -100 489 l 1 - -136 76 l 1 - -170 45 l 1 - -226 107 l 1 - -226 113 l 1 - -217 218 l 1 - -199 424 l 1 - -132 485 l 1 -EndSplineSet -EndChar - -StartChar: uEE03 -Encoding: 60931 60931 3 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --185 31 m 1 - -219 0 l 1 - -685 0 l 1 - -713 31 l 1 - -646 93 l 1 - -626 93 l 1 - -490 93 l 1 - -398 93 l 1 - -262 93 l 1 - -242 93 l 1 - -185 31 l 1 -EndSplineSet -EndChar - -StartChar: uEE04 -Encoding: 60932 60932 4 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --726 45 m 1 - -754 76 l 1 - -718 490 l 1 - -691 490 l 1 - -687 485 l 1 - -631 424 l 1 - -649 218 l 1 - -658 113 l 1 - -658 107 l 1 - -726 45 l 1 -EndSplineSet -EndChar - -StartChar: uEE05 -Encoding: 60933 60933 5 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --685 515 m 1 - -689 510 l 1 - -716 510 l 1 - -680 924 l 1 - -646 955 l 1 - -590 893 l 1 - -590 887 l 1 - -599 782 l 1 - -617 576 l 1 - -685 515 l 1 -EndSplineSet -EndChar - -StartChar: uEE06 -Encoding: 60934 60934 6 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --672 500 m 1 - -620 547 l 1 - -620 546 l 1 - -484 546 l 1 - -443 546 l 1 - -420 500 l 1 - -451 454 l 1 - -608 454 l 1 - -611 454 l 1 - -628 454 l 1 - -628 453 l 1 - -672 500 l 1 -EndSplineSet -EndChar - -StartChar: uEE07 -Encoding: 60935 60935 7 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --188 546 m 1 - -145 499 l 1 - -196 453 l 1 - -196 454 l 1 - -214 454 l 1 - -216 454 l 1 - -345 454 l 1 - -366 454 l 1 - -373 454 l 1 - -396 500 l 1 - -365 546 l 1 - -358 546 l 1 - -337 546 l 1 - -324 546 l 1 - -188 546 l 1 -EndSplineSet -EndChar - -StartChar: uEE08 -Encoding: 60936 60936 8 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --469 567 m 1 - -494 567 l 1 - -582 746 l 1 - -570 887 l 1 - -544 887 l 1 - -457 709 l 1 - -469 567 l 1 -EndSplineSet -EndChar - -StartChar: uEE09 -Encoding: 60937 60937 9 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --439 673 m 1 - -420 887 l 1 - -328 887 l 1 - -329 876 l 1 - -347 673 l 1 - -353 601 l 1 - -406 521 l 1 - -445 601 l 1 - -439 673 l 1 -EndSplineSet -EndChar - -StartChar: uEE0A -Encoding: 60938 60938 10 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --204 887 m 1 - -178 887 l 1 - -190 746 l 1 - -310 567 l 1 - -335 567 l 1 - -323 709 l 1 - -204 887 l 1 -EndSplineSet -EndChar - -StartChar: uEE0B -Encoding: 60939 60939 11 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --493 291 m 1 - -612 113 l 1 - -638 113 l 1 - -626 254 l 1 - -506 433 l 1 - -481 433 l 1 - -493 291 l 1 -EndSplineSet -EndChar - -StartChar: uEE0C -Encoding: 60940 60940 12 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --392 157 m 1 - -396 113 l 1 - -488 113 l 1 - -469 327 l 1 - -463 399 l 1 - -410 479 l 1 - -371 399 l 1 - -377 327 l 1 - -392 157 l 1 -EndSplineSet -EndChar - -StartChar: uEE0D -Encoding: 60941 60941 13 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet --347 433 m 1 - -322 433 l 1 - -234 254 l 1 - -246 113 l 1 - -272 113 l 1 - -359 291 l 1 - -347 433 l 1 -EndSplineSet -EndChar - -StartChar: uEE0E -Encoding: 60942 60942 14 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -SplineSet -18 62 m 0 - 18 53 16 45 13 38 c 0 - 10 31 6 24 0 18 c 0 - -6 12 -13 8 -20 5 c 0 - -27 2 -35 0 -44 0 c 0 - -53 0 -61 2 -68 5 c 0 - -75 8 -82 12 -88 18 c 0 - -94 24 -98 31 -101 38 c 0 - -104 45 -106 53 -106 62 c 0 - -106 71 -104 79 -101 86 c 0 - -98 93 -94 100 -88 106 c 0 - -82 112 -75 116 -68 119 c 0 - -61 122 -53 124 -44 124 c 0 - -35 124 -27 122 -20 119 c 0 - -13 116 -6 112 0 106 c 0 - 6 100 10 93 13 86 c 0 - 16 79 18 71 18 62 c 0 -EndSplineSet -EndChar - -StartChar: uEE0F -Encoding: 60943 60943 15 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: uEE10 -Encoding: 60944 60944 16 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -SplineSet -180 693 m 0 - 180 684 178 676 175 669 c 0 - 172 662 168 655 162 649 c 0 - 156 643 149 639 142 636 c 0 - 135 633 127 631 118 631 c 0 - 109 631 101 633 94 636 c 0 - 87 639 80 643 74 649 c 0 - 68 655 64 662 61 669 c 0 - 58 676 56 684 56 693 c 0 - 56 702 58 710 61 717 c 0 - 64 724 68 730 74 736 c 0 - 80 742 87 747 94 750 c 0 - 101 753 109 754 118 754 c 0 - 127 754 135 753 142 750 c 0 - 149 747 156 742 162 736 c 0 - 168 730 172 724 175 717 c 0 - 178 710 180 702 180 693 c 0 -EndSplineSet -EndChar - -StartChar: uEE11 -Encoding: 60945 60945 17 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -SplineSet -144 281 m 0 - 144 272 142 264 139 257 c 0 - 136 250 132 243 126 237 c 0 - 120 231 113 227 106 224 c 0 - 99 221 91 219 82 219 c 0 - 73 219 65 221 58 224 c 0 - 51 227 44 231 38 237 c 0 - 32 243 28 250 25 257 c 0 - 22 264 20 272 20 281 c 0 - 20 290 22 298 25 305 c 0 - 28 312 32 318 38 324 c 0 - 44 330 51 335 58 338 c 0 - 65 341 73 342 82 342 c 0 - 91 342 99 341 106 338 c 0 - 113 335 120 330 126 324 c 0 - 132 318 136 312 139 305 c 0 - 142 298 144 290 144 281 c 0 -EndSplineSet -EndChar - -StartChar: u0020 -Encoding: 32 32 18 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u0021 -Encoding: 33 33 19 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u0022 -Encoding: 34 34 20 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0023 -Encoding: 35 35 21 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -Refer: 17 60945 N 1 0 0 1 0 0 2 -EndChar - -StartChar: u0024 -Encoding: 36 36 22 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0025 -Encoding: 37 37 23 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0026 -Encoding: 38 38 24 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0027 -Encoding: 39 39 25 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0028 -Encoding: 40 40 26 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0029 -Encoding: 41 41 27 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u002A -Encoding: 42 42 28 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u002B -Encoding: 43 43 29 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u002C -Encoding: 44 44 30 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u002D -Encoding: 45 45 31 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u002E -Encoding: 46 46 32 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 0 0 2 -EndChar - -StartChar: u002F -Encoding: 47 47 33 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0030 -Encoding: 48 48 34 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0031 -Encoding: 49 49 35 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0032 -Encoding: 50 50 36 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0033 -Encoding: 51 51 37 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0034 -Encoding: 52 52 38 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0035 -Encoding: 53 53 39 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0036 -Encoding: 54 54 40 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0037 -Encoding: 55 55 41 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0038 -Encoding: 56 56 42 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0039 -Encoding: 57 57 43 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003A -Encoding: 58 58 44 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003B -Encoding: 59 59 45 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003C -Encoding: 60 60 46 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003D -Encoding: 61 61 47 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003E -Encoding: 62 62 48 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u003F -Encoding: 63 63 49 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0040 -Encoding: 64 64 50 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0041 -Encoding: 65 65 51 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0042 -Encoding: 66 66 52 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0043 -Encoding: 67 67 53 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0044 -Encoding: 68 68 54 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0045 -Encoding: 69 69 55 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0046 -Encoding: 70 70 56 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0047 -Encoding: 71 71 57 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0048 -Encoding: 72 72 58 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0049 -Encoding: 73 73 59 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004A -Encoding: 74 74 60 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004B -Encoding: 75 75 61 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004C -Encoding: 76 76 62 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004D -Encoding: 77 77 63 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004E -Encoding: 78 78 64 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u004F -Encoding: 79 79 65 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0050 -Encoding: 80 80 66 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0051 -Encoding: 81 81 67 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0052 -Encoding: 82 82 68 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0053 -Encoding: 83 83 69 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0054 -Encoding: 84 84 70 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0055 -Encoding: 85 85 71 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0056 -Encoding: 86 86 72 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0057 -Encoding: 87 87 73 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0058 -Encoding: 88 88 74 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0059 -Encoding: 89 89 75 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005A -Encoding: 90 90 76 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005B -Encoding: 91 91 77 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005C -Encoding: 92 92 78 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005D -Encoding: 93 93 79 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005E -Encoding: 94 94 80 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u005F -Encoding: 95 95 81 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0060 -Encoding: 96 96 82 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0061 -Encoding: 97 97 83 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0062 -Encoding: 98 98 84 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0063 -Encoding: 99 99 85 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0064 -Encoding: 100 100 86 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0065 -Encoding: 101 101 87 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0066 -Encoding: 102 102 88 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0067 -Encoding: 103 103 89 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0068 -Encoding: 104 104 90 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0069 -Encoding: 105 105 91 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u006A -Encoding: 106 106 92 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u006B -Encoding: 107 107 93 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u006C -Encoding: 108 108 94 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u006D -Encoding: 109 109 95 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 0 0 2 -EndChar - -StartChar: u006E -Encoding: 110 110 96 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u006F -Encoding: 111 111 97 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0070 -Encoding: 112 112 98 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0071 -Encoding: 113 113 99 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0072 -Encoding: 114 114 100 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0073 -Encoding: 115 115 101 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0074 -Encoding: 116 116 102 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0075 -Encoding: 117 117 103 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0076 -Encoding: 118 118 104 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0077 -Encoding: 119 119 105 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0078 -Encoding: 120 120 106 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u0079 -Encoding: 121 121 107 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u007A -Encoding: 122 122 108 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u007B -Encoding: 123 123 109 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u007C -Encoding: 124 124 110 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u007D -Encoding: 125 125 111 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u007E -Encoding: 126 126 112 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 S 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00A0 -Encoding: 160 160 113 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u00A1 -Encoding: 161 161 114 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -Refer: 16 60944 N 1 0 0 1 0 0 2 -EndChar - -StartChar: u00A5 -Encoding: 165 165 115 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00A6 -Encoding: 166 166 116 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00AC -Encoding: 172 172 117 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00AD -Encoding: 173 173 118 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00AF -Encoding: 175 175 119 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00B0 -Encoding: 176 176 120 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00B1 -Encoding: 177 177 121 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00B4 -Encoding: 180 180 122 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00B5 -Encoding: 181 181 123 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00B9 -Encoding: 185 185 124 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00BA -Encoding: 186 186 125 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u00BF -Encoding: 191 191 126 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2007 -Encoding: 8199 8199 127 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u2008 -Encoding: 8200 8200 128 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u200B -Encoding: 8203 8203 129 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u200C -Encoding: 8204 8204 130 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u200D -Encoding: 8205 8205 131 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u2010 -Encoding: 8208 8208 132 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2011 -Encoding: 8209 8209 133 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2012 -Encoding: 8210 8210 134 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2015 -Encoding: 8213 8213 135 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2018 -Encoding: 8216 8216 136 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2019 -Encoding: 8217 8217 137 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201A -Encoding: 8218 8218 138 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201B -Encoding: 8219 8219 139 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201C -Encoding: 8220 8220 140 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201D -Encoding: 8221 8221 141 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201E -Encoding: 8222 8222 142 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u201F -Encoding: 8223 8223 143 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2190 -Encoding: 8592 8592 144 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2191 -Encoding: 8593 8593 145 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2192 -Encoding: 8594 8594 146 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2193 -Encoding: 8595 8595 147 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2196 -Encoding: 8598 8598 148 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2197 -Encoding: 8599 8599 149 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2198 -Encoding: 8600 8600 150 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2199 -Encoding: 8601 8601 151 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21BC -Encoding: 8636 8636 152 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21BD -Encoding: 8637 8637 153 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21BE -Encoding: 8638 8638 154 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21BF -Encoding: 8639 8639 155 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21C0 -Encoding: 8640 8640 156 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21C1 -Encoding: 8641 8641 157 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21C2 -Encoding: 8642 8642 158 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21C3 -Encoding: 8643 8643 159 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21E6 -Encoding: 8678 8678 160 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21E7 -Encoding: 8679 8679 161 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21E8 -Encoding: 8680 8680 162 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u21E9 -Encoding: 8681 8681 163 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2800 -Encoding: 10240 10240 164 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: u2801 -Encoding: 10241 10241 165 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2802 -Encoding: 10242 10242 166 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2803 -Encoding: 10243 10243 167 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2804 -Encoding: 10244 10244 168 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2805 -Encoding: 10245 10245 169 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2806 -Encoding: 10246 10246 170 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2807 -Encoding: 10247 10247 171 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2808 -Encoding: 10248 10248 172 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2809 -Encoding: 10249 10249 173 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280A -Encoding: 10250 10250 174 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280B -Encoding: 10251 10251 175 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280C -Encoding: 10252 10252 176 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280D -Encoding: 10253 10253 177 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280E -Encoding: 10254 10254 178 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u280F -Encoding: 10255 10255 179 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2810 -Encoding: 10256 10256 180 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2811 -Encoding: 10257 10257 181 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2812 -Encoding: 10258 10258 182 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2813 -Encoding: 10259 10259 183 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2814 -Encoding: 10260 10260 184 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2815 -Encoding: 10261 10261 185 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2816 -Encoding: 10262 10262 186 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2817 -Encoding: 10263 10263 187 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2818 -Encoding: 10264 10264 188 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2819 -Encoding: 10265 10265 189 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281A -Encoding: 10266 10266 190 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281B -Encoding: 10267 10267 191 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281C -Encoding: 10268 10268 192 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281D -Encoding: 10269 10269 193 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281E -Encoding: 10270 10270 194 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u281F -Encoding: 10271 10271 195 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2820 -Encoding: 10272 10272 196 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2821 -Encoding: 10273 10273 197 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2822 -Encoding: 10274 10274 198 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2823 -Encoding: 10275 10275 199 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2824 -Encoding: 10276 10276 200 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2825 -Encoding: 10277 10277 201 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2826 -Encoding: 10278 10278 202 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2827 -Encoding: 10279 10279 203 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2828 -Encoding: 10280 10280 204 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2829 -Encoding: 10281 10281 205 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282A -Encoding: 10282 10282 206 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282B -Encoding: 10283 10283 207 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282C -Encoding: 10284 10284 208 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282D -Encoding: 10285 10285 209 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282E -Encoding: 10286 10286 210 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u282F -Encoding: 10287 10287 211 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2830 -Encoding: 10288 10288 212 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2831 -Encoding: 10289 10289 213 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2832 -Encoding: 10290 10290 214 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2833 -Encoding: 10291 10291 215 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2834 -Encoding: 10292 10292 216 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2835 -Encoding: 10293 10293 217 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2836 -Encoding: 10294 10294 218 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2837 -Encoding: 10295 10295 219 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2838 -Encoding: 10296 10296 220 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2839 -Encoding: 10297 10297 221 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283A -Encoding: 10298 10298 222 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283B -Encoding: 10299 10299 223 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283C -Encoding: 10300 10300 224 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283D -Encoding: 10301 10301 225 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283E -Encoding: 10302 10302 226 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u283F -Encoding: 10303 10303 227 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2840 -Encoding: 10304 10304 228 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2841 -Encoding: 10305 10305 229 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2842 -Encoding: 10306 10306 230 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2843 -Encoding: 10307 10307 231 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2844 -Encoding: 10308 10308 232 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2845 -Encoding: 10309 10309 233 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2846 -Encoding: 10310 10310 234 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2847 -Encoding: 10311 10311 235 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2848 -Encoding: 10312 10312 236 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2849 -Encoding: 10313 10313 237 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284A -Encoding: 10314 10314 238 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284B -Encoding: 10315 10315 239 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284C -Encoding: 10316 10316 240 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284D -Encoding: 10317 10317 241 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284E -Encoding: 10318 10318 242 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u284F -Encoding: 10319 10319 243 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2850 -Encoding: 10320 10320 244 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2851 -Encoding: 10321 10321 245 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2852 -Encoding: 10322 10322 246 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2853 -Encoding: 10323 10323 247 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2854 -Encoding: 10324 10324 248 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2855 -Encoding: 10325 10325 249 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2856 -Encoding: 10326 10326 250 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2857 -Encoding: 10327 10327 251 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2858 -Encoding: 10328 10328 252 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2859 -Encoding: 10329 10329 253 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285A -Encoding: 10330 10330 254 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285B -Encoding: 10331 10331 255 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285C -Encoding: 10332 10332 256 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285D -Encoding: 10333 10333 257 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285E -Encoding: 10334 10334 258 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u285F -Encoding: 10335 10335 259 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2860 -Encoding: 10336 10336 260 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2861 -Encoding: 10337 10337 261 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2862 -Encoding: 10338 10338 262 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2863 -Encoding: 10339 10339 263 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2864 -Encoding: 10340 10340 264 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2865 -Encoding: 10341 10341 265 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2866 -Encoding: 10342 10342 266 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2867 -Encoding: 10343 10343 267 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2868 -Encoding: 10344 10344 268 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2869 -Encoding: 10345 10345 269 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286A -Encoding: 10346 10346 270 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286B -Encoding: 10347 10347 271 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286C -Encoding: 10348 10348 272 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286D -Encoding: 10349 10349 273 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286E -Encoding: 10350 10350 274 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u286F -Encoding: 10351 10351 275 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2870 -Encoding: 10352 10352 276 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2871 -Encoding: 10353 10353 277 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2872 -Encoding: 10354 10354 278 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2873 -Encoding: 10355 10355 279 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2874 -Encoding: 10356 10356 280 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2875 -Encoding: 10357 10357 281 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2876 -Encoding: 10358 10358 282 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2877 -Encoding: 10359 10359 283 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2878 -Encoding: 10360 10360 284 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2879 -Encoding: 10361 10361 285 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287A -Encoding: 10362 10362 286 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287B -Encoding: 10363 10363 287 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287C -Encoding: 10364 10364 288 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287D -Encoding: 10365 10365 289 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287E -Encoding: 10366 10366 290 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u287F -Encoding: 10367 10367 291 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2880 -Encoding: 10368 10368 292 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2881 -Encoding: 10369 10369 293 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2882 -Encoding: 10370 10370 294 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2883 -Encoding: 10371 10371 295 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2884 -Encoding: 10372 10372 296 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2885 -Encoding: 10373 10373 297 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2886 -Encoding: 10374 10374 298 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2887 -Encoding: 10375 10375 299 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2888 -Encoding: 10376 10376 300 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2889 -Encoding: 10377 10377 301 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288A -Encoding: 10378 10378 302 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288B -Encoding: 10379 10379 303 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288C -Encoding: 10380 10380 304 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288D -Encoding: 10381 10381 305 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288E -Encoding: 10382 10382 306 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u288F -Encoding: 10383 10383 307 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2890 -Encoding: 10384 10384 308 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2891 -Encoding: 10385 10385 309 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2892 -Encoding: 10386 10386 310 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2893 -Encoding: 10387 10387 311 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2894 -Encoding: 10388 10388 312 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2895 -Encoding: 10389 10389 313 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2896 -Encoding: 10390 10390 314 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2897 -Encoding: 10391 10391 315 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2898 -Encoding: 10392 10392 316 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2899 -Encoding: 10393 10393 317 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289A -Encoding: 10394 10394 318 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289B -Encoding: 10395 10395 319 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289C -Encoding: 10396 10396 320 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289D -Encoding: 10397 10397 321 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289E -Encoding: 10398 10398 322 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u289F -Encoding: 10399 10399 323 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A0 -Encoding: 10400 10400 324 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A1 -Encoding: 10401 10401 325 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A2 -Encoding: 10402 10402 326 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A3 -Encoding: 10403 10403 327 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A4 -Encoding: 10404 10404 328 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A5 -Encoding: 10405 10405 329 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A6 -Encoding: 10406 10406 330 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A7 -Encoding: 10407 10407 331 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A8 -Encoding: 10408 10408 332 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28A9 -Encoding: 10409 10409 333 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AA -Encoding: 10410 10410 334 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AB -Encoding: 10411 10411 335 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AC -Encoding: 10412 10412 336 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AD -Encoding: 10413 10413 337 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AE -Encoding: 10414 10414 338 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28AF -Encoding: 10415 10415 339 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B0 -Encoding: 10416 10416 340 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B1 -Encoding: 10417 10417 341 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B2 -Encoding: 10418 10418 342 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B3 -Encoding: 10419 10419 343 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B4 -Encoding: 10420 10420 344 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B5 -Encoding: 10421 10421 345 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B6 -Encoding: 10422 10422 346 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B7 -Encoding: 10423 10423 347 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B8 -Encoding: 10424 10424 348 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28B9 -Encoding: 10425 10425 349 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BA -Encoding: 10426 10426 350 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BB -Encoding: 10427 10427 351 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BC -Encoding: 10428 10428 352 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BD -Encoding: 10429 10429 353 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BE -Encoding: 10430 10430 354 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28BF -Encoding: 10431 10431 355 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C0 -Encoding: 10432 10432 356 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C1 -Encoding: 10433 10433 357 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C2 -Encoding: 10434 10434 358 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C3 -Encoding: 10435 10435 359 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C4 -Encoding: 10436 10436 360 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C5 -Encoding: 10437 10437 361 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C6 -Encoding: 10438 10438 362 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C7 -Encoding: 10439 10439 363 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C8 -Encoding: 10440 10440 364 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28C9 -Encoding: 10441 10441 365 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CA -Encoding: 10442 10442 366 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CB -Encoding: 10443 10443 367 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CC -Encoding: 10444 10444 368 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CD -Encoding: 10445 10445 369 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CE -Encoding: 10446 10446 370 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28CF -Encoding: 10447 10447 371 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D0 -Encoding: 10448 10448 372 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D1 -Encoding: 10449 10449 373 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D2 -Encoding: 10450 10450 374 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D3 -Encoding: 10451 10451 375 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D4 -Encoding: 10452 10452 376 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D5 -Encoding: 10453 10453 377 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D6 -Encoding: 10454 10454 378 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D7 -Encoding: 10455 10455 379 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D8 -Encoding: 10456 10456 380 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28D9 -Encoding: 10457 10457 381 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DA -Encoding: 10458 10458 382 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DB -Encoding: 10459 10459 383 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DC -Encoding: 10460 10460 384 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DD -Encoding: 10461 10461 385 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DE -Encoding: 10462 10462 386 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28DF -Encoding: 10463 10463 387 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E0 -Encoding: 10464 10464 388 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E1 -Encoding: 10465 10465 389 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E2 -Encoding: 10466 10466 390 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E3 -Encoding: 10467 10467 391 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E4 -Encoding: 10468 10468 392 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E5 -Encoding: 10469 10469 393 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E6 -Encoding: 10470 10470 394 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E7 -Encoding: 10471 10471 395 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E8 -Encoding: 10472 10472 396 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28E9 -Encoding: 10473 10473 397 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28EA -Encoding: 10474 10474 398 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28EB -Encoding: 10475 10475 399 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28EC -Encoding: 10476 10476 400 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28ED -Encoding: 10477 10477 401 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28EE -Encoding: 10478 10478 402 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28EF -Encoding: 10479 10479 403 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F0 -Encoding: 10480 10480 404 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F1 -Encoding: 10481 10481 405 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F2 -Encoding: 10482 10482 406 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F3 -Encoding: 10483 10483 407 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F4 -Encoding: 10484 10484 408 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F5 -Encoding: 10485 10485 409 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F6 -Encoding: 10486 10486 410 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F7 -Encoding: 10487 10487 411 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F8 -Encoding: 10488 10488 412 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28F9 -Encoding: 10489 10489 413 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FA -Encoding: 10490 10490 414 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FB -Encoding: 10491 10491 415 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FC -Encoding: 10492 10492 416 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FD -Encoding: 10493 10493 417 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FE -Encoding: 10494 10494 418 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u28FF -Encoding: 10495 10495 419 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 14 60942 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2B00 -Encoding: 11008 11008 420 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2B01 -Encoding: 11009 11009 421 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2B02 -Encoding: 11010 11010 422 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u2B03 -Encoding: 11011 11011 423 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3041 -Encoding: 12353 12353 424 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3042 -Encoding: 12354 12354 425 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3043 -Encoding: 12355 12355 426 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3044 -Encoding: 12356 12356 427 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3045 -Encoding: 12357 12357 428 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3046 -Encoding: 12358 12358 429 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3047 -Encoding: 12359 12359 430 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3048 -Encoding: 12360 12360 431 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3049 -Encoding: 12361 12361 432 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u304A -Encoding: 12362 12362 433 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u304B -Encoding: 12363 12363 434 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u304C -Encoding: 12364 12364 435 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u304D -Encoding: 12365 12365 436 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u304E -Encoding: 12366 12366 437 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u304F -Encoding: 12367 12367 438 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3050 -Encoding: 12368 12368 439 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3051 -Encoding: 12369 12369 440 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3052 -Encoding: 12370 12370 441 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3053 -Encoding: 12371 12371 442 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3054 -Encoding: 12372 12372 443 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3055 -Encoding: 12373 12373 444 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3056 -Encoding: 12374 12374 445 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3057 -Encoding: 12375 12375 446 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3058 -Encoding: 12376 12376 447 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3059 -Encoding: 12377 12377 448 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u305A -Encoding: 12378 12378 449 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u305B -Encoding: 12379 12379 450 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u305C -Encoding: 12380 12380 451 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u305D -Encoding: 12381 12381 452 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u305E -Encoding: 12382 12382 453 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u305F -Encoding: 12383 12383 454 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3060 -Encoding: 12384 12384 455 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3061 -Encoding: 12385 12385 456 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3062 -Encoding: 12386 12386 457 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3063 -Encoding: 12387 12387 458 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3064 -Encoding: 12388 12388 459 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3065 -Encoding: 12389 12389 460 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3066 -Encoding: 12390 12390 461 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3067 -Encoding: 12391 12391 462 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3068 -Encoding: 12392 12392 463 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3069 -Encoding: 12393 12393 464 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u306A -Encoding: 12394 12394 465 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u306B -Encoding: 12395 12395 466 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u306C -Encoding: 12396 12396 467 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u306D -Encoding: 12397 12397 468 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u306E -Encoding: 12398 12398 469 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u306F -Encoding: 12399 12399 470 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3070 -Encoding: 12400 12400 471 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3071 -Encoding: 12401 12401 472 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3072 -Encoding: 12402 12402 473 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3073 -Encoding: 12403 12403 474 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3074 -Encoding: 12404 12404 475 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3075 -Encoding: 12405 12405 476 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3076 -Encoding: 12406 12406 477 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3077 -Encoding: 12407 12407 478 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3078 -Encoding: 12408 12408 479 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3079 -Encoding: 12409 12409 480 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u307A -Encoding: 12410 12410 481 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u307B -Encoding: 12411 12411 482 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u307C -Encoding: 12412 12412 483 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u307D -Encoding: 12413 12413 484 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u307E -Encoding: 12414 12414 485 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u307F -Encoding: 12415 12415 486 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3080 -Encoding: 12416 12416 487 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3081 -Encoding: 12417 12417 488 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3082 -Encoding: 12418 12418 489 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3083 -Encoding: 12419 12419 490 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3084 -Encoding: 12420 12420 491 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3085 -Encoding: 12421 12421 492 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3086 -Encoding: 12422 12422 493 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3087 -Encoding: 12423 12423 494 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3088 -Encoding: 12424 12424 495 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3089 -Encoding: 12425 12425 496 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308A -Encoding: 12426 12426 497 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308B -Encoding: 12427 12427 498 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308C -Encoding: 12428 12428 499 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308D -Encoding: 12429 12429 500 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308E -Encoding: 12430 12430 501 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u308F -Encoding: 12431 12431 502 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3090 -Encoding: 12432 12432 503 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3091 -Encoding: 12433 12433 504 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3092 -Encoding: 12434 12434 505 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3093 -Encoding: 12435 12435 506 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3094 -Encoding: 12436 12436 507 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u3095 -Encoding: 12437 12437 508 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3096 -Encoding: 12438 12438 509 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u3099 -Encoding: 12441 12441 510 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u309A -Encoding: 12442 12442 511 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u309B -Encoding: 12443 12443 512 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u309C -Encoding: 12444 12444 513 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u309D -Encoding: 12445 12445 514 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u309E -Encoding: 12446 12446 515 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u309F -Encoding: 12447 12447 516 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A0 -Encoding: 12448 12448 517 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A1 -Encoding: 12449 12449 518 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A2 -Encoding: 12450 12450 519 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A3 -Encoding: 12451 12451 520 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A4 -Encoding: 12452 12452 521 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A5 -Encoding: 12453 12453 522 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A6 -Encoding: 12454 12454 523 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A7 -Encoding: 12455 12455 524 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A8 -Encoding: 12456 12456 525 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30A9 -Encoding: 12457 12457 526 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30AA -Encoding: 12458 12458 527 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30AB -Encoding: 12459 12459 528 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30AC -Encoding: 12460 12460 529 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30AD -Encoding: 12461 12461 530 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30AE -Encoding: 12462 12462 531 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30AF -Encoding: 12463 12463 532 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30B0 -Encoding: 12464 12464 533 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30B1 -Encoding: 12465 12465 534 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30B2 -Encoding: 12466 12466 535 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30B3 -Encoding: 12467 12467 536 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30B4 -Encoding: 12468 12468 537 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30B5 -Encoding: 12469 12469 538 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30B6 -Encoding: 12470 12470 539 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30B7 -Encoding: 12471 12471 540 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30B8 -Encoding: 12472 12472 541 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30B9 -Encoding: 12473 12473 542 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30BA -Encoding: 12474 12474 543 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30BB -Encoding: 12475 12475 544 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30BC -Encoding: 12476 12476 545 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30BD -Encoding: 12477 12477 546 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30BE -Encoding: 12478 12478 547 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30BF -Encoding: 12479 12479 548 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C0 -Encoding: 12480 12480 549 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30C1 -Encoding: 12481 12481 550 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C2 -Encoding: 12482 12482 551 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30C3 -Encoding: 12483 12483 552 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C4 -Encoding: 12484 12484 553 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C5 -Encoding: 12485 12485 554 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30C6 -Encoding: 12486 12486 555 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C7 -Encoding: 12487 12487 556 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30C8 -Encoding: 12488 12488 557 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30C9 -Encoding: 12489 12489 558 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30CA -Encoding: 12490 12490 559 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30CB -Encoding: 12491 12491 560 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30CC -Encoding: 12492 12492 561 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30CD -Encoding: 12493 12493 562 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30CE -Encoding: 12494 12494 563 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30CF -Encoding: 12495 12495 564 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30D0 -Encoding: 12496 12496 565 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D1 -Encoding: 12497 12497 566 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D2 -Encoding: 12498 12498 567 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30D3 -Encoding: 12499 12499 568 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D4 -Encoding: 12500 12500 569 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D5 -Encoding: 12501 12501 570 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30D6 -Encoding: 12502 12502 571 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D7 -Encoding: 12503 12503 572 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30D8 -Encoding: 12504 12504 573 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30D9 -Encoding: 12505 12505 574 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30DA -Encoding: 12506 12506 575 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30DB -Encoding: 12507 12507 576 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30DC -Encoding: 12508 12508 577 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30DD -Encoding: 12509 12509 578 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -Refer: 0 60928 N 1 0 0 1 1632 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 10 60938 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30DE -Encoding: 12510 12510 579 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30DF -Encoding: 12511 12511 580 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E0 -Encoding: 12512 12512 581 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E1 -Encoding: 12513 12513 582 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E2 -Encoding: 12514 12514 583 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E3 -Encoding: 12515 12515 584 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E4 -Encoding: 12516 12516 585 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E5 -Encoding: 12517 12517 586 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E6 -Encoding: 12518 12518 587 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E7 -Encoding: 12519 12519 588 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E8 -Encoding: 12520 12520 589 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30E9 -Encoding: 12521 12521 590 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30EA -Encoding: 12522 12522 591 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30EB -Encoding: 12523 12523 592 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30EC -Encoding: 12524 12524 593 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30ED -Encoding: 12525 12525 594 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30EE -Encoding: 12526 12526 595 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30EF -Encoding: 12527 12527 596 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F0 -Encoding: 12528 12528 597 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F1 -Encoding: 12529 12529 598 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F2 -Encoding: 12530 12530 599 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F3 -Encoding: 12531 12531 600 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F4 -Encoding: 12532 12532 601 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30F5 -Encoding: 12533 12533 602 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F6 -Encoding: 12534 12534 603 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30F7 -Encoding: 12535 12535 604 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30F8 -Encoding: 12536 12536 605 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30F9 -Encoding: 12537 12537 606 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30FA -Encoding: 12538 12538 607 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30FB -Encoding: 12539 12539 608 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 0 0 2 -EndChar - -StartChar: u30FC -Encoding: 12540 12540 609 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30FD -Encoding: 12541 12541 610 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u30FE -Encoding: 12542 12542 611 -Width: 1632 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 1632 0 2 -Refer: 9 60937 N 1 0 0 1 1632 0 2 -EndChar - -StartChar: u30FF -Encoding: 12543 12543 612 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F0 -Encoding: 12784 12784 613 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F1 -Encoding: 12785 12785 614 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F2 -Encoding: 12786 12786 615 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F3 -Encoding: 12787 12787 616 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F4 -Encoding: 12788 12788 617 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F5 -Encoding: 12789 12789 618 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F6 -Encoding: 12790 12790 619 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F7 -Encoding: 12791 12791 620 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F8 -Encoding: 12792 12792 621 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31F9 -Encoding: 12793 12793 622 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FA -Encoding: 12794 12794 623 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FB -Encoding: 12795 12795 624 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FC -Encoding: 12796 12796 625 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FD -Encoding: 12797 12797 626 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FE -Encoding: 12798 12798 627 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u31FF -Encoding: 12799 12799 628 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uEE12 -Encoding: 60946 60946 629 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uEE13 -Encoding: 60947 60947 630 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uEE14 -Encoding: 60948 60948 631 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF01 -Encoding: 65281 65281 632 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar - -StartChar: uFF02 -Encoding: 65282 65282 633 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF03 -Encoding: 65283 65283 634 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -Refer: 17 60945 N 1 0 0 1 0 0 2 -EndChar - -StartChar: uFF04 -Encoding: 65284 65284 635 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF05 -Encoding: 65285 65285 636 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF06 -Encoding: 65286 65286 637 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF07 -Encoding: 65287 65287 638 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF08 -Encoding: 65288 65288 639 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF09 -Encoding: 65289 65289 640 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF0A -Encoding: 65290 65290 641 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF0B -Encoding: 65291 65291 642 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF0C -Encoding: 65292 65292 643 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF0D -Encoding: 65293 65293 644 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF0E -Encoding: 65294 65294 645 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 0 0 2 -EndChar - -StartChar: uFF0F -Encoding: 65295 65295 646 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF10 -Encoding: 65296 65296 647 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF11 -Encoding: 65297 65297 648 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF12 -Encoding: 65298 65298 649 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF13 -Encoding: 65299 65299 650 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF14 -Encoding: 65300 65300 651 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF15 -Encoding: 65301 65301 652 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF16 -Encoding: 65302 65302 653 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF17 -Encoding: 65303 65303 654 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF18 -Encoding: 65304 65304 655 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF19 -Encoding: 65305 65305 656 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF1A -Encoding: 65306 65306 657 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -Refer: 16 60944 N 1 0 0 1 0 0 2 -Refer: 17 60945 N 1 0 0 1 0 0 2 -EndChar - -StartChar: uFF1B -Encoding: 65307 65307 658 -Width: 200 -VWidth: 0 -Flags: HW -LayerCount: 2 -Fore -Refer: 16 60944 N 1 0 0 1 0 0 2 -EndChar - -StartChar: uFF1C -Encoding: 65308 65308 659 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF1D -Encoding: 65309 65309 660 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF1E -Encoding: 65310 65310 661 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF1F -Encoding: 65311 65311 662 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF20 -Encoding: 65312 65312 663 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF21 -Encoding: 65313 65313 664 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF22 -Encoding: 65314 65314 665 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF23 -Encoding: 65315 65315 666 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF24 -Encoding: 65316 65316 667 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF25 -Encoding: 65317 65317 668 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF26 -Encoding: 65318 65318 669 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF27 -Encoding: 65319 65319 670 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF28 -Encoding: 65320 65320 671 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF29 -Encoding: 65321 65321 672 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2A -Encoding: 65322 65322 673 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2B -Encoding: 65323 65323 674 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2C -Encoding: 65324 65324 675 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2D -Encoding: 65325 65325 676 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2E -Encoding: 65326 65326 677 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF2F -Encoding: 65327 65327 678 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF30 -Encoding: 65328 65328 679 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF31 -Encoding: 65329 65329 680 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF32 -Encoding: 65330 65330 681 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF33 -Encoding: 65331 65331 682 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF34 -Encoding: 65332 65332 683 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF35 -Encoding: 65333 65333 684 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF36 -Encoding: 65334 65334 685 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF37 -Encoding: 65335 65335 686 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF38 -Encoding: 65336 65336 687 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF39 -Encoding: 65337 65337 688 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3A -Encoding: 65338 65338 689 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3B -Encoding: 65339 65339 690 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3C -Encoding: 65340 65340 691 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3D -Encoding: 65341 65341 692 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3E -Encoding: 65342 65342 693 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF3F -Encoding: 65343 65343 694 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF40 -Encoding: 65344 65344 695 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF41 -Encoding: 65345 65345 696 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF42 -Encoding: 65346 65346 697 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF43 -Encoding: 65347 65347 698 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF44 -Encoding: 65348 65348 699 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF45 -Encoding: 65349 65349 700 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF46 -Encoding: 65350 65350 701 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF47 -Encoding: 65351 65351 702 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF48 -Encoding: 65352 65352 703 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF49 -Encoding: 65353 65353 704 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4A -Encoding: 65354 65354 705 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4B -Encoding: 65355 65355 706 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4C -Encoding: 65356 65356 707 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4D -Encoding: 65357 65357 708 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4E -Encoding: 65358 65358 709 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF4F -Encoding: 65359 65359 710 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF50 -Encoding: 65360 65360 711 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF51 -Encoding: 65361 65361 712 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF52 -Encoding: 65362 65362 713 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF53 -Encoding: 65363 65363 714 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF54 -Encoding: 65364 65364 715 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF55 -Encoding: 65365 65365 716 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF56 -Encoding: 65366 65366 717 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF57 -Encoding: 65367 65367 718 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF58 -Encoding: 65368 65368 719 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF59 -Encoding: 65369 65369 720 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF5A -Encoding: 65370 65370 721 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF5B -Encoding: 65371 65371 722 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF5C -Encoding: 65372 65372 723 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF5D -Encoding: 65373 65373 724 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF5E -Encoding: 65374 65374 725 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF65 -Encoding: 65381 65381 726 -Width: 0 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 14 60942 N 1 0 0 1 0 0 2 -EndChar - -StartChar: uFF66 -Encoding: 65382 65382 727 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF67 -Encoding: 65383 65383 728 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF68 -Encoding: 65384 65384 729 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF69 -Encoding: 65385 65385 730 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6A -Encoding: 65386 65386 731 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6B -Encoding: 65387 65387 732 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6C -Encoding: 65388 65388 733 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6D -Encoding: 65389 65389 734 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6E -Encoding: 65390 65390 735 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF6F -Encoding: 65391 65391 736 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF70 -Encoding: 65392 65392 737 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF71 -Encoding: 65393 65393 738 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF72 -Encoding: 65394 65394 739 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF73 -Encoding: 65395 65395 740 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF74 -Encoding: 65396 65396 741 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF75 -Encoding: 65397 65397 742 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF76 -Encoding: 65398 65398 743 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF77 -Encoding: 65399 65399 744 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF78 -Encoding: 65400 65400 745 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF79 -Encoding: 65401 65401 746 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7A -Encoding: 65402 65402 747 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7B -Encoding: 65403 65403 748 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7C -Encoding: 65404 65404 749 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7D -Encoding: 65405 65405 750 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7E -Encoding: 65406 65406 751 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF7F -Encoding: 65407 65407 752 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF80 -Encoding: 65408 65408 753 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF81 -Encoding: 65409 65409 754 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF82 -Encoding: 65410 65410 755 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF83 -Encoding: 65411 65411 756 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF84 -Encoding: 65412 65412 757 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF85 -Encoding: 65413 65413 758 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF86 -Encoding: 65414 65414 759 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF87 -Encoding: 65415 65415 760 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF88 -Encoding: 65416 65416 761 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF89 -Encoding: 65417 65417 762 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8A -Encoding: 65418 65418 763 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8B -Encoding: 65419 65419 764 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8C -Encoding: 65420 65420 765 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8D -Encoding: 65421 65421 766 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8E -Encoding: 65422 65422 767 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF8F -Encoding: 65423 65423 768 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF90 -Encoding: 65424 65424 769 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF91 -Encoding: 65425 65425 770 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF92 -Encoding: 65426 65426 771 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF93 -Encoding: 65427 65427 772 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF94 -Encoding: 65428 65428 773 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF95 -Encoding: 65429 65429 774 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF96 -Encoding: 65430 65430 775 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF97 -Encoding: 65431 65431 776 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF98 -Encoding: 65432 65432 777 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF99 -Encoding: 65433 65433 778 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9A -Encoding: 65434 65434 779 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9B -Encoding: 65435 65435 780 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9C -Encoding: 65436 65436 781 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9D -Encoding: 65437 65437 782 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9E -Encoding: 65438 65438 783 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFF9F -Encoding: 65439 65439 784 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE2 -Encoding: 65506 65506 785 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE3 -Encoding: 65507 65507 786 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE4 -Encoding: 65508 65508 787 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE5 -Encoding: 65509 65509 788 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE8 -Encoding: 65512 65512 789 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFE9 -Encoding: 65513 65513 790 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFEA -Encoding: 65514 65514 791 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFEB -Encoding: 65515 65515 792 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uFFEC -Encoding: 65516 65516 793 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 8 60936 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B000 -Encoding: 110592 110592 794 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B001 -Encoding: 110593 110593 795 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -Refer: 13 60941 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B150 -Encoding: 110928 110928 796 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B151 -Encoding: 110929 110929 797 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B152 -Encoding: 110930 110930 798 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B164 -Encoding: 110948 110948 799 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B165 -Encoding: 110949 110949 800 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 9 60937 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 12 60940 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B166 -Encoding: 110950 110950 801 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 10 60938 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1B167 -Encoding: 110951 110951 802 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 11 60939 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF0 -Encoding: 130032 130032 803 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF1 -Encoding: 130033 130033 804 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF2 -Encoding: 130034 130034 805 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF3 -Encoding: 130035 130035 806 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF4 -Encoding: 130036 130036 807 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF5 -Encoding: 130037 130037 808 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF6 -Encoding: 130038 130038 809 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF7 -Encoding: 130039 130039 810 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF8 -Encoding: 130040 130040 811 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 4 60932 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: u1FBF9 -Encoding: 130041 130041 812 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -Fore -Refer: 0 60928 N 1 0 0 1 816 0 2 -Refer: 1 60929 N 1 0 0 1 816 0 2 -Refer: 2 60930 N 1 0 0 1 816 0 2 -Refer: 3 60931 N 1 0 0 1 816 0 2 -Refer: 5 60933 N 1 0 0 1 816 0 2 -Refer: 6 60934 N 1 0 0 1 816 0 2 -Refer: 7 60935 N 1 0 0 1 816 0 2 -EndChar - -StartChar: uni0009 -Encoding: 9 9 813 -Width: 816 -VWidth: 200 -Flags: HW -LayerCount: 2 -EndChar -EndChars -EndSplineFont +SplineFontDB: 3.2 +FontName: DSEG14Classic-Italic +FullName: DSEG14 Classic-Italic +FamilyName: DSEG14 Classic +Weight: Regular +Copyright: Created by Keshikan(https://twitter.com/keshinomi_88pro)\nwith FontForge 2.0 (http://fontforge.sf.net) +UComments: "2014-8-31: Created." +Version: 0.46 +ItalicAngle: -5 +UnderlinePosition: -100 +UnderlineWidth: 50 +Ascent: 1000 +Descent: 0 +InvalidEm: 0 +LayerCount: 2 +Layer: 0 0 "+gMyXYgAA" 1 +Layer: 1 0 "+Uk2XYgAA" 0 +XUID: [1021 682 390630330 14528854] +FSType: 8 +OS2Version: 0 +OS2_WeightWidthSlopeOnly: 0 +OS2_UseTypoMetrics: 1 +CreationTime: 1409488158 +ModificationTime: 1681891359 +PfmFamily: 17 +TTFWeight: 400 +TTFWidth: 5 +LineGap: 90 +VLineGap: 0 +OS2TypoAscent: 0 +OS2TypoAOffset: 1 +OS2TypoDescent: 0 +OS2TypoDOffset: 1 +OS2TypoLinegap: 90 +OS2WinAscent: 0 +OS2WinAOffset: 1 +OS2WinDescent: 0 +OS2WinDOffset: 1 +HheadAscent: 0 +HheadAOffset: 1 +HheadDescent: 0 +HheadDOffset: 1 +OS2Vendor: 'PfEd' +MarkAttachClasses: 1 +DEI: 91125 +LangName: 1033 "Created by Keshikan+AAoA-with FontForge 2.0 (http://fontforge.sf.net)" "" "" "" "" "Version 0.46" "" "" "" "Keshikan(Twitter:@keshinomi_88pro)" "" "" "http://www.keshikan.net" "Copyright (c) 2018, keshikan (http://www.keshikan.net),+AAoA-with Reserved Font Name +ACIA-DSEG+ACIA.+AAoACgAA-This Font Software is licensed under the SIL Open Font License, Version 1.1.+AAoA-This license is copied below, and is also available with a FAQ at:+AAoA-http://scripts.sil.org/OFL+AAoACgAK------------------------------------------------------------+AAoA-SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007+AAoA------------------------------------------------------------+AAoACgAA-PREAMBLE+AAoA-The goals of the Open Font License (OFL) are to stimulate worldwide+AAoA-development of collaborative font projects, to support the font creation+AAoA-efforts of academic and linguistic communities, and to provide a free and+AAoA-open framework in which fonts may be shared and improved in partnership+AAoA-with others.+AAoACgAA-The OFL allows the licensed fonts to be used, studied, modified and+AAoA-redistributed freely as long as they are not sold by themselves. The+AAoA-fonts, including any derivative works, can be bundled, embedded, +AAoA-redistributed and/or sold with any software provided that any reserved+AAoA-names are not used by derivative works. The fonts and derivatives,+AAoA-however, cannot be released under any other type of license. The+AAoA-requirement for fonts to remain under this license does not apply+AAoA-to any document created using the fonts or their derivatives.+AAoACgAA-DEFINITIONS+AAoAIgAA-Font Software+ACIA refers to the set of files released by the Copyright+AAoA-Holder(s) under this license and clearly marked as such. This may+AAoA-include source files, build scripts and documentation.+AAoACgAi-Reserved Font Name+ACIA refers to any names specified as such after the+AAoA-copyright statement(s).+AAoACgAi-Original Version+ACIA refers to the collection of Font Software components as+AAoA-distributed by the Copyright Holder(s).+AAoACgAi-Modified Version+ACIA refers to any derivative made by adding to, deleting,+AAoA-or substituting -- in part or in whole -- any of the components of the+AAoA-Original Version, by changing formats or by porting the Font Software to a+AAoA-new environment.+AAoACgAi-Author+ACIA refers to any designer, engineer, programmer, technical+AAoA-writer or other person who contributed to the Font Software.+AAoACgAA-PERMISSION & CONDITIONS+AAoA-Permission is hereby granted, free of charge, to any person obtaining+AAoA-a copy of the Font Software, to use, study, copy, merge, embed, modify,+AAoA-redistribute, and sell modified and unmodified copies of the Font+AAoA-Software, subject to the following conditions:+AAoACgAA-1) Neither the Font Software nor any of its individual components,+AAoA-in Original or Modified Versions, may be sold by itself.+AAoACgAA-2) Original or Modified Versions of the Font Software may be bundled,+AAoA-redistributed and/or sold with any software, provided that each copy+AAoA-contains the above copyright notice and this license. These can be+AAoA-included either as stand-alone text files, human-readable headers or+AAoA-in the appropriate machine-readable metadata fields within text or+AAoA-binary files as long as those fields can be easily viewed by the user.+AAoACgAA-3) No Modified Version of the Font Software may use the Reserved Font+AAoA-Name(s) unless explicit written permission is granted by the corresponding+AAoA-Copyright Holder. This restriction only applies to the primary font name as+AAoA-presented to the users.+AAoACgAA-4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font+AAoA-Software shall not be used to promote, endorse or advertise any+AAoA-Modified Version, except to acknowledge the contribution(s) of the+AAoA-Copyright Holder(s) and the Author(s) or with their explicit written+AAoA-permission.+AAoACgAA-5) The Font Software, modified or unmodified, in part or in whole,+AAoA-must be distributed entirely under this license, and must not be+AAoA-distributed under any other license. The requirement for fonts to+AAoA-remain under this license does not apply to any document created+AAoA-using the Font Software.+AAoACgAA-TERMINATION+AAoA-This license becomes null and void if any of the above conditions are+AAoA-not met.+AAoACgAA-DISCLAIMER+AAoA-THE FONT SOFTWARE IS PROVIDED +ACIA-AS IS+ACIA, WITHOUT WARRANTY OF ANY KIND,+AAoA-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF+AAoA-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT+AAoA-OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE+AAoA-COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+AAoA-INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL+AAoA-DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+AAoA-FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM+AAoA-OTHER DEALINGS IN THE FONT SOFTWARE." "http://scripts.sil.org/OFL" "" "" "" "" "DSEG14 12:34" +Encoding: UnicodeFull +UnicodeInterp: none +NameList: Adobe Glyph List +DisplaySize: -48 +AntiAlias: 1 +FitToEm: 1 +WinInfo: 0 32 9 +BeginPrivate: 0 +EndPrivate +TeXData: 1 0 0 209715 104857 69905 930087 1048576 69905 783286 444596 497025 792723 393216 433062 380633 303038 157286 324010 404750 52429 2506097 1059062 262144 +BeginChars: 1114112 814 + +StartChar: uEE00 +Encoding: 60928 60928 0 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-631 969 m 1 + -597 1000 l 1 + -131 1000 l 1 + -103 969 l 1 + -170 907 l 1 + -190 907 l 1 + -326 907 l 1 + -418 907 l 1 + -554 907 l 1 + -574 907 l 1 + -631 969 l 1 +EndSplineSet +EndChar + +StartChar: uEE01 +Encoding: 60929 60929 1 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-90 955 m 1 + -62 924 l 1 + -98 510 l 1 + -125 510 l 1 + -130 514 l 1 + -185 575 l 1 + -183 607 l 1 + -167 782 l 1 + -158 887 l 1 + -158 893 l 1 + -90 955 l 1 +EndSplineSet +EndChar + +StartChar: uEE02 +Encoding: 60930 60930 2 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-132 485 m 1 + -127 489 l 1 + -100 489 l 1 + -136 76 l 1 + -170 45 l 1 + -226 107 l 1 + -226 113 l 1 + -217 218 l 1 + -199 424 l 1 + -132 485 l 1 +EndSplineSet +EndChar + +StartChar: uEE03 +Encoding: 60931 60931 3 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-185 31 m 1 + -219 0 l 1 + -685 0 l 1 + -713 31 l 1 + -646 93 l 1 + -626 93 l 1 + -490 93 l 1 + -398 93 l 1 + -262 93 l 1 + -242 93 l 1 + -185 31 l 1 +EndSplineSet +EndChar + +StartChar: uEE04 +Encoding: 60932 60932 4 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-726 45 m 1 + -754 76 l 1 + -718 490 l 1 + -691 490 l 1 + -687 485 l 1 + -631 424 l 1 + -649 218 l 1 + -658 113 l 1 + -658 107 l 1 + -726 45 l 1 +EndSplineSet +EndChar + +StartChar: uEE05 +Encoding: 60933 60933 5 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-685 515 m 1 + -689 510 l 1 + -716 510 l 1 + -680 924 l 1 + -646 955 l 1 + -590 893 l 1 + -590 887 l 1 + -599 782 l 1 + -617 576 l 1 + -685 515 l 1 +EndSplineSet +EndChar + +StartChar: uEE06 +Encoding: 60934 60934 6 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-672 500 m 1 + -620 547 l 1 + -620 546 l 1 + -484 546 l 1 + -443 546 l 1 + -420 500 l 1 + -451 454 l 1 + -608 454 l 1 + -611 454 l 1 + -628 454 l 1 + -628 453 l 1 + -672 500 l 1 +EndSplineSet +EndChar + +StartChar: uEE07 +Encoding: 60935 60935 7 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-188 546 m 1 + -145 499 l 1 + -196 453 l 1 + -196 454 l 1 + -214 454 l 1 + -216 454 l 1 + -345 454 l 1 + -366 454 l 1 + -373 454 l 1 + -396 500 l 1 + -365 546 l 1 + -358 546 l 1 + -337 546 l 1 + -324 546 l 1 + -188 546 l 1 +EndSplineSet +EndChar + +StartChar: uEE08 +Encoding: 60936 60936 8 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-469 567 m 1 + -494 567 l 1 + -582 746 l 1 + -570 887 l 1 + -544 887 l 1 + -457 709 l 1 + -469 567 l 1 +EndSplineSet +EndChar + +StartChar: uEE09 +Encoding: 60937 60937 9 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-439 673 m 1 + -420 887 l 1 + -328 887 l 1 + -329 876 l 1 + -347 673 l 1 + -353 601 l 1 + -406 521 l 1 + -445 601 l 1 + -439 673 l 1 +EndSplineSet +EndChar + +StartChar: uEE0A +Encoding: 60938 60938 10 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-204 887 m 1 + -178 887 l 1 + -190 746 l 1 + -310 567 l 1 + -335 567 l 1 + -323 709 l 1 + -204 887 l 1 +EndSplineSet +EndChar + +StartChar: uEE0B +Encoding: 60939 60939 11 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-493 291 m 1 + -612 113 l 1 + -638 113 l 1 + -626 254 l 1 + -506 433 l 1 + -481 433 l 1 + -493 291 l 1 +EndSplineSet +EndChar + +StartChar: uEE0C +Encoding: 60940 60940 12 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-392 157 m 1 + -396 113 l 1 + -488 113 l 1 + -469 327 l 1 + -463 399 l 1 + -410 479 l 1 + -371 399 l 1 + -377 327 l 1 + -392 157 l 1 +EndSplineSet +EndChar + +StartChar: uEE0D +Encoding: 60941 60941 13 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +-347 433 m 1 + -322 433 l 1 + -234 254 l 1 + -246 113 l 1 + -272 113 l 1 + -359 291 l 1 + -347 433 l 1 +EndSplineSet +EndChar + +StartChar: uEE0E +Encoding: 60942 60942 14 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +SplineSet +18 62 m 0 + 18 53 16 45 13 38 c 0 + 10 31 6 24 0 18 c 0 + -6 12 -13 8 -20 5 c 0 + -27 2 -35 0 -44 0 c 0 + -53 0 -61 2 -68 5 c 0 + -75 8 -82 12 -88 18 c 0 + -94 24 -98 31 -101 38 c 0 + -104 45 -106 53 -106 62 c 0 + -106 71 -104 79 -101 86 c 0 + -98 93 -94 100 -88 106 c 0 + -82 112 -75 116 -68 119 c 0 + -61 122 -53 124 -44 124 c 0 + -35 124 -27 122 -20 119 c 0 + -13 116 -6 112 0 106 c 0 + 6 100 10 93 13 86 c 0 + 16 79 18 71 18 62 c 0 +EndSplineSet +EndChar + +StartChar: uEE0F +Encoding: 60943 60943 15 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: uEE10 +Encoding: 60944 60944 16 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +SplineSet +180 693 m 0 + 180 684 178 676 175 669 c 0 + 172 662 168 655 162 649 c 0 + 156 643 149 639 142 636 c 0 + 135 633 127 631 118 631 c 0 + 109 631 101 633 94 636 c 0 + 87 639 80 643 74 649 c 0 + 68 655 64 662 61 669 c 0 + 58 676 56 684 56 693 c 0 + 56 702 58 710 61 717 c 0 + 64 724 68 730 74 736 c 0 + 80 742 87 747 94 750 c 0 + 101 753 109 754 118 754 c 0 + 127 754 135 753 142 750 c 0 + 149 747 156 742 162 736 c 0 + 168 730 172 724 175 717 c 0 + 178 710 180 702 180 693 c 0 +EndSplineSet +EndChar + +StartChar: uEE11 +Encoding: 60945 60945 17 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +SplineSet +144 281 m 0 + 144 272 142 264 139 257 c 0 + 136 250 132 243 126 237 c 0 + 120 231 113 227 106 224 c 0 + 99 221 91 219 82 219 c 0 + 73 219 65 221 58 224 c 0 + 51 227 44 231 38 237 c 0 + 32 243 28 250 25 257 c 0 + 22 264 20 272 20 281 c 0 + 20 290 22 298 25 305 c 0 + 28 312 32 318 38 324 c 0 + 44 330 51 335 58 338 c 0 + 65 341 73 342 82 342 c 0 + 91 342 99 341 106 338 c 0 + 113 335 120 330 126 324 c 0 + 132 318 136 312 139 305 c 0 + 142 298 144 290 144 281 c 0 +EndSplineSet +EndChar + +StartChar: u0020 +Encoding: 32 32 18 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u0021 +Encoding: 33 33 19 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u0022 +Encoding: 34 34 20 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0023 +Encoding: 35 35 21 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +Refer: 17 60945 N 1 0 0 1 0 0 2 +EndChar + +StartChar: u0024 +Encoding: 36 36 22 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0025 +Encoding: 37 37 23 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0026 +Encoding: 38 38 24 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0027 +Encoding: 39 39 25 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0028 +Encoding: 40 40 26 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0029 +Encoding: 41 41 27 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u002A +Encoding: 42 42 28 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u002B +Encoding: 43 43 29 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u002C +Encoding: 44 44 30 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u002D +Encoding: 45 45 31 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u002E +Encoding: 46 46 32 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 0 0 2 +EndChar + +StartChar: u002F +Encoding: 47 47 33 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0030 +Encoding: 48 48 34 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0031 +Encoding: 49 49 35 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0032 +Encoding: 50 50 36 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0033 +Encoding: 51 51 37 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0034 +Encoding: 52 52 38 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0035 +Encoding: 53 53 39 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0036 +Encoding: 54 54 40 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0037 +Encoding: 55 55 41 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0038 +Encoding: 56 56 42 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0039 +Encoding: 57 57 43 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003A +Encoding: 58 58 44 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003B +Encoding: 59 59 45 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003C +Encoding: 60 60 46 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003D +Encoding: 61 61 47 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003E +Encoding: 62 62 48 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u003F +Encoding: 63 63 49 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0040 +Encoding: 64 64 50 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0041 +Encoding: 65 65 51 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0042 +Encoding: 66 66 52 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0043 +Encoding: 67 67 53 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0044 +Encoding: 68 68 54 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0045 +Encoding: 69 69 55 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0046 +Encoding: 70 70 56 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0047 +Encoding: 71 71 57 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0048 +Encoding: 72 72 58 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0049 +Encoding: 73 73 59 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004A +Encoding: 74 74 60 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004B +Encoding: 75 75 61 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004C +Encoding: 76 76 62 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004D +Encoding: 77 77 63 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004E +Encoding: 78 78 64 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u004F +Encoding: 79 79 65 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0050 +Encoding: 80 80 66 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0051 +Encoding: 81 81 67 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0052 +Encoding: 82 82 68 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0053 +Encoding: 83 83 69 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0054 +Encoding: 84 84 70 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0055 +Encoding: 85 85 71 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0056 +Encoding: 86 86 72 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0057 +Encoding: 87 87 73 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0058 +Encoding: 88 88 74 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0059 +Encoding: 89 89 75 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005A +Encoding: 90 90 76 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005B +Encoding: 91 91 77 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005C +Encoding: 92 92 78 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005D +Encoding: 93 93 79 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005E +Encoding: 94 94 80 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u005F +Encoding: 95 95 81 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0060 +Encoding: 96 96 82 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0061 +Encoding: 97 97 83 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0062 +Encoding: 98 98 84 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0063 +Encoding: 99 99 85 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0064 +Encoding: 100 100 86 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0065 +Encoding: 101 101 87 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0066 +Encoding: 102 102 88 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0067 +Encoding: 103 103 89 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0068 +Encoding: 104 104 90 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0069 +Encoding: 105 105 91 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u006A +Encoding: 106 106 92 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u006B +Encoding: 107 107 93 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u006C +Encoding: 108 108 94 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u006D +Encoding: 109 109 95 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 0 0 2 +EndChar + +StartChar: u006E +Encoding: 110 110 96 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u006F +Encoding: 111 111 97 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0070 +Encoding: 112 112 98 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0071 +Encoding: 113 113 99 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0072 +Encoding: 114 114 100 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0073 +Encoding: 115 115 101 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0074 +Encoding: 116 116 102 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0075 +Encoding: 117 117 103 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0076 +Encoding: 118 118 104 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0077 +Encoding: 119 119 105 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0078 +Encoding: 120 120 106 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u0079 +Encoding: 121 121 107 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u007A +Encoding: 122 122 108 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u007B +Encoding: 123 123 109 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u007C +Encoding: 124 124 110 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u007D +Encoding: 125 125 111 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u007E +Encoding: 126 126 112 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 S 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00A0 +Encoding: 160 160 113 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u00A1 +Encoding: 161 161 114 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +Refer: 16 60944 N 1 0 0 1 0 0 2 +EndChar + +StartChar: u00A5 +Encoding: 165 165 115 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00A6 +Encoding: 166 166 116 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00AC +Encoding: 172 172 117 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00AD +Encoding: 173 173 118 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00AF +Encoding: 175 175 119 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00B0 +Encoding: 176 176 120 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00B1 +Encoding: 177 177 121 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00B4 +Encoding: 180 180 122 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00B5 +Encoding: 181 181 123 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00B9 +Encoding: 185 185 124 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00BA +Encoding: 186 186 125 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u00BF +Encoding: 191 191 126 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2007 +Encoding: 8199 8199 127 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u2008 +Encoding: 8200 8200 128 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u200B +Encoding: 8203 8203 129 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u200C +Encoding: 8204 8204 130 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u200D +Encoding: 8205 8205 131 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u2010 +Encoding: 8208 8208 132 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2011 +Encoding: 8209 8209 133 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2012 +Encoding: 8210 8210 134 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2015 +Encoding: 8213 8213 135 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2018 +Encoding: 8216 8216 136 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2019 +Encoding: 8217 8217 137 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201A +Encoding: 8218 8218 138 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201B +Encoding: 8219 8219 139 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201C +Encoding: 8220 8220 140 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201D +Encoding: 8221 8221 141 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201E +Encoding: 8222 8222 142 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u201F +Encoding: 8223 8223 143 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2190 +Encoding: 8592 8592 144 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2191 +Encoding: 8593 8593 145 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2192 +Encoding: 8594 8594 146 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2193 +Encoding: 8595 8595 147 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2196 +Encoding: 8598 8598 148 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2197 +Encoding: 8599 8599 149 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2198 +Encoding: 8600 8600 150 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2199 +Encoding: 8601 8601 151 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21BC +Encoding: 8636 8636 152 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21BD +Encoding: 8637 8637 153 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21BE +Encoding: 8638 8638 154 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21BF +Encoding: 8639 8639 155 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21C0 +Encoding: 8640 8640 156 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21C1 +Encoding: 8641 8641 157 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21C2 +Encoding: 8642 8642 158 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21C3 +Encoding: 8643 8643 159 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21E6 +Encoding: 8678 8678 160 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21E7 +Encoding: 8679 8679 161 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21E8 +Encoding: 8680 8680 162 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u21E9 +Encoding: 8681 8681 163 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2800 +Encoding: 10240 10240 164 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: u2801 +Encoding: 10241 10241 165 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2802 +Encoding: 10242 10242 166 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2803 +Encoding: 10243 10243 167 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2804 +Encoding: 10244 10244 168 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2805 +Encoding: 10245 10245 169 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2806 +Encoding: 10246 10246 170 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2807 +Encoding: 10247 10247 171 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2808 +Encoding: 10248 10248 172 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2809 +Encoding: 10249 10249 173 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280A +Encoding: 10250 10250 174 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280B +Encoding: 10251 10251 175 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280C +Encoding: 10252 10252 176 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280D +Encoding: 10253 10253 177 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280E +Encoding: 10254 10254 178 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u280F +Encoding: 10255 10255 179 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2810 +Encoding: 10256 10256 180 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2811 +Encoding: 10257 10257 181 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2812 +Encoding: 10258 10258 182 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2813 +Encoding: 10259 10259 183 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2814 +Encoding: 10260 10260 184 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2815 +Encoding: 10261 10261 185 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2816 +Encoding: 10262 10262 186 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2817 +Encoding: 10263 10263 187 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2818 +Encoding: 10264 10264 188 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2819 +Encoding: 10265 10265 189 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281A +Encoding: 10266 10266 190 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281B +Encoding: 10267 10267 191 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281C +Encoding: 10268 10268 192 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281D +Encoding: 10269 10269 193 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281E +Encoding: 10270 10270 194 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u281F +Encoding: 10271 10271 195 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2820 +Encoding: 10272 10272 196 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2821 +Encoding: 10273 10273 197 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2822 +Encoding: 10274 10274 198 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2823 +Encoding: 10275 10275 199 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2824 +Encoding: 10276 10276 200 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2825 +Encoding: 10277 10277 201 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2826 +Encoding: 10278 10278 202 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2827 +Encoding: 10279 10279 203 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2828 +Encoding: 10280 10280 204 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2829 +Encoding: 10281 10281 205 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282A +Encoding: 10282 10282 206 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282B +Encoding: 10283 10283 207 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282C +Encoding: 10284 10284 208 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282D +Encoding: 10285 10285 209 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282E +Encoding: 10286 10286 210 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u282F +Encoding: 10287 10287 211 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2830 +Encoding: 10288 10288 212 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2831 +Encoding: 10289 10289 213 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2832 +Encoding: 10290 10290 214 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2833 +Encoding: 10291 10291 215 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2834 +Encoding: 10292 10292 216 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2835 +Encoding: 10293 10293 217 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2836 +Encoding: 10294 10294 218 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2837 +Encoding: 10295 10295 219 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2838 +Encoding: 10296 10296 220 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2839 +Encoding: 10297 10297 221 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283A +Encoding: 10298 10298 222 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283B +Encoding: 10299 10299 223 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283C +Encoding: 10300 10300 224 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283D +Encoding: 10301 10301 225 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283E +Encoding: 10302 10302 226 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u283F +Encoding: 10303 10303 227 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2840 +Encoding: 10304 10304 228 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2841 +Encoding: 10305 10305 229 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2842 +Encoding: 10306 10306 230 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2843 +Encoding: 10307 10307 231 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2844 +Encoding: 10308 10308 232 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2845 +Encoding: 10309 10309 233 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2846 +Encoding: 10310 10310 234 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2847 +Encoding: 10311 10311 235 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2848 +Encoding: 10312 10312 236 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2849 +Encoding: 10313 10313 237 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284A +Encoding: 10314 10314 238 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284B +Encoding: 10315 10315 239 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284C +Encoding: 10316 10316 240 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284D +Encoding: 10317 10317 241 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284E +Encoding: 10318 10318 242 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u284F +Encoding: 10319 10319 243 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2850 +Encoding: 10320 10320 244 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2851 +Encoding: 10321 10321 245 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2852 +Encoding: 10322 10322 246 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2853 +Encoding: 10323 10323 247 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2854 +Encoding: 10324 10324 248 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2855 +Encoding: 10325 10325 249 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2856 +Encoding: 10326 10326 250 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2857 +Encoding: 10327 10327 251 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2858 +Encoding: 10328 10328 252 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2859 +Encoding: 10329 10329 253 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285A +Encoding: 10330 10330 254 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285B +Encoding: 10331 10331 255 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285C +Encoding: 10332 10332 256 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285D +Encoding: 10333 10333 257 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285E +Encoding: 10334 10334 258 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u285F +Encoding: 10335 10335 259 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2860 +Encoding: 10336 10336 260 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2861 +Encoding: 10337 10337 261 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2862 +Encoding: 10338 10338 262 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2863 +Encoding: 10339 10339 263 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2864 +Encoding: 10340 10340 264 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2865 +Encoding: 10341 10341 265 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2866 +Encoding: 10342 10342 266 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2867 +Encoding: 10343 10343 267 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2868 +Encoding: 10344 10344 268 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2869 +Encoding: 10345 10345 269 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286A +Encoding: 10346 10346 270 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286B +Encoding: 10347 10347 271 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286C +Encoding: 10348 10348 272 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286D +Encoding: 10349 10349 273 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286E +Encoding: 10350 10350 274 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u286F +Encoding: 10351 10351 275 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2870 +Encoding: 10352 10352 276 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2871 +Encoding: 10353 10353 277 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2872 +Encoding: 10354 10354 278 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2873 +Encoding: 10355 10355 279 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2874 +Encoding: 10356 10356 280 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2875 +Encoding: 10357 10357 281 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2876 +Encoding: 10358 10358 282 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2877 +Encoding: 10359 10359 283 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2878 +Encoding: 10360 10360 284 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2879 +Encoding: 10361 10361 285 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287A +Encoding: 10362 10362 286 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287B +Encoding: 10363 10363 287 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287C +Encoding: 10364 10364 288 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287D +Encoding: 10365 10365 289 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287E +Encoding: 10366 10366 290 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u287F +Encoding: 10367 10367 291 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2880 +Encoding: 10368 10368 292 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2881 +Encoding: 10369 10369 293 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2882 +Encoding: 10370 10370 294 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2883 +Encoding: 10371 10371 295 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2884 +Encoding: 10372 10372 296 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2885 +Encoding: 10373 10373 297 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2886 +Encoding: 10374 10374 298 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2887 +Encoding: 10375 10375 299 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2888 +Encoding: 10376 10376 300 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2889 +Encoding: 10377 10377 301 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288A +Encoding: 10378 10378 302 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288B +Encoding: 10379 10379 303 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288C +Encoding: 10380 10380 304 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288D +Encoding: 10381 10381 305 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288E +Encoding: 10382 10382 306 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u288F +Encoding: 10383 10383 307 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2890 +Encoding: 10384 10384 308 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2891 +Encoding: 10385 10385 309 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2892 +Encoding: 10386 10386 310 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2893 +Encoding: 10387 10387 311 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2894 +Encoding: 10388 10388 312 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2895 +Encoding: 10389 10389 313 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2896 +Encoding: 10390 10390 314 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2897 +Encoding: 10391 10391 315 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2898 +Encoding: 10392 10392 316 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2899 +Encoding: 10393 10393 317 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289A +Encoding: 10394 10394 318 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289B +Encoding: 10395 10395 319 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289C +Encoding: 10396 10396 320 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289D +Encoding: 10397 10397 321 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289E +Encoding: 10398 10398 322 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u289F +Encoding: 10399 10399 323 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A0 +Encoding: 10400 10400 324 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A1 +Encoding: 10401 10401 325 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A2 +Encoding: 10402 10402 326 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A3 +Encoding: 10403 10403 327 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A4 +Encoding: 10404 10404 328 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A5 +Encoding: 10405 10405 329 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A6 +Encoding: 10406 10406 330 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A7 +Encoding: 10407 10407 331 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A8 +Encoding: 10408 10408 332 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28A9 +Encoding: 10409 10409 333 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AA +Encoding: 10410 10410 334 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AB +Encoding: 10411 10411 335 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AC +Encoding: 10412 10412 336 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AD +Encoding: 10413 10413 337 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AE +Encoding: 10414 10414 338 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28AF +Encoding: 10415 10415 339 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B0 +Encoding: 10416 10416 340 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B1 +Encoding: 10417 10417 341 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B2 +Encoding: 10418 10418 342 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B3 +Encoding: 10419 10419 343 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B4 +Encoding: 10420 10420 344 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B5 +Encoding: 10421 10421 345 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B6 +Encoding: 10422 10422 346 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B7 +Encoding: 10423 10423 347 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B8 +Encoding: 10424 10424 348 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28B9 +Encoding: 10425 10425 349 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BA +Encoding: 10426 10426 350 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BB +Encoding: 10427 10427 351 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BC +Encoding: 10428 10428 352 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BD +Encoding: 10429 10429 353 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BE +Encoding: 10430 10430 354 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28BF +Encoding: 10431 10431 355 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C0 +Encoding: 10432 10432 356 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C1 +Encoding: 10433 10433 357 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C2 +Encoding: 10434 10434 358 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C3 +Encoding: 10435 10435 359 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C4 +Encoding: 10436 10436 360 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C5 +Encoding: 10437 10437 361 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C6 +Encoding: 10438 10438 362 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C7 +Encoding: 10439 10439 363 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C8 +Encoding: 10440 10440 364 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28C9 +Encoding: 10441 10441 365 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CA +Encoding: 10442 10442 366 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CB +Encoding: 10443 10443 367 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CC +Encoding: 10444 10444 368 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CD +Encoding: 10445 10445 369 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CE +Encoding: 10446 10446 370 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28CF +Encoding: 10447 10447 371 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D0 +Encoding: 10448 10448 372 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D1 +Encoding: 10449 10449 373 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D2 +Encoding: 10450 10450 374 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D3 +Encoding: 10451 10451 375 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D4 +Encoding: 10452 10452 376 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D5 +Encoding: 10453 10453 377 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D6 +Encoding: 10454 10454 378 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D7 +Encoding: 10455 10455 379 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D8 +Encoding: 10456 10456 380 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28D9 +Encoding: 10457 10457 381 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DA +Encoding: 10458 10458 382 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DB +Encoding: 10459 10459 383 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DC +Encoding: 10460 10460 384 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DD +Encoding: 10461 10461 385 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DE +Encoding: 10462 10462 386 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28DF +Encoding: 10463 10463 387 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E0 +Encoding: 10464 10464 388 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E1 +Encoding: 10465 10465 389 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E2 +Encoding: 10466 10466 390 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E3 +Encoding: 10467 10467 391 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E4 +Encoding: 10468 10468 392 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E5 +Encoding: 10469 10469 393 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E6 +Encoding: 10470 10470 394 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E7 +Encoding: 10471 10471 395 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E8 +Encoding: 10472 10472 396 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28E9 +Encoding: 10473 10473 397 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28EA +Encoding: 10474 10474 398 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28EB +Encoding: 10475 10475 399 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28EC +Encoding: 10476 10476 400 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28ED +Encoding: 10477 10477 401 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28EE +Encoding: 10478 10478 402 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28EF +Encoding: 10479 10479 403 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F0 +Encoding: 10480 10480 404 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F1 +Encoding: 10481 10481 405 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F2 +Encoding: 10482 10482 406 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F3 +Encoding: 10483 10483 407 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F4 +Encoding: 10484 10484 408 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F5 +Encoding: 10485 10485 409 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F6 +Encoding: 10486 10486 410 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F7 +Encoding: 10487 10487 411 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F8 +Encoding: 10488 10488 412 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28F9 +Encoding: 10489 10489 413 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FA +Encoding: 10490 10490 414 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FB +Encoding: 10491 10491 415 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FC +Encoding: 10492 10492 416 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FD +Encoding: 10493 10493 417 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FE +Encoding: 10494 10494 418 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u28FF +Encoding: 10495 10495 419 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 14 60942 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2B00 +Encoding: 11008 11008 420 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2B01 +Encoding: 11009 11009 421 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2B02 +Encoding: 11010 11010 422 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u2B03 +Encoding: 11011 11011 423 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3041 +Encoding: 12353 12353 424 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3042 +Encoding: 12354 12354 425 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3043 +Encoding: 12355 12355 426 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3044 +Encoding: 12356 12356 427 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3045 +Encoding: 12357 12357 428 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3046 +Encoding: 12358 12358 429 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3047 +Encoding: 12359 12359 430 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3048 +Encoding: 12360 12360 431 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3049 +Encoding: 12361 12361 432 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u304A +Encoding: 12362 12362 433 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u304B +Encoding: 12363 12363 434 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u304C +Encoding: 12364 12364 435 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u304D +Encoding: 12365 12365 436 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u304E +Encoding: 12366 12366 437 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u304F +Encoding: 12367 12367 438 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3050 +Encoding: 12368 12368 439 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3051 +Encoding: 12369 12369 440 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3052 +Encoding: 12370 12370 441 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3053 +Encoding: 12371 12371 442 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3054 +Encoding: 12372 12372 443 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3055 +Encoding: 12373 12373 444 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3056 +Encoding: 12374 12374 445 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3057 +Encoding: 12375 12375 446 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3058 +Encoding: 12376 12376 447 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3059 +Encoding: 12377 12377 448 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u305A +Encoding: 12378 12378 449 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u305B +Encoding: 12379 12379 450 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u305C +Encoding: 12380 12380 451 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u305D +Encoding: 12381 12381 452 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u305E +Encoding: 12382 12382 453 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u305F +Encoding: 12383 12383 454 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3060 +Encoding: 12384 12384 455 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3061 +Encoding: 12385 12385 456 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3062 +Encoding: 12386 12386 457 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3063 +Encoding: 12387 12387 458 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3064 +Encoding: 12388 12388 459 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3065 +Encoding: 12389 12389 460 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3066 +Encoding: 12390 12390 461 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3067 +Encoding: 12391 12391 462 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3068 +Encoding: 12392 12392 463 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3069 +Encoding: 12393 12393 464 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u306A +Encoding: 12394 12394 465 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u306B +Encoding: 12395 12395 466 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u306C +Encoding: 12396 12396 467 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u306D +Encoding: 12397 12397 468 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u306E +Encoding: 12398 12398 469 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u306F +Encoding: 12399 12399 470 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3070 +Encoding: 12400 12400 471 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3071 +Encoding: 12401 12401 472 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3072 +Encoding: 12402 12402 473 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3073 +Encoding: 12403 12403 474 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3074 +Encoding: 12404 12404 475 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3075 +Encoding: 12405 12405 476 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3076 +Encoding: 12406 12406 477 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3077 +Encoding: 12407 12407 478 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3078 +Encoding: 12408 12408 479 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3079 +Encoding: 12409 12409 480 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u307A +Encoding: 12410 12410 481 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u307B +Encoding: 12411 12411 482 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u307C +Encoding: 12412 12412 483 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u307D +Encoding: 12413 12413 484 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u307E +Encoding: 12414 12414 485 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u307F +Encoding: 12415 12415 486 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3080 +Encoding: 12416 12416 487 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3081 +Encoding: 12417 12417 488 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3082 +Encoding: 12418 12418 489 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3083 +Encoding: 12419 12419 490 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3084 +Encoding: 12420 12420 491 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3085 +Encoding: 12421 12421 492 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3086 +Encoding: 12422 12422 493 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3087 +Encoding: 12423 12423 494 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3088 +Encoding: 12424 12424 495 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3089 +Encoding: 12425 12425 496 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308A +Encoding: 12426 12426 497 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308B +Encoding: 12427 12427 498 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308C +Encoding: 12428 12428 499 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308D +Encoding: 12429 12429 500 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308E +Encoding: 12430 12430 501 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u308F +Encoding: 12431 12431 502 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3090 +Encoding: 12432 12432 503 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3091 +Encoding: 12433 12433 504 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3092 +Encoding: 12434 12434 505 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3093 +Encoding: 12435 12435 506 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3094 +Encoding: 12436 12436 507 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u3095 +Encoding: 12437 12437 508 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3096 +Encoding: 12438 12438 509 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u3099 +Encoding: 12441 12441 510 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u309A +Encoding: 12442 12442 511 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u309B +Encoding: 12443 12443 512 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u309C +Encoding: 12444 12444 513 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u309D +Encoding: 12445 12445 514 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u309E +Encoding: 12446 12446 515 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u309F +Encoding: 12447 12447 516 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A0 +Encoding: 12448 12448 517 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A1 +Encoding: 12449 12449 518 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A2 +Encoding: 12450 12450 519 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A3 +Encoding: 12451 12451 520 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A4 +Encoding: 12452 12452 521 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A5 +Encoding: 12453 12453 522 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A6 +Encoding: 12454 12454 523 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A7 +Encoding: 12455 12455 524 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A8 +Encoding: 12456 12456 525 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30A9 +Encoding: 12457 12457 526 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30AA +Encoding: 12458 12458 527 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30AB +Encoding: 12459 12459 528 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30AC +Encoding: 12460 12460 529 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30AD +Encoding: 12461 12461 530 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30AE +Encoding: 12462 12462 531 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30AF +Encoding: 12463 12463 532 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30B0 +Encoding: 12464 12464 533 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30B1 +Encoding: 12465 12465 534 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30B2 +Encoding: 12466 12466 535 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30B3 +Encoding: 12467 12467 536 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30B4 +Encoding: 12468 12468 537 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30B5 +Encoding: 12469 12469 538 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30B6 +Encoding: 12470 12470 539 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30B7 +Encoding: 12471 12471 540 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30B8 +Encoding: 12472 12472 541 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30B9 +Encoding: 12473 12473 542 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30BA +Encoding: 12474 12474 543 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30BB +Encoding: 12475 12475 544 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30BC +Encoding: 12476 12476 545 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30BD +Encoding: 12477 12477 546 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30BE +Encoding: 12478 12478 547 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30BF +Encoding: 12479 12479 548 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C0 +Encoding: 12480 12480 549 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30C1 +Encoding: 12481 12481 550 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C2 +Encoding: 12482 12482 551 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30C3 +Encoding: 12483 12483 552 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C4 +Encoding: 12484 12484 553 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C5 +Encoding: 12485 12485 554 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30C6 +Encoding: 12486 12486 555 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C7 +Encoding: 12487 12487 556 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30C8 +Encoding: 12488 12488 557 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30C9 +Encoding: 12489 12489 558 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30CA +Encoding: 12490 12490 559 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30CB +Encoding: 12491 12491 560 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30CC +Encoding: 12492 12492 561 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30CD +Encoding: 12493 12493 562 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30CE +Encoding: 12494 12494 563 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30CF +Encoding: 12495 12495 564 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30D0 +Encoding: 12496 12496 565 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D1 +Encoding: 12497 12497 566 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D2 +Encoding: 12498 12498 567 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30D3 +Encoding: 12499 12499 568 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D4 +Encoding: 12500 12500 569 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D5 +Encoding: 12501 12501 570 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30D6 +Encoding: 12502 12502 571 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D7 +Encoding: 12503 12503 572 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30D8 +Encoding: 12504 12504 573 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30D9 +Encoding: 12505 12505 574 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30DA +Encoding: 12506 12506 575 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30DB +Encoding: 12507 12507 576 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30DC +Encoding: 12508 12508 577 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30DD +Encoding: 12509 12509 578 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +Refer: 0 60928 N 1 0 0 1 1632 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 10 60938 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30DE +Encoding: 12510 12510 579 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30DF +Encoding: 12511 12511 580 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E0 +Encoding: 12512 12512 581 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E1 +Encoding: 12513 12513 582 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E2 +Encoding: 12514 12514 583 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E3 +Encoding: 12515 12515 584 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E4 +Encoding: 12516 12516 585 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E5 +Encoding: 12517 12517 586 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E6 +Encoding: 12518 12518 587 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E7 +Encoding: 12519 12519 588 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E8 +Encoding: 12520 12520 589 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30E9 +Encoding: 12521 12521 590 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30EA +Encoding: 12522 12522 591 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30EB +Encoding: 12523 12523 592 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30EC +Encoding: 12524 12524 593 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30ED +Encoding: 12525 12525 594 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30EE +Encoding: 12526 12526 595 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30EF +Encoding: 12527 12527 596 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F0 +Encoding: 12528 12528 597 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F1 +Encoding: 12529 12529 598 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F2 +Encoding: 12530 12530 599 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F3 +Encoding: 12531 12531 600 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F4 +Encoding: 12532 12532 601 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30F5 +Encoding: 12533 12533 602 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F6 +Encoding: 12534 12534 603 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30F7 +Encoding: 12535 12535 604 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30F8 +Encoding: 12536 12536 605 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30F9 +Encoding: 12537 12537 606 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30FA +Encoding: 12538 12538 607 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30FB +Encoding: 12539 12539 608 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 0 0 2 +EndChar + +StartChar: u30FC +Encoding: 12540 12540 609 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30FD +Encoding: 12541 12541 610 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u30FE +Encoding: 12542 12542 611 +Width: 1632 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 1632 0 2 +Refer: 9 60937 N 1 0 0 1 1632 0 2 +EndChar + +StartChar: u30FF +Encoding: 12543 12543 612 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F0 +Encoding: 12784 12784 613 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F1 +Encoding: 12785 12785 614 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F2 +Encoding: 12786 12786 615 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F3 +Encoding: 12787 12787 616 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F4 +Encoding: 12788 12788 617 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F5 +Encoding: 12789 12789 618 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F6 +Encoding: 12790 12790 619 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F7 +Encoding: 12791 12791 620 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F8 +Encoding: 12792 12792 621 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31F9 +Encoding: 12793 12793 622 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FA +Encoding: 12794 12794 623 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FB +Encoding: 12795 12795 624 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FC +Encoding: 12796 12796 625 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FD +Encoding: 12797 12797 626 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FE +Encoding: 12798 12798 627 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u31FF +Encoding: 12799 12799 628 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uEE12 +Encoding: 60946 60946 629 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uEE13 +Encoding: 60947 60947 630 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uEE14 +Encoding: 60948 60948 631 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF01 +Encoding: 65281 65281 632 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar + +StartChar: uFF02 +Encoding: 65282 65282 633 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF03 +Encoding: 65283 65283 634 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +Refer: 17 60945 N 1 0 0 1 0 0 2 +EndChar + +StartChar: uFF04 +Encoding: 65284 65284 635 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF05 +Encoding: 65285 65285 636 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF06 +Encoding: 65286 65286 637 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF07 +Encoding: 65287 65287 638 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF08 +Encoding: 65288 65288 639 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF09 +Encoding: 65289 65289 640 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF0A +Encoding: 65290 65290 641 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF0B +Encoding: 65291 65291 642 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF0C +Encoding: 65292 65292 643 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF0D +Encoding: 65293 65293 644 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF0E +Encoding: 65294 65294 645 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 0 0 2 +EndChar + +StartChar: uFF0F +Encoding: 65295 65295 646 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF10 +Encoding: 65296 65296 647 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF11 +Encoding: 65297 65297 648 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF12 +Encoding: 65298 65298 649 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF13 +Encoding: 65299 65299 650 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF14 +Encoding: 65300 65300 651 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF15 +Encoding: 65301 65301 652 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF16 +Encoding: 65302 65302 653 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF17 +Encoding: 65303 65303 654 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF18 +Encoding: 65304 65304 655 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF19 +Encoding: 65305 65305 656 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF1A +Encoding: 65306 65306 657 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +Refer: 16 60944 N 1 0 0 1 0 0 2 +Refer: 17 60945 N 1 0 0 1 0 0 2 +EndChar + +StartChar: uFF1B +Encoding: 65307 65307 658 +Width: 200 +VWidth: 0 +Flags: HW +LayerCount: 2 +Fore +Refer: 16 60944 N 1 0 0 1 0 0 2 +EndChar + +StartChar: uFF1C +Encoding: 65308 65308 659 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF1D +Encoding: 65309 65309 660 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF1E +Encoding: 65310 65310 661 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF1F +Encoding: 65311 65311 662 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF20 +Encoding: 65312 65312 663 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF21 +Encoding: 65313 65313 664 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF22 +Encoding: 65314 65314 665 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF23 +Encoding: 65315 65315 666 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF24 +Encoding: 65316 65316 667 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF25 +Encoding: 65317 65317 668 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF26 +Encoding: 65318 65318 669 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF27 +Encoding: 65319 65319 670 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF28 +Encoding: 65320 65320 671 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF29 +Encoding: 65321 65321 672 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2A +Encoding: 65322 65322 673 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2B +Encoding: 65323 65323 674 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2C +Encoding: 65324 65324 675 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2D +Encoding: 65325 65325 676 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2E +Encoding: 65326 65326 677 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF2F +Encoding: 65327 65327 678 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF30 +Encoding: 65328 65328 679 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF31 +Encoding: 65329 65329 680 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF32 +Encoding: 65330 65330 681 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF33 +Encoding: 65331 65331 682 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF34 +Encoding: 65332 65332 683 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF35 +Encoding: 65333 65333 684 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF36 +Encoding: 65334 65334 685 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF37 +Encoding: 65335 65335 686 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF38 +Encoding: 65336 65336 687 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF39 +Encoding: 65337 65337 688 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3A +Encoding: 65338 65338 689 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3B +Encoding: 65339 65339 690 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3C +Encoding: 65340 65340 691 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3D +Encoding: 65341 65341 692 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3E +Encoding: 65342 65342 693 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF3F +Encoding: 65343 65343 694 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF40 +Encoding: 65344 65344 695 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF41 +Encoding: 65345 65345 696 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF42 +Encoding: 65346 65346 697 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF43 +Encoding: 65347 65347 698 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF44 +Encoding: 65348 65348 699 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF45 +Encoding: 65349 65349 700 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF46 +Encoding: 65350 65350 701 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF47 +Encoding: 65351 65351 702 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF48 +Encoding: 65352 65352 703 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF49 +Encoding: 65353 65353 704 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4A +Encoding: 65354 65354 705 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4B +Encoding: 65355 65355 706 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4C +Encoding: 65356 65356 707 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4D +Encoding: 65357 65357 708 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4E +Encoding: 65358 65358 709 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF4F +Encoding: 65359 65359 710 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF50 +Encoding: 65360 65360 711 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF51 +Encoding: 65361 65361 712 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF52 +Encoding: 65362 65362 713 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF53 +Encoding: 65363 65363 714 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF54 +Encoding: 65364 65364 715 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF55 +Encoding: 65365 65365 716 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF56 +Encoding: 65366 65366 717 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF57 +Encoding: 65367 65367 718 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF58 +Encoding: 65368 65368 719 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF59 +Encoding: 65369 65369 720 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF5A +Encoding: 65370 65370 721 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF5B +Encoding: 65371 65371 722 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF5C +Encoding: 65372 65372 723 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF5D +Encoding: 65373 65373 724 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF5E +Encoding: 65374 65374 725 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF65 +Encoding: 65381 65381 726 +Width: 0 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 14 60942 N 1 0 0 1 0 0 2 +EndChar + +StartChar: uFF66 +Encoding: 65382 65382 727 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF67 +Encoding: 65383 65383 728 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF68 +Encoding: 65384 65384 729 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF69 +Encoding: 65385 65385 730 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6A +Encoding: 65386 65386 731 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6B +Encoding: 65387 65387 732 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6C +Encoding: 65388 65388 733 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6D +Encoding: 65389 65389 734 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6E +Encoding: 65390 65390 735 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF6F +Encoding: 65391 65391 736 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF70 +Encoding: 65392 65392 737 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF71 +Encoding: 65393 65393 738 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF72 +Encoding: 65394 65394 739 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF73 +Encoding: 65395 65395 740 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF74 +Encoding: 65396 65396 741 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF75 +Encoding: 65397 65397 742 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF76 +Encoding: 65398 65398 743 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF77 +Encoding: 65399 65399 744 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF78 +Encoding: 65400 65400 745 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF79 +Encoding: 65401 65401 746 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7A +Encoding: 65402 65402 747 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7B +Encoding: 65403 65403 748 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7C +Encoding: 65404 65404 749 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7D +Encoding: 65405 65405 750 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7E +Encoding: 65406 65406 751 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF7F +Encoding: 65407 65407 752 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF80 +Encoding: 65408 65408 753 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF81 +Encoding: 65409 65409 754 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF82 +Encoding: 65410 65410 755 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF83 +Encoding: 65411 65411 756 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF84 +Encoding: 65412 65412 757 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF85 +Encoding: 65413 65413 758 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF86 +Encoding: 65414 65414 759 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF87 +Encoding: 65415 65415 760 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF88 +Encoding: 65416 65416 761 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF89 +Encoding: 65417 65417 762 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8A +Encoding: 65418 65418 763 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8B +Encoding: 65419 65419 764 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8C +Encoding: 65420 65420 765 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8D +Encoding: 65421 65421 766 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8E +Encoding: 65422 65422 767 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF8F +Encoding: 65423 65423 768 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF90 +Encoding: 65424 65424 769 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF91 +Encoding: 65425 65425 770 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF92 +Encoding: 65426 65426 771 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF93 +Encoding: 65427 65427 772 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF94 +Encoding: 65428 65428 773 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF95 +Encoding: 65429 65429 774 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF96 +Encoding: 65430 65430 775 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF97 +Encoding: 65431 65431 776 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF98 +Encoding: 65432 65432 777 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF99 +Encoding: 65433 65433 778 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9A +Encoding: 65434 65434 779 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9B +Encoding: 65435 65435 780 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9C +Encoding: 65436 65436 781 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9D +Encoding: 65437 65437 782 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9E +Encoding: 65438 65438 783 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFF9F +Encoding: 65439 65439 784 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE2 +Encoding: 65506 65506 785 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE3 +Encoding: 65507 65507 786 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE4 +Encoding: 65508 65508 787 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE5 +Encoding: 65509 65509 788 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE8 +Encoding: 65512 65512 789 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFE9 +Encoding: 65513 65513 790 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFEA +Encoding: 65514 65514 791 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFEB +Encoding: 65515 65515 792 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uFFEC +Encoding: 65516 65516 793 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 8 60936 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B000 +Encoding: 110592 110592 794 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B001 +Encoding: 110593 110593 795 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +Refer: 13 60941 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B150 +Encoding: 110928 110928 796 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B151 +Encoding: 110929 110929 797 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B152 +Encoding: 110930 110930 798 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B164 +Encoding: 110948 110948 799 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B165 +Encoding: 110949 110949 800 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 9 60937 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 12 60940 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B166 +Encoding: 110950 110950 801 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 10 60938 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1B167 +Encoding: 110951 110951 802 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 11 60939 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF0 +Encoding: 130032 130032 803 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF1 +Encoding: 130033 130033 804 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF2 +Encoding: 130034 130034 805 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF3 +Encoding: 130035 130035 806 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF4 +Encoding: 130036 130036 807 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF5 +Encoding: 130037 130037 808 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF6 +Encoding: 130038 130038 809 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF7 +Encoding: 130039 130039 810 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF8 +Encoding: 130040 130040 811 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 4 60932 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: u1FBF9 +Encoding: 130041 130041 812 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +Fore +Refer: 0 60928 N 1 0 0 1 816 0 2 +Refer: 1 60929 N 1 0 0 1 816 0 2 +Refer: 2 60930 N 1 0 0 1 816 0 2 +Refer: 3 60931 N 1 0 0 1 816 0 2 +Refer: 5 60933 N 1 0 0 1 816 0 2 +Refer: 6 60934 N 1 0 0 1 816 0 2 +Refer: 7 60935 N 1 0 0 1 816 0 2 +EndChar + +StartChar: uni0009 +Encoding: 9 9 813 +Width: 816 +VWidth: 200 +Flags: HW +LayerCount: 2 +EndChar +EndChars +EndSplineFont diff --git a/src/spice2x/external/easywsclient/easywsclient.cpp b/src/spice2x/external/easywsclient/easywsclient.cpp index a183978..55092d1 100644 --- a/src/spice2x/external/easywsclient/easywsclient.cpp +++ b/src/spice2x/external/easywsclient/easywsclient.cpp @@ -1,552 +1,552 @@ -// This code comes from: -// https://github.com/dhbaird/easywsclient -// -// To get the latest version: -// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp -// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp - -#include "easywsclient.hpp" - -#ifdef _WIN32 - #ifndef WIN32_LEAN_AND_MEAN - #define WIN32_LEAN_AND_MEAN - #endif - #include - #include - #pragma comment( lib, "ws2_32" ) - #include - #include - #include - #include - #include - typedef SOCKET socket_t; - #ifndef _SSIZE_T_DEFINED - typedef int ssize_t; - #define _SSIZE_T_DEFINED - #endif - #ifndef _SOCKET_T_DEFINED - typedef SOCKET socket_t; - #define _SOCKET_T_DEFINED - #endif - #if defined(_MSC_VER) && !defined(snprintf) - #define snprintf _snprintf_s - #endif - #include - #define socketerrno WSAGetLastError() - #define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS - #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK -#else - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - typedef int socket_t; - #ifndef INVALID_SOCKET - #define INVALID_SOCKET (-1) - #endif - #ifndef SOCKET_ERROR - #define SOCKET_ERROR (-1) - #endif - #define closesocket(s) ::close(s) - #include - #define socketerrno errno - #define SOCKET_EAGAIN_EINPROGRESS EAGAIN - #define SOCKET_EWOULDBLOCK EWOULDBLOCK -#endif - -#include -#include - -#include - -#include "util/logging.h" - -// When false, easywsclient's diagnostics are suppressed. The host application -// opts in by setting this flag (see obs_websocket.cpp). -bool EASYWSCLIENT_LOGGING_ENABLED = false; - -// Route easywsclient's diagnostic output through the project logger instead of -// writing to stderr, without editing the upstream source lines below: these two -// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc. -// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set. -static inline void easywsclient_logf(const char *fmt, ...) { - if (!EASYWSCLIENT_LOGGING_ENABLED) { - return; - } - char buf[1024]; - va_list args; - va_start(args, fmt); - vsnprintf(buf, sizeof(buf), fmt, args); - va_end(args); - // trim trailing newline(s); the logger appends its own - size_t len = strlen(buf); - while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { - buf[--len] = '\0'; - } - log_misc("easywsclient", "{}", buf); -} -#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__) -#define fputs(str, stream) easywsclient_logf("%s", str) - -using namespace easywsclient; - -namespace { // private module-only namespace - -socket_t hostname_connect(const std::string& hostname, int port) { - struct addrinfo hints; - struct addrinfo *result; - struct addrinfo *p; - int ret; - socket_t sockfd = INVALID_SOCKET; - char sport[16]; - memset(&hints, 0, sizeof(hints)); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - snprintf(sport, 16, "%d", port); - if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0) - { - fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret)); - return 1; - } - for(p = result; p != NULL; p = p->ai_next) - { - sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); - if (sockfd == INVALID_SOCKET) { continue; } - if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) { - break; - } - closesocket(sockfd); - sockfd = INVALID_SOCKET; - } - freeaddrinfo(result); - return sockfd; -} - - -class _DummyWebSocket : public easywsclient::WebSocket -{ - public: - void poll(int timeout) { } - void send(const std::string& message) { } - void sendBinary(const std::string& message) { } - void sendBinary(const std::vector& message) { } - void sendPing() { } - void close() { } - readyStateValues getReadyState() const { return CLOSED; } - void _dispatch(Callback_Imp & callable) { } - void _dispatchBinary(BytesCallback_Imp& callable) { } -}; - - -class _RealWebSocket : public easywsclient::WebSocket -{ - public: - // http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol - // - // 0 1 2 3 - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - // +-+-+-+-+-------+-+-------------+-------------------------------+ - // |F|R|R|R| opcode|M| Payload len | Extended payload length | - // |I|S|S|S| (4) |A| (7) | (16/64) | - // |N|V|V|V| |S| | (if payload len==126/127) | - // | |1|2|3| |K| | | - // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - // | Extended payload length continued, if payload len == 127 | - // + - - - - - - - - - - - - - - - +-------------------------------+ - // | |Masking-key, if MASK set to 1 | - // +-------------------------------+-------------------------------+ - // | Masking-key (continued) | Payload Data | - // +-------------------------------- - - - - - - - - - - - - - - - + - // : Payload Data continued ... : - // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - // | Payload Data continued ... | - // +---------------------------------------------------------------+ - - struct wsheader_type { - unsigned header_size; - bool fin; - bool mask; - enum opcode_type { - CONTINUATION = 0x0, - TEXT_FRAME = 0x1, - BINARY_FRAME = 0x2, - CLOSE = 8, - PING = 9, - PONG = 0xa, - } opcode; - int N0; - uint64_t N; - uint8_t masking_key[4]; - }; - - std::vector rxbuf; - std::vector txbuf; - std::vector receivedData; - - socket_t sockfd; - readyStateValues readyState; - bool useMask; - bool isRxBad; - - _RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) { - } - - readyStateValues getReadyState() const { - return readyState; - } - - void poll(int timeout) { // timeout in milliseconds - if (readyState == CLOSED) { - if (timeout > 0) { - timeval tv = { timeout/1000, (timeout%1000) * 1000 }; - select(0, NULL, NULL, NULL, &tv); - } - return; - } - if (timeout != 0) { - fd_set rfds; - fd_set wfds; - timeval tv = { timeout/1000, (timeout%1000) * 1000 }; - FD_ZERO(&rfds); - FD_ZERO(&wfds); - FD_SET(sockfd, &rfds); - if (txbuf.size()) { FD_SET(sockfd, &wfds); } - select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL); - } - while (true) { - // FD_ISSET(0, &rfds) will be true - int N = rxbuf.size(); - ssize_t ret; - rxbuf.resize(N + 1500); - ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0); - if (false) { } - else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) { - rxbuf.resize(N); - break; - } - else if (ret <= 0) { - rxbuf.resize(N); - closesocket(sockfd); - readyState = CLOSED; - fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr); - break; - } - else { - rxbuf.resize(N + ret); - } - } - while (txbuf.size()) { - int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0); - if (false) { } // ?? - else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) { - break; - } - else if (ret <= 0) { - closesocket(sockfd); - readyState = CLOSED; - fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr); - break; - } - else { - txbuf.erase(txbuf.begin(), txbuf.begin() + ret); - } - } - if (!txbuf.size() && readyState == CLOSING) { - closesocket(sockfd); - readyState = CLOSED; - } - } - - virtual void _dispatch(Callback_Imp & callable) { - struct CallbackAdapter : public BytesCallback_Imp - // Adapt void(const std::string&) to void(const std::string&) - { - Callback_Imp& callable; - CallbackAdapter(Callback_Imp& callable) : callable(callable) { } - void operator()(const std::vector& message) { - std::string stringMessage(message.begin(), message.end()); - callable(stringMessage); - } - }; - CallbackAdapter bytesCallback(callable); - _dispatchBinary(bytesCallback); - } - - virtual void _dispatchBinary(BytesCallback_Imp& callable) { - // TODO: consider acquiring a lock on rxbuf... - while (true) { - wsheader_type ws; - if (rxbuf.size() < 2) { return; /* Need at least 2 */ } - const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume - ws.fin = (data[0] & 0x80) == 0x80; - ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f); - ws.mask = (data[1] & 0x80) == 0x80; - ws.N0 = (data[1] & 0x7f); - ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0); - if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ } - int i = 0; - if (ws.N0 < 126) { - ws.N = ws.N0; - i = 2; - } - else if (ws.N0 == 126) { - ws.N = 0; - ws.N |= ((uint64_t) data[2]) << 8; - ws.N |= ((uint64_t) data[3]) << 0; - i = 4; - } - else if (ws.N0 == 127) { - ws.N = 0; - ws.N |= ((uint64_t) data[2]) << 56; - ws.N |= ((uint64_t) data[3]) << 48; - ws.N |= ((uint64_t) data[4]) << 40; - ws.N |= ((uint64_t) data[5]) << 32; - ws.N |= ((uint64_t) data[6]) << 24; - ws.N |= ((uint64_t) data[7]) << 16; - ws.N |= ((uint64_t) data[8]) << 8; - ws.N |= ((uint64_t) data[9]) << 0; - i = 10; - } - if (ws.mask) { - ws.masking_key[0] = ((uint8_t) data[i+0]) << 0; - ws.masking_key[1] = ((uint8_t) data[i+1]) << 0; - ws.masking_key[2] = ((uint8_t) data[i+2]) << 0; - ws.masking_key[3] = ((uint8_t) data[i+3]) << 0; - } - else { - ws.masking_key[0] = 0; - ws.masking_key[1] = 0; - ws.masking_key[2] = 0; - ws.masking_key[3] = 0; - } - - // Note: The checks above should hopefully ensure this addition - // cannot overflow: - if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ } - - // We got a whole message, now do something with it: - if (false) { } - else if ( - ws.opcode == wsheader_type::TEXT_FRAME - || ws.opcode == wsheader_type::BINARY_FRAME - || ws.opcode == wsheader_type::CONTINUATION - ) { - if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } - receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed - if (ws.fin) { - callable((const std::vector) receivedData); - receivedData.erase(receivedData.begin(), receivedData.end()); - std::vector ().swap(receivedData);// free memory - } - } - else if (ws.opcode == wsheader_type::PING) { - if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } - std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N); - sendData(wsheader_type::PONG, data.size(), data.begin(), data.end()); - } - else if (ws.opcode == wsheader_type::PONG) { } - else if (ws.opcode == wsheader_type::CLOSE) { close(); } - else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); } - - rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N); - } - } - - void sendPing() { - std::string empty; - sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end()); - } - - void send(const std::string& message) { - sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end()); - } - - void sendBinary(const std::string& message) { - sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); - } - - void sendBinary(const std::vector& message) { - sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); - } - - template - void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) { - // TODO: - // Masking key should (must) be derived from a high quality random - // number generator, to mitigate attacks on non-WebSocket friendly - // middleware: - const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 }; - // TODO: consider acquiring a lock on txbuf... - if (readyState == CLOSING || readyState == CLOSED) { return; } - std::vector header; - header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0); - header[0] = 0x80 | type; - if (false) { } - else if (message_size < 126) { - header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0); - if (useMask) { - header[2] = masking_key[0]; - header[3] = masking_key[1]; - header[4] = masking_key[2]; - header[5] = masking_key[3]; - } - } - else if (message_size < 65536) { - header[1] = 126 | (useMask ? 0x80 : 0); - header[2] = (message_size >> 8) & 0xff; - header[3] = (message_size >> 0) & 0xff; - if (useMask) { - header[4] = masking_key[0]; - header[5] = masking_key[1]; - header[6] = masking_key[2]; - header[7] = masking_key[3]; - } - } - else { // TODO: run coverage testing here - header[1] = 127 | (useMask ? 0x80 : 0); - header[2] = (message_size >> 56) & 0xff; - header[3] = (message_size >> 48) & 0xff; - header[4] = (message_size >> 40) & 0xff; - header[5] = (message_size >> 32) & 0xff; - header[6] = (message_size >> 24) & 0xff; - header[7] = (message_size >> 16) & 0xff; - header[8] = (message_size >> 8) & 0xff; - header[9] = (message_size >> 0) & 0xff; - if (useMask) { - header[10] = masking_key[0]; - header[11] = masking_key[1]; - header[12] = masking_key[2]; - header[13] = masking_key[3]; - } - } - // N.B. - txbuf will keep growing until it can be transmitted over the socket: - txbuf.insert(txbuf.end(), header.begin(), header.end()); - txbuf.insert(txbuf.end(), message_begin, message_end); - if (useMask) { - size_t message_offset = txbuf.size() - message_size; - for (size_t i = 0; i != message_size; ++i) { - txbuf[message_offset + i] ^= masking_key[i&0x3]; - } - } - } - - void close() { - if(readyState == CLOSING || readyState == CLOSED) { return; } - readyState = CLOSING; - uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key - std::vector header(closeFrame, closeFrame+6); - txbuf.insert(txbuf.end(), header.begin(), header.end()); - } - -}; - - -easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) { - char host[512]; - int port; - char path[512]; - if (url.size() >= 512) { - fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str()); - return NULL; - } - if (origin.size() >= 200) { - fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str()); - return NULL; - } - if (false) { } - else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) { - } - else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) { - port = 80; - } - else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) { - path[0] = '\0'; - } - else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) { - port = 80; - path[0] = '\0'; - } - else { - fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str()); - return NULL; - } - //fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path); - socket_t sockfd = hostname_connect(host, port); - if (sockfd == INVALID_SOCKET) { - fprintf(stderr, "Unable to connect to %s:%d\n", host, port); - return NULL; - } - { - // XXX: this should be done non-blocking, - char line[1024]; - int status; - int i; - snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0); - if (port == 80) { - snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0); - } - else { - snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0); - } - snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0); - snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0); - if (!origin.empty()) { - snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0); - } - snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0); - snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0); - snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0); - for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } } - line[i] = 0; - if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; } - if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; } - // TODO: verify response headers, - while (true) { - for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } } - if (line[0] == '\r' && line[1] == '\n') { break; } - } - } - int flag = 1; - setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm -#ifdef _WIN32 - u_long on = 1; - ioctlsocket(sockfd, FIONBIO, &on); -#else - fcntl(sockfd, F_SETFL, O_NONBLOCK); -#endif - //fprintf(stderr, "Connected to: %s\n", url.c_str()); - return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask)); -} - -} // end of module-only namespace - - - -namespace easywsclient { - -WebSocket::pointer WebSocket::create_dummy() { - static pointer dummy = pointer(new _DummyWebSocket); - return dummy; -} - - -WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) { - return ::from_url(url, true, origin); -} - -WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) { - return ::from_url(url, false, origin); -} - - -} // namespace easywsclient +// This code comes from: +// https://github.com/dhbaird/easywsclient +// +// To get the latest version: +// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp +// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp + +#include "easywsclient.hpp" + +#ifdef _WIN32 + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + #include + #include + #pragma comment( lib, "ws2_32" ) + #include + #include + #include + #include + #include + typedef SOCKET socket_t; + #ifndef _SSIZE_T_DEFINED + typedef int ssize_t; + #define _SSIZE_T_DEFINED + #endif + #ifndef _SOCKET_T_DEFINED + typedef SOCKET socket_t; + #define _SOCKET_T_DEFINED + #endif + #if defined(_MSC_VER) && !defined(snprintf) + #define snprintf _snprintf_s + #endif + #include + #define socketerrno WSAGetLastError() + #define SOCKET_EAGAIN_EINPROGRESS WSAEINPROGRESS + #define SOCKET_EWOULDBLOCK WSAEWOULDBLOCK +#else + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + typedef int socket_t; + #ifndef INVALID_SOCKET + #define INVALID_SOCKET (-1) + #endif + #ifndef SOCKET_ERROR + #define SOCKET_ERROR (-1) + #endif + #define closesocket(s) ::close(s) + #include + #define socketerrno errno + #define SOCKET_EAGAIN_EINPROGRESS EAGAIN + #define SOCKET_EWOULDBLOCK EWOULDBLOCK +#endif + +#include +#include + +#include + +#include "util/logging.h" + +// When false, easywsclient's diagnostics are suppressed. The host application +// opts in by setting this flag (see obs_websocket.cpp). +bool EASYWSCLIENT_LOGGING_ENABLED = false; + +// Route easywsclient's diagnostic output through the project logger instead of +// writing to stderr, without editing the upstream source lines below: these two +// macros redirect fprintf(stderr, ...) / fputs(..., stderr) to log_misc. +// Only emitted when EASYWSCLIENT_LOGGING_ENABLED is set. +static inline void easywsclient_logf(const char *fmt, ...) { + if (!EASYWSCLIENT_LOGGING_ENABLED) { + return; + } + char buf[1024]; + va_list args; + va_start(args, fmt); + vsnprintf(buf, sizeof(buf), fmt, args); + va_end(args); + // trim trailing newline(s); the logger appends its own + size_t len = strlen(buf); + while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) { + buf[--len] = '\0'; + } + log_misc("easywsclient", "{}", buf); +} +#define fprintf(stream, ...) easywsclient_logf(__VA_ARGS__) +#define fputs(str, stream) easywsclient_logf("%s", str) + +using namespace easywsclient; + +namespace { // private module-only namespace + +socket_t hostname_connect(const std::string& hostname, int port) { + struct addrinfo hints; + struct addrinfo *result; + struct addrinfo *p; + int ret; + socket_t sockfd = INVALID_SOCKET; + char sport[16]; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + snprintf(sport, 16, "%d", port); + if ((ret = getaddrinfo(hostname.c_str(), sport, &hints, &result)) != 0) + { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(ret)); + return 1; + } + for(p = result; p != NULL; p = p->ai_next) + { + sockfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (sockfd == INVALID_SOCKET) { continue; } + if (connect(sockfd, p->ai_addr, p->ai_addrlen) != SOCKET_ERROR) { + break; + } + closesocket(sockfd); + sockfd = INVALID_SOCKET; + } + freeaddrinfo(result); + return sockfd; +} + + +class _DummyWebSocket : public easywsclient::WebSocket +{ + public: + void poll(int timeout) { } + void send(const std::string& message) { } + void sendBinary(const std::string& message) { } + void sendBinary(const std::vector& message) { } + void sendPing() { } + void close() { } + readyStateValues getReadyState() const { return CLOSED; } + void _dispatch(Callback_Imp & callable) { } + void _dispatchBinary(BytesCallback_Imp& callable) { } +}; + + +class _RealWebSocket : public easywsclient::WebSocket +{ + public: + // http://tools.ietf.org/html/rfc6455#section-5.2 Base Framing Protocol + // + // 0 1 2 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-------+-+-------------+-------------------------------+ + // |F|R|R|R| opcode|M| Payload len | Extended payload length | + // |I|S|S|S| (4) |A| (7) | (16/64) | + // |N|V|V|V| |S| | (if payload len==126/127) | + // | |1|2|3| |K| | | + // +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + // | Extended payload length continued, if payload len == 127 | + // + - - - - - - - - - - - - - - - +-------------------------------+ + // | |Masking-key, if MASK set to 1 | + // +-------------------------------+-------------------------------+ + // | Masking-key (continued) | Payload Data | + // +-------------------------------- - - - - - - - - - - - - - - - + + // : Payload Data continued ... : + // + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + // | Payload Data continued ... | + // +---------------------------------------------------------------+ + + struct wsheader_type { + unsigned header_size; + bool fin; + bool mask; + enum opcode_type { + CONTINUATION = 0x0, + TEXT_FRAME = 0x1, + BINARY_FRAME = 0x2, + CLOSE = 8, + PING = 9, + PONG = 0xa, + } opcode; + int N0; + uint64_t N; + uint8_t masking_key[4]; + }; + + std::vector rxbuf; + std::vector txbuf; + std::vector receivedData; + + socket_t sockfd; + readyStateValues readyState; + bool useMask; + bool isRxBad; + + _RealWebSocket(socket_t sockfd, bool useMask) : sockfd(sockfd), readyState(OPEN), useMask(useMask), isRxBad(false) { + } + + readyStateValues getReadyState() const { + return readyState; + } + + void poll(int timeout) { // timeout in milliseconds + if (readyState == CLOSED) { + if (timeout > 0) { + timeval tv = { timeout/1000, (timeout%1000) * 1000 }; + select(0, NULL, NULL, NULL, &tv); + } + return; + } + if (timeout != 0) { + fd_set rfds; + fd_set wfds; + timeval tv = { timeout/1000, (timeout%1000) * 1000 }; + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_SET(sockfd, &rfds); + if (txbuf.size()) { FD_SET(sockfd, &wfds); } + select(sockfd + 1, &rfds, &wfds, NULL, timeout > 0 ? &tv : NULL); + } + while (true) { + // FD_ISSET(0, &rfds) will be true + int N = rxbuf.size(); + ssize_t ret; + rxbuf.resize(N + 1500); + ret = recv(sockfd, (char*)&rxbuf[0] + N, 1500, 0); + if (false) { } + else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) { + rxbuf.resize(N); + break; + } + else if (ret <= 0) { + rxbuf.resize(N); + closesocket(sockfd); + readyState = CLOSED; + fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr); + break; + } + else { + rxbuf.resize(N + ret); + } + } + while (txbuf.size()) { + int ret = ::send(sockfd, (char*)&txbuf[0], txbuf.size(), 0); + if (false) { } // ?? + else if (ret < 0 && (socketerrno == SOCKET_EWOULDBLOCK || socketerrno == SOCKET_EAGAIN_EINPROGRESS)) { + break; + } + else if (ret <= 0) { + closesocket(sockfd); + readyState = CLOSED; + fputs(ret < 0 ? "Connection error!\n" : "Connection closed!\n", stderr); + break; + } + else { + txbuf.erase(txbuf.begin(), txbuf.begin() + ret); + } + } + if (!txbuf.size() && readyState == CLOSING) { + closesocket(sockfd); + readyState = CLOSED; + } + } + + virtual void _dispatch(Callback_Imp & callable) { + struct CallbackAdapter : public BytesCallback_Imp + // Adapt void(const std::string&) to void(const std::string&) + { + Callback_Imp& callable; + CallbackAdapter(Callback_Imp& callable) : callable(callable) { } + void operator()(const std::vector& message) { + std::string stringMessage(message.begin(), message.end()); + callable(stringMessage); + } + }; + CallbackAdapter bytesCallback(callable); + _dispatchBinary(bytesCallback); + } + + virtual void _dispatchBinary(BytesCallback_Imp& callable) { + // TODO: consider acquiring a lock on rxbuf... + while (true) { + wsheader_type ws; + if (rxbuf.size() < 2) { return; /* Need at least 2 */ } + const uint8_t * data = (uint8_t *) &rxbuf[0]; // peek, but don't consume + ws.fin = (data[0] & 0x80) == 0x80; + ws.opcode = (wsheader_type::opcode_type) (data[0] & 0x0f); + ws.mask = (data[1] & 0x80) == 0x80; + ws.N0 = (data[1] & 0x7f); + ws.header_size = 2 + (ws.N0 == 126? 2 : 0) + (ws.N0 == 127? 8 : 0) + (ws.mask? 4 : 0); + if (rxbuf.size() < ws.header_size) { return; /* Need: ws.header_size - rxbuf.size() */ } + int i = 0; + if (ws.N0 < 126) { + ws.N = ws.N0; + i = 2; + } + else if (ws.N0 == 126) { + ws.N = 0; + ws.N |= ((uint64_t) data[2]) << 8; + ws.N |= ((uint64_t) data[3]) << 0; + i = 4; + } + else if (ws.N0 == 127) { + ws.N = 0; + ws.N |= ((uint64_t) data[2]) << 56; + ws.N |= ((uint64_t) data[3]) << 48; + ws.N |= ((uint64_t) data[4]) << 40; + ws.N |= ((uint64_t) data[5]) << 32; + ws.N |= ((uint64_t) data[6]) << 24; + ws.N |= ((uint64_t) data[7]) << 16; + ws.N |= ((uint64_t) data[8]) << 8; + ws.N |= ((uint64_t) data[9]) << 0; + i = 10; + } + if (ws.mask) { + ws.masking_key[0] = ((uint8_t) data[i+0]) << 0; + ws.masking_key[1] = ((uint8_t) data[i+1]) << 0; + ws.masking_key[2] = ((uint8_t) data[i+2]) << 0; + ws.masking_key[3] = ((uint8_t) data[i+3]) << 0; + } + else { + ws.masking_key[0] = 0; + ws.masking_key[1] = 0; + ws.masking_key[2] = 0; + ws.masking_key[3] = 0; + } + + // Note: The checks above should hopefully ensure this addition + // cannot overflow: + if (rxbuf.size() < ws.header_size+ws.N) { return; /* Need: ws.header_size+ws.N - rxbuf.size() */ } + + // We got a whole message, now do something with it: + if (false) { } + else if ( + ws.opcode == wsheader_type::TEXT_FRAME + || ws.opcode == wsheader_type::BINARY_FRAME + || ws.opcode == wsheader_type::CONTINUATION + ) { + if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } + receivedData.insert(receivedData.end(), rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N);// just feed + if (ws.fin) { + callable((const std::vector) receivedData); + receivedData.erase(receivedData.begin(), receivedData.end()); + std::vector ().swap(receivedData);// free memory + } + } + else if (ws.opcode == wsheader_type::PING) { + if (ws.mask) { for (size_t i = 0; i != ws.N; ++i) { rxbuf[i+ws.header_size] ^= ws.masking_key[i&0x3]; } } + std::string data(rxbuf.begin()+ws.header_size, rxbuf.begin()+ws.header_size+(size_t)ws.N); + sendData(wsheader_type::PONG, data.size(), data.begin(), data.end()); + } + else if (ws.opcode == wsheader_type::PONG) { } + else if (ws.opcode == wsheader_type::CLOSE) { close(); } + else { fprintf(stderr, "ERROR: Got unexpected WebSocket message.\n"); close(); } + + rxbuf.erase(rxbuf.begin(), rxbuf.begin() + ws.header_size+(size_t)ws.N); + } + } + + void sendPing() { + std::string empty; + sendData(wsheader_type::PING, empty.size(), empty.begin(), empty.end()); + } + + void send(const std::string& message) { + sendData(wsheader_type::TEXT_FRAME, message.size(), message.begin(), message.end()); + } + + void sendBinary(const std::string& message) { + sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); + } + + void sendBinary(const std::vector& message) { + sendData(wsheader_type::BINARY_FRAME, message.size(), message.begin(), message.end()); + } + + template + void sendData(wsheader_type::opcode_type type, uint64_t message_size, Iterator message_begin, Iterator message_end) { + // TODO: + // Masking key should (must) be derived from a high quality random + // number generator, to mitigate attacks on non-WebSocket friendly + // middleware: + const uint8_t masking_key[4] = { 0x12, 0x34, 0x56, 0x78 }; + // TODO: consider acquiring a lock on txbuf... + if (readyState == CLOSING || readyState == CLOSED) { return; } + std::vector header; + header.assign(2 + (message_size >= 126 ? 2 : 0) + (message_size >= 65536 ? 6 : 0) + (useMask ? 4 : 0), 0); + header[0] = 0x80 | type; + if (false) { } + else if (message_size < 126) { + header[1] = (message_size & 0xff) | (useMask ? 0x80 : 0); + if (useMask) { + header[2] = masking_key[0]; + header[3] = masking_key[1]; + header[4] = masking_key[2]; + header[5] = masking_key[3]; + } + } + else if (message_size < 65536) { + header[1] = 126 | (useMask ? 0x80 : 0); + header[2] = (message_size >> 8) & 0xff; + header[3] = (message_size >> 0) & 0xff; + if (useMask) { + header[4] = masking_key[0]; + header[5] = masking_key[1]; + header[6] = masking_key[2]; + header[7] = masking_key[3]; + } + } + else { // TODO: run coverage testing here + header[1] = 127 | (useMask ? 0x80 : 0); + header[2] = (message_size >> 56) & 0xff; + header[3] = (message_size >> 48) & 0xff; + header[4] = (message_size >> 40) & 0xff; + header[5] = (message_size >> 32) & 0xff; + header[6] = (message_size >> 24) & 0xff; + header[7] = (message_size >> 16) & 0xff; + header[8] = (message_size >> 8) & 0xff; + header[9] = (message_size >> 0) & 0xff; + if (useMask) { + header[10] = masking_key[0]; + header[11] = masking_key[1]; + header[12] = masking_key[2]; + header[13] = masking_key[3]; + } + } + // N.B. - txbuf will keep growing until it can be transmitted over the socket: + txbuf.insert(txbuf.end(), header.begin(), header.end()); + txbuf.insert(txbuf.end(), message_begin, message_end); + if (useMask) { + size_t message_offset = txbuf.size() - message_size; + for (size_t i = 0; i != message_size; ++i) { + txbuf[message_offset + i] ^= masking_key[i&0x3]; + } + } + } + + void close() { + if(readyState == CLOSING || readyState == CLOSED) { return; } + readyState = CLOSING; + uint8_t closeFrame[6] = {0x88, 0x80, 0x00, 0x00, 0x00, 0x00}; // last 4 bytes are a masking key + std::vector header(closeFrame, closeFrame+6); + txbuf.insert(txbuf.end(), header.begin(), header.end()); + } + +}; + + +easywsclient::WebSocket::pointer from_url(const std::string& url, bool useMask, const std::string& origin) { + char host[512]; + int port; + char path[512]; + if (url.size() >= 512) { + fprintf(stderr, "ERROR: url size limit exceeded: %s\n", url.c_str()); + return NULL; + } + if (origin.size() >= 200) { + fprintf(stderr, "ERROR: origin size limit exceeded: %s\n", origin.c_str()); + return NULL; + } + if (false) { } + else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3) { + } + else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2) { + port = 80; + } + else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2) { + path[0] = '\0'; + } + else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1) { + port = 80; + path[0] = '\0'; + } + else { + fprintf(stderr, "ERROR: Could not parse WebSocket url: %s\n", url.c_str()); + return NULL; + } + //fprintf(stderr, "easywsclient: connecting: host=%s port=%d path=/%s\n", host, port, path); + socket_t sockfd = hostname_connect(host, port); + if (sockfd == INVALID_SOCKET) { + fprintf(stderr, "Unable to connect to %s:%d\n", host, port); + return NULL; + } + { + // XXX: this should be done non-blocking, + char line[1024]; + int status; + int i; + snprintf(line, 1024, "GET /%s HTTP/1.1\r\n", path); ::send(sockfd, line, strlen(line), 0); + if (port == 80) { + snprintf(line, 1024, "Host: %s\r\n", host); ::send(sockfd, line, strlen(line), 0); + } + else { + snprintf(line, 1024, "Host: %s:%d\r\n", host, port); ::send(sockfd, line, strlen(line), 0); + } + snprintf(line, 1024, "Upgrade: websocket\r\n"); ::send(sockfd, line, strlen(line), 0); + snprintf(line, 1024, "Connection: Upgrade\r\n"); ::send(sockfd, line, strlen(line), 0); + if (!origin.empty()) { + snprintf(line, 1024, "Origin: %s\r\n", origin.c_str()); ::send(sockfd, line, strlen(line), 0); + } + snprintf(line, 1024, "Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==\r\n"); ::send(sockfd, line, strlen(line), 0); + snprintf(line, 1024, "Sec-WebSocket-Version: 13\r\n"); ::send(sockfd, line, strlen(line), 0); + snprintf(line, 1024, "\r\n"); ::send(sockfd, line, strlen(line), 0); + for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } } + line[i] = 0; + if (i == 1023) { fprintf(stderr, "ERROR: Got invalid status line connecting to: %s\n", url.c_str()); return NULL; } + if (sscanf(line, "HTTP/1.1 %d", &status) != 1 || status != 101) { fprintf(stderr, "ERROR: Got bad status connecting to %s: %s", url.c_str(), line); return NULL; } + // TODO: verify response headers, + while (true) { + for (i = 0; i < 2 || (i < 1023 && line[i-2] != '\r' && line[i-1] != '\n'); ++i) { if (recv(sockfd, line+i, 1, 0) == 0) { return NULL; } } + if (line[0] == '\r' && line[1] == '\n') { break; } + } + } + int flag = 1; + setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, (char*) &flag, sizeof(flag)); // Disable Nagle's algorithm +#ifdef _WIN32 + u_long on = 1; + ioctlsocket(sockfd, FIONBIO, &on); +#else + fcntl(sockfd, F_SETFL, O_NONBLOCK); +#endif + //fprintf(stderr, "Connected to: %s\n", url.c_str()); + return easywsclient::WebSocket::pointer(new _RealWebSocket(sockfd, useMask)); +} + +} // end of module-only namespace + + + +namespace easywsclient { + +WebSocket::pointer WebSocket::create_dummy() { + static pointer dummy = pointer(new _DummyWebSocket); + return dummy; +} + + +WebSocket::pointer WebSocket::from_url(const std::string& url, const std::string& origin) { + return ::from_url(url, true, origin); +} + +WebSocket::pointer WebSocket::from_url_no_mask(const std::string& url, const std::string& origin) { + return ::from_url(url, false, origin); +} + + +} // namespace easywsclient diff --git a/src/spice2x/external/easywsclient/easywsclient.hpp b/src/spice2x/external/easywsclient/easywsclient.hpp index 0cad04b..df60500 100644 --- a/src/spice2x/external/easywsclient/easywsclient.hpp +++ b/src/spice2x/external/easywsclient/easywsclient.hpp @@ -1,73 +1,73 @@ -#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD -#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD - -// This code comes from: -// https://github.com/dhbaird/easywsclient -// -// To get the latest version: -// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp -// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp - -#include -#include -#include - -namespace easywsclient { - -struct Callback_Imp { virtual void operator()(const std::string& message) = 0; }; -struct BytesCallback_Imp { virtual void operator()(const std::vector& message) = 0; }; - -class WebSocket { - public: - typedef WebSocket * pointer; - typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues; - - // Factories: - static pointer create_dummy(); - static pointer from_url(const std::string& url, const std::string& origin = std::string()); - static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string()); - - // Interfaces: - virtual ~WebSocket() { } - virtual void poll(int timeout = 0) = 0; // timeout in milliseconds - virtual void send(const std::string& message) = 0; - virtual void sendBinary(const std::string& message) = 0; - virtual void sendBinary(const std::vector& message) = 0; - virtual void sendPing() = 0; - virtual void close() = 0; - virtual readyStateValues getReadyState() const = 0; - - template - void dispatch(Callable callable) - // For callbacks that accept a string argument. - { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers - struct _Callback : public Callback_Imp { - Callable& callable; - _Callback(Callable& callable) : callable(callable) { } - void operator()(const std::string& message) { callable(message); } - }; - _Callback callback(callable); - _dispatch(callback); - } - - template - void dispatchBinary(Callable callable) - // For callbacks that accept a std::vector argument. - { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers - struct _Callback : public BytesCallback_Imp { - Callable& callable; - _Callback(Callable& callable) : callable(callable) { } - void operator()(const std::vector& message) { callable(message); } - }; - _Callback callback(callable); - _dispatchBinary(callback); - } - - protected: - virtual void _dispatch(Callback_Imp& callable) = 0; - virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0; -}; - -} // namespace easywsclient - -#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */ +#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD +#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD + +// This code comes from: +// https://github.com/dhbaird/easywsclient +// +// To get the latest version: +// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp +// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp + +#include +#include +#include + +namespace easywsclient { + +struct Callback_Imp { virtual void operator()(const std::string& message) = 0; }; +struct BytesCallback_Imp { virtual void operator()(const std::vector& message) = 0; }; + +class WebSocket { + public: + typedef WebSocket * pointer; + typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues; + + // Factories: + static pointer create_dummy(); + static pointer from_url(const std::string& url, const std::string& origin = std::string()); + static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string()); + + // Interfaces: + virtual ~WebSocket() { } + virtual void poll(int timeout = 0) = 0; // timeout in milliseconds + virtual void send(const std::string& message) = 0; + virtual void sendBinary(const std::string& message) = 0; + virtual void sendBinary(const std::vector& message) = 0; + virtual void sendPing() = 0; + virtual void close() = 0; + virtual readyStateValues getReadyState() const = 0; + + template + void dispatch(Callable callable) + // For callbacks that accept a string argument. + { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers + struct _Callback : public Callback_Imp { + Callable& callable; + _Callback(Callable& callable) : callable(callable) { } + void operator()(const std::string& message) { callable(message); } + }; + _Callback callback(callable); + _dispatch(callback); + } + + template + void dispatchBinary(Callable callable) + // For callbacks that accept a std::vector argument. + { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers + struct _Callback : public BytesCallback_Imp { + Callable& callable; + _Callback(Callable& callable) : callable(callable) { } + void operator()(const std::vector& message) { callable(message); } + }; + _Callback callback(callable); + _dispatchBinary(callback); + } + + protected: + virtual void _dispatch(Callback_Imp& callable) = 0; + virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0; +}; + +} // namespace easywsclient + +#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */ diff --git a/src/spice2x/external/fpng/fpng.cpp b/src/spice2x/external/fpng/fpng.cpp new file mode 100644 index 0000000..68be28d --- /dev/null +++ b/src/spice2x/external/fpng/fpng.cpp @@ -0,0 +1,3222 @@ +// fpng.cpp 1.0.6 - Fast 24/32bpp .PNG image writer/reader. See unlicense at the end of this file. +// PNG's generated by this code have been tested to load successfully with stb_image.h, lodepng.cpp, wuffs, libpng, and pngcheck. +// +// Uses code from the simple PNG writer function by Alex Evans, 2011. Released into the public domain: https://gist.github.com/908299 +// Some low-level Deflate/Huffman functions derived from the original 2011 Google Code version of miniz (public domain by R. Geldreich, Jr.): https://code.google.com/archive/p/miniz/ +// Low-level Huffman code size function: public domain, originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. +// +// Optional config macros: +// FPNG_NO_SSE - Set to 1 to completely disable SSE usage, even on x86/x64. By default, on x86/x64 it's enabled. +// FPNG_DISABLE_DECODE_CRC32_CHECKS - Set to 1 to disable PNG chunk CRC-32 tests, for improved fuzzing. Defaults to 0. +// FPNG_USE_UNALIGNED_LOADS - Set to 1 to indicate it's OK to read/write unaligned 32-bit/64-bit values. Defaults to 0, unless x86/x64. +// +// With gcc/clang on x86, compile with -msse4.1 -mpclmul -fno-strict-aliasing +// Only tested with -fno-strict-aliasing (which the Linux kernel uses, and MSVC's default). +// +#include "fpng.h" +#include +#include + +#ifdef _MSC_VER + #pragma warning (disable:4127) // conditional expression is constant +#endif + +// Set FPNG_NO_SSE to 1 to completely disable SSE usage. +#ifndef FPNG_NO_SSE + #define FPNG_NO_SSE (0) +#endif + +// Detect if we're compiling on x86/x64 +#if defined(_M_IX86) || defined(_M_X64) || defined(__i386__) || defined(__i386) || defined(__i486__) || defined(__i486) || defined(i386) || defined(__ia64__) || defined(__x86_64__) + #define FPNG_X86_OR_X64_CPU (1) +#else + #define FPNG_X86_OR_X64_CPU (0) +#endif + +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + #ifdef _MSC_VER + #include + #endif + #include // SSE + #include // SSE2 + #include // SSE4.1 + #include // pclmul +#endif + +#ifndef FPNG_NO_STDIO + #include +#endif + +// Allow the disabling of the chunk data CRC32 checks, for fuzz testing of the decoder +#ifndef FPNG_DISABLE_DECODE_CRC32_CHECKS + #define FPNG_DISABLE_DECODE_CRC32_CHECKS (0) +#endif + +// Using unaligned loads and stores causes errors when using UBSan. Jam it off. +#if defined(__has_feature) + #if __has_feature(undefined_behavior_sanitizer) + #undef FPNG_USE_UNALIGNED_LOADS + #define FPNG_USE_UNALIGNED_LOADS (0) + #endif +#endif + +// Set to 0 if your platform doesn't support unaligned 32-bit/64-bit reads/writes. +#ifndef FPNG_USE_UNALIGNED_LOADS + #if FPNG_X86_OR_X64_CPU + // On x86/x64 we default to enabled, for a noticeable perf gain. + #define FPNG_USE_UNALIGNED_LOADS (1) + #else + #define FPNG_USE_UNALIGNED_LOADS (0) + #endif +#endif + +#if defined(_MSC_VER) || defined(__MINGW32__) || FPNG_X86_OR_X64_CPU + #ifndef __LITTLE_ENDIAN + #define __LITTLE_ENDIAN 1234 + #endif + #ifndef __BIG_ENDIAN + #define __BIG_ENDIAN 4321 + #endif + + // Assume little endian on Windows/x86/x64. + #define __BYTE_ORDER __LITTLE_ENDIAN +#elif defined(__APPLE__) + #define __BYTE_ORDER __BYTE_ORDER__ + #define __LITTLE_ENDIAN __LITTLE_ENDIAN__ + #define __BIG_ENDIAN __BIG_ENDIAN__ +#else + // for __BYTE_ORDER (__LITTLE_ENDIAN or __BIG_ENDIAN) + #include + + #ifndef __LITTLE_ENDIAN + #define __LITTLE_ENDIAN 1234 + #endif + #ifndef __BIG_ENDIAN + #define __BIG_ENDIAN 4321 + #endif +#endif + +#if !defined(__BYTE_ORDER) + #error __BYTE_ORDER undefined. Compile with -D__BYTE_ORDER=1234 for little endian or -D__BYTE_ORDER=4321 for big endian. +#endif + +namespace fpng +{ + static const int FPNG_FALSE = 0; + static const uint8_t FPNG_FDEC_VERSION = 0; + static const uint32_t FPNG_MAX_SUPPORTED_DIM = 1 << 24; + + template static inline S maximum(S a, S b) { return (a > b) ? a : b; } + template static inline S minimum(S a, S b) { return (a < b) ? a : b; } + + static inline uint32_t simple_swap32(uint32_t x) { return (x >> 24) | ((x >> 8) & 0x0000FF00) | ((x << 8) & 0x00FF0000) | (x << 24); } + static inline uint64_t simple_swap64(uint64_t x) { return (((uint64_t)simple_swap32((uint32_t)x)) << 32U) | simple_swap32((uint32_t)(x >> 32U)); } + + static inline uint32_t swap32(uint32_t x) + { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap32(x); +#else + return simple_swap32(x); +#endif + } + + static inline uint64_t swap64(uint64_t x) + { +#if defined(__GNUC__) || defined(__clang__) + return __builtin_bswap64(x); +#else + return simple_swap64(x); +#endif + } + +#if FPNG_USE_UNALIGNED_LOADS + #if __BYTE_ORDER == __BIG_ENDIAN + #define READ_LE32(p) swap32(*reinterpret_cast(p)) + #define WRITE_LE32(p, v) *reinterpret_cast(p) = swap32((uint32_t)(v)) + #define WRITE_LE64(p, v) *reinterpret_cast(p) = swap64((uint64_t)(v)) + + #define READ_BE32(p) *reinterpret_cast(p) + #else + #define READ_LE32(p) (*reinterpret_cast(p)) + #define WRITE_LE32(p, v) *reinterpret_cast(p) = (uint32_t)(v) + #define WRITE_LE64(p, v) *reinterpret_cast(p) = (uint64_t)(v) + + #define READ_BE32(p) swap32(*reinterpret_cast(p)) + #endif +#else + // A good compiler should be able to optimize these routines - hopefully. They are crucial for performance. + static inline uint32_t READ_LE32(const void* p) + { + const uint8_t* pBytes = (const uint8_t*)p; + return ((uint32_t)pBytes[0]) | (((uint32_t)pBytes[1]) << 8U) | (((uint32_t)pBytes[2]) << 16U) | (((uint32_t)pBytes[3]) << 24U); + } + + static inline uint32_t READ_BE32(const void* p) + { + const uint8_t* pBytes = (const uint8_t*)p; + return ((uint32_t)pBytes[3]) | (((uint32_t)pBytes[2]) << 8U) | (((uint32_t)pBytes[1]) << 16U) | (((uint32_t)pBytes[0]) << 24U); + } + + static inline void WRITE_LE32(const void* p, uint32_t v) + { + uint8_t* pBytes = (uint8_t*)p; + pBytes[0] = (uint8_t)(v); + pBytes[1] = (uint8_t)(v >> 8); + pBytes[2] = (uint8_t)(v >> 16); + pBytes[3] = (uint8_t)(v >> 24); + } + + static inline void WRITE_LE64(const void* p, uint64_t v) + { + uint8_t* pBytes = (uint8_t*)p; + pBytes[0] = (uint8_t)(v); + pBytes[1] = (uint8_t)(v >> 8); + pBytes[2] = (uint8_t)(v >> 16); + pBytes[3] = (uint8_t)(v >> 24); + pBytes[4] = (uint8_t)(v >> 32); + pBytes[5] = (uint8_t)(v >> 40); + pBytes[6] = (uint8_t)(v >> 48); + pBytes[7] = (uint8_t)(v >> 56); + } +#endif + + // Customized the very common case of reading a 24bpp pixel from memory + static inline uint32_t READ_RGB_PIXEL(const void* p) + { +#if FPNG_USE_UNALIGNED_LOADS + return READ_LE32(p) & 0xFFFFFF; +#else + const uint8_t* pBytes = (const uint8_t*)p; + return ((uint32_t)pBytes[0]) | (((uint32_t)pBytes[1]) << 8U) | (((uint32_t)pBytes[2]) << 16U); +#endif + } + + // See "Slicing by 4" CRC-32 algorithm here: + // https://create.stephan-brumme.com/crc32/ + + // Precomputed 4KB of CRC-32 tables + static const uint32_t g_crc32_4[4][256] = { + {00, 016701630226, 035603460454, 023102250672, 0733342031, 016032572217, 035130722465, 023631112643, 01666704062, 017167134244, 034065364436, 022764554610, 01155446053, 017654276275, 034756026407, 022057616621, 03555610144, 015254020362, 036356270510, 020457440736, 03266552175, 015567362353, 036465132521, 020364702707, 02333114126, 014432724300, 037530574572, 021231344754, 02400256117, 014301466331, 037203636543, 021502006765, + 07333420310, 011432210136, 032530040744, 024231670562, 07400762321, 011301152107, 032203302775, 024502532553, 06555324372, 010254514154, 033356744726, 025457174500, 06266066343, 010567656165, 033465406717, 025364236531, 04666230254, 012167400072, 031065650600, 027764060426, 04155172265, 012654742043, 031756512631, 027057322417, 05000534236, 013701304010, 030603154662, 026102764444, 05733676207, 013032046021, 030130216653, 026631426475, + 016667040620, 0166670406, 023064420274, 035765210052, 016154302611, 0655532437, 023757762245, 035056152063, 017001744642, 01700174464, 022602324216, 034103514030, 017732406673, 01033236455, 022131066227, 034630656001, 015332650764, 03433060542, 020531230330, 036230400116, 015401512755, 03300322573, 020202172301, 036503742127, 014554154706, 02255764520, 021357534352, 037456304174, 014267216737, 02566426511, 021464676363, 037365046145, + 011554460530, 07255250716, 024357000164, 032456630342, 011267722501, 07566112727, 024464342155, 032365572373, 010332364552, 06433554774, 025531704106, 033230134320, 010401026563, 06300616745, 025202446137, 033503276311, 012001270474, 04700440652, 027602610020, 031103020206, 012732132445, 04033702663, 027131552011, 031630362237, 013667574416, 05166344630, 026064114042, 030765724264, 013154636427, 05655006601, 026757256073, 030056466255, + 035556101440, 023257731666, 0355561014, 016454351232, 035265243471, 023564473657, 0466623025, 016367013203, 034330605422, 022431035604, 01533265076, 017232455250, 034403547413, 022302377635, 01200127047, 017501717261, 036003711504, 020702121722, 03600371150, 015101541376, 036730453535, 020031263713, 03133033161, 015632603347, 037665015566, 021164625740, 02066475132, 014767245314, 037156357557, 021657567771, 02755737103, 014054107325, + 032665521750, 024164311576, 07066141304, 011767771122, 032156663761, 024657053547, 07755203335, 011054433113, 033003225732, 025702415514, 06600645366, 010101075140, 033730167703, 025031757525, 06133507357, 010632337171, 031330331614, 027431501432, 04533751240, 012232161066, 031403073625, 027302643403, 04200413271, 012501223057, 030556435676, 026257205450, 05355055222, 013454665004, 030265777647, 026564147461, 05466317213, 013367527035, + 023331141260, 035430771046, 016532521634, 0233311412, 023402203251, 035303433077, 016201663605, 0500053423, 022557645202, 034256075024, 017354225656, 01455415470, 022264507233, 034565337015, 017467167667, 01366757441, 020664751324, 036165161102, 015067331770, 03766501556, 020157413315, 036656223133, 015754073741, 03055643567, 021002055346, 037703665160, 014601435712, 02100205534, 021731317377, 037030527151, 014132777723, 02633147505, + 024002561170, 032703351356, 011601101524, 07100731702, 024731623141, 032030013367, 011132243515, 07633473733, 025664265112, 033165455334, 010067605546, 06766035760, 025157127123, 033656717305, 010754547577, 06055377751, 027557371034, 031256541212, 012354711460, 04455121646, 027264033005, 031565603223, 012467453451, 04366263677, 026331475056, 030430245270, 013532015402, 05233625624, 026402737067, 030303107241, 013201357433, 05500567615, + }, { 00,03106630501,06215461202,05313251703,014433142404,017535772105,012626523606,011720313307,031066305010,032160535511,037273764212,034375154713,025455247414,026553477115,023640626616,020746016317,011260411121,012366221420,017075070323,014173640622,05653553525,06755363024,03446132727,0540702226,020206714131,023300124430,026013375333,025115545632,034635656535,037733066034,032420237737,031526407236, + 022541022242,021447612743,024754443040,027652273541,036172160646,035074750347,030367501444,033261331145,013527327252,010421517753,015732746050,016634176551,07114265656,04012455357,01301604454,02207034155,033721433363,030627203662,035534052161,036432662460,027312571767,024214341266,021107110565,022001720064,02747736373,01641106672,04552357171,07454567470,016374674777,015272044276,010161215575,013067425074, + 036036247405,035130477104,030223626607,033325016306,022405305001,021503535500,024610764203,027716154702,07050142415,04156772114,01245523617,02343313316,013463000011,010565630510,015676461213,016770251712,027256656524,024350066025,021043237726,022145407227,033665714120,030763124421,035470375322,036576545623,016230553534,015336363035,010025132736,013123702237,02603411130,01705221431,04416070332,07510640633, + 014577265647,017471455346,012762604445,011664034144,0144327243,03042517742,06351746041,05257176540,025511160657,026417750356,023704501455,020602331154,031122022253,032024612752,037337443051,034231273550,05717674766,06611044267,03502215564,0404425065,011324736362,012222106663,017131357160,014037567461,034771571776,037677341277,032564110574,031462720075,020342433372,023244203673,026157052170,025051662471, + 07340714113,04246124412,01155375311,02053545610,013773656517,010675066016,015566237715,016460407214,036326411103,035220221402,030133070301,033035640600,022715553507,021613363006,024500132705,027406702204,016120305032,015026535533,010335764230,013233154731,02513247436,01415477137,04706626634,07600016335,027146000022,024040630523,021353461220,022255251721,033575142426,030473772127,035760523624,036666313325, + 025601736351,026707106650,023414357153,020512567452,031232674755,032334044254,037027215557,034121425056,014667433341,017761203640,012472052143,011574662442,0254571745,03352341244,06041110547,05147720046,034461327270,037567517771,032674746072,031772176573,020052265674,023154455375,026247604476,025341034177,05407022260,06501612761,03612443062,0714273563,011034160664,012132750365,017221501466,014327331167, + 031376553516,032270363017,037163132714,034065702215,025745411112,026643221413,023550070310,020456640611,0310656506,03216066007,06105237704,05003407205,014723714102,017625124403,012536375300,011430545601,020116142437,023010772136,026303523635,025205313334,034525000033,037423630532,032730461231,031636251730,011170247427,012076477126,017365626625,014263016324,05543305023,06445535522,03756764221,0650154720, + 013637571754,010731341255,015422110556,016524720057,07204433350,04302203651,01011052152,02117662453,022651674744,021757044245,024444215546,027542425047,036262736340,035364106641,030077357142,033171567443,02457160675,01551750374,04642501477,07744331176,016064022271,015162612770,010271443073,013377273572,033431265665,030537455364,035624604467,036722034166,027002327261,024104517760,021217746063,022311176562, + }, { 00,0160465067,0341152156,0221537131,0702324334,0662741353,0443276262,0523613205,01604650670,01764235617,01545702726,01425367741,01106574544,01066111523,01247426412,01327043475,03411521560,03571144507,03750473436,03630016451,03313605654,03273260633,03052757702,03132332765,02215371310,02375714377,02154223246,02034646221,02517055024,02477430043,02656107172,02736562115, + 07023243340,07143626327,07362311216,07202774271,07721167074,07641502013,07460035122,07500450145,06627413530,06747076557,06566541466,06406124401,06125737604,06045352663,06264665752,06304200735,04432762620,04552307647,04773630776,04613255711,04330446514,04250023573,04071514442,04111171425,05236132050,05356557037,05177060106,05017405161,05534216364,05454673303,05675344232,05715721255, + 016046506700,016126163767,016307454656,016267031631,016744622434,016624247453,016405770562,016565315505,017642356170,017722733117,017503204026,017463661041,017140072244,017020417223,017201120312,017361545375,015457027260,015537442207,015716175336,015676510351,015355303154,015235766133,015014251002,015174634065,014253677410,014333212477,014112725546,014072340521,014551553724,014431136743,014610401672,014770064615, + 011065745440,011105320427,011324617516,011244272571,011767461774,011607004713,011426533622,011546156645,010661115230,010701570257,010520047366,010440422301,010163231104,010003654163,010222363052,010342706035,012474264120,012514601147,012735336076,012655753011,012376140214,012216525273,012037012342,012157477325,013270434750,013310051737,013131566606,013051103661,013572710464,013412375403,013633642532,013753227555, + 034115215600,034075670667,034254347756,034334722731,034617131534,034777554553,034556063462,034436406405,035711445070,035671020017,035450517126,035530172141,035013761344,035173304323,035352633212,035232256275,037504734360,037464351307,037645666236,037725203251,037206410054,037366075033,037147542102,037027127165,036300164510,036260501577,036041036446,036121453421,036402240624,036562625643,036743312772,036623777715, + 033136056540,033056433527,033277104416,033317561471,033634372674,033754717613,033575220722,033415645745,032732606330,032652263357,032473754266,032513331201,032030522004,032150147063,032371470152,032211015135,030527577020,030447112047,030666425176,030706040111,030225653314,030345236373,030164701242,030004364225,031323327650,031243742637,031062275706,031102610761,031421003564,031541466503,031760151432,031600534455, + 022153713100,022033376167,022212641056,022372224031,022651437234,022731052253,022510565362,022470100305,023757143770,023637526717,023416011626,023576474641,023055267444,023135602423,023314335512,023274750575,021542232460,021422657407,021603360536,021763705551,021240116754,021320573733,021101044602,021061421665,020346462210,020226007277,020007530346,020167155321,020444746124,020524323143,020705614072,020665271015, + 025170550240,025010135227,025231402316,025351067371,025672674174,025712211113,025533726022,025453343045,024774300430,024614765457,024435252566,024555637501,024076024704,024116441763,024337176652,024257513635,026561071720,026401414747,026620123676,026740546611,026263355414,026303730473,026122207542,026042662525,027365621150,027205244137,027024773006,027144316061,027467505264,027507160203,027726457332,027646032355, + }, { 00,027057063545,025202344213,02255327756,021730513527,06767570062,04532657734,023565634271,030555024357,017502047612,015757360144,032700303401,011265537670,036232554335,034067673463,013030610126,012006253637,035051230372,037204117424,010253174161,033736740310,014761723655,016534404103,031563467446,022553277560,05504214025,07751133773,020706150236,03263764047,024234707502,026061420254,01036443711, + 024014527476,03043544133,01216663665,026241600320,05724034151,022773057414,020526370342,07571313607,014541503721,033516560264,031743647532,016714624077,035271010206,012226073743,010073354015,037024337550,036012774241,011045717704,013210430052,034247453517,017722267766,030775204223,032520123575,015577140030,06547750116,021510733453,023745414305,04712477640,027277243431,0220220174,02075107622,025022164367, + 023305054075,04352037530,06107310266,021150373723,02435547552,025462524017,027637603741,0660660204,013650070322,034607013667,036452334131,011405357474,032160563605,015137500340,017362627416,030335644153,031303207642,016354264307,014101143451,033156120114,010433714365,037464777620,035631450176,012666433433,01656223515,026601240050,024454167706,03403104243,020166730032,07131753577,05364474221,022333417764, + 07311573403,020346510146,022113637610,05144654355,026421060124,01476003461,03623324337,024674347672,037644557754,010613534211,012446613547,035411670002,016174044273,031123027736,033376300060,014321363525,015317720234,032340743771,030115464027,017142407562,034427233713,013470250256,011625177500,036672114045,025642704163,02615767426,0440440370,027417423635,04172217444,023125274101,021370153657,06327130312, + 035526333073,012571350536,010724077260,037773014725,014216620554,033241643011,031014564747,016043507202,05073317324,022024374661,020271053137,07226030472,024743604603,03714667346,01541540410,026516523155,027520160644,0577103301,02722224457,025775247112,06210473363,021247410626,023012737170,04045754435,017075144513,030022127056,032277200700,015220263245,036745457034,011712434571,013547713227,034510770762, + 011532614405,036565677140,034730550616,013767533353,030202307122,017255364467,015000043331,032057020674,021067630752,06030653217,04265574541,023232517004,0757323275,027700340730,025555067066,02502004523,03534447232,024563424777,026736703021,01761760564,022204154715,05253137250,07006210506,020051273043,033061463165,014036400420,016263727376,031234744633,012751170442,035706113107,037553234651,010504257314, + 016623367006,031674304543,033421023215,014476040750,037113674521,010144617064,012311530732,035346553277,026376343351,01321320614,03174007142,024123064407,07446650676,020411633333,022644514465,05613577120,04625134631,023672157374,021427270422,06470213167,025115427316,02142444653,0317763105,027340700440,034370110566,013327173023,011172254775,036125237230,015440403041,032417460504,030642747252,017615724717, + 032637640470,015660623135,017435504663,030462567326,013107353157,034150330412,036305017344,011352074601,02362664727,025335607262,027160520534,0137543071,023452377200,04405314745,06650033013,021607050556,020631413247,07666470702,05433757054,022464734511,01101100760,026156163225,024303244573,03354227036,010364437110,037333454455,035166773303,012131710646,031454124437,016403147172,014656260624,033601203361, + } }; + + static uint32_t crc32_slice_by_4(const void* pData, size_t data_len, uint32_t cur_crc32 = 0) + { + uint32_t crc = ~cur_crc32; + const uint32_t* pData32 = static_cast(pData); + + for (; data_len >= sizeof(uint32_t); ++pData32, data_len -= 4) + { + uint32_t v = READ_LE32(pData32) ^ crc; + crc = g_crc32_4[0][v >> 24] ^ g_crc32_4[1][(v >> 16) & 0xFF] ^ g_crc32_4[2][(v >> 8) & 0xFF] ^ g_crc32_4[3][v & 0xFF]; + } + + for (const uint8_t* pData8 = reinterpret_cast(pData32); data_len; --data_len) + crc = (crc >> 8) ^ g_crc32_4[0][(crc & 0xFF) ^ *pData8++]; + + return ~crc; + } + +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + // See Fast CRC Computation for Generic Polynomials Using PCLMULQDQ Instruction": + // https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/fast-crc-computation-generic-polynomials-pclmulqdq-paper.pdf + // Requires PCLMUL and SSE 4.1. This function skips Step 1 (fold by 4) for simplicity/less code. + static uint32_t crc32_pclmul(const uint8_t* p, size_t size, uint32_t crc) + { + assert(size >= 16); + + // See page 22 (bit reflected constants for gzip) +#ifdef _MSC_VER + static const uint64_t __declspec(align(16)) +#else + static const uint64_t __attribute__((aligned(16))) +#endif + s_u[2] = { 0x1DB710641, 0x1F7011641 }, s_k5k0[2] = { 0x163CD6124, 0 }, s_k3k4[2] = { 0x1751997D0, 0xCCAA009E }; + + // Load first 16 bytes, apply initial CRC32 + __m128i b = _mm_xor_si128(_mm_cvtsi32_si128(~crc), _mm_loadu_si128(reinterpret_cast(p))); + + // We're skipping directly to Step 2 page 12 - iteratively folding by 1 (by 4 is overkill for our needs) + const __m128i k3k4 = _mm_load_si128(reinterpret_cast(s_k3k4)); + + for (size -= 16, p += 16; size >= 16; size -= 16, p += 16) + b = _mm_xor_si128(_mm_xor_si128(_mm_clmulepi64_si128(b, k3k4, 17), _mm_loadu_si128(reinterpret_cast(p))), _mm_clmulepi64_si128(b, k3k4, 0)); + + // Final stages: fold to 64-bits, 32-bit Barrett reduction + const __m128i z = _mm_set_epi32(0, ~0, 0, ~0), u = _mm_load_si128(reinterpret_cast(s_u)); + b = _mm_xor_si128(_mm_srli_si128(b, 8), _mm_clmulepi64_si128(b, k3k4, 16)); + b = _mm_xor_si128(_mm_clmulepi64_si128(_mm_and_si128(b, z), _mm_loadl_epi64(reinterpret_cast(s_k5k0)), 0), _mm_srli_si128(b, 4)); + return ~_mm_extract_epi32(_mm_xor_si128(b, _mm_clmulepi64_si128(_mm_and_si128(_mm_clmulepi64_si128(_mm_and_si128(b, z), u, 16), z), u, 0)), 1); + } + + static uint32_t crc32_sse41_simd(const unsigned char* buf, size_t len, uint32_t prev_crc32) + { + if (len < 16) + return crc32_slice_by_4(buf, len, prev_crc32); + + uint32_t simd_len = len & ~15; + uint32_t c = crc32_pclmul(buf, simd_len, prev_crc32); + return crc32_slice_by_4(buf + simd_len, len - simd_len, c); + } +#endif + +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + +#ifndef _MSC_VER + static void do_cpuid(uint32_t eax, uint32_t ecx, uint32_t* regs) + { + uint32_t ebx = 0, edx = 0; + +#if defined(__PIC__) && defined(__i386__) + __asm__("movl %%ebx, %%edi;" + "cpuid;" + "xchgl %%ebx, %%edi;" + : "=D"(ebx), "+a"(eax), "+c"(ecx), "=d"(edx)); +#else + __asm__("cpuid;" : "+b"(ebx), "+a"(eax), "+c"(ecx), "=d"(edx)); +#endif + + regs[0] = eax; regs[1] = ebx; regs[2] = ecx; regs[3] = edx; + } +#endif + + struct cpu_info + { + cpu_info() { memset(this, 0, sizeof(*this)); } + + bool m_initialized, m_has_fpu, m_has_mmx, m_has_sse, m_has_sse2, m_has_sse3, m_has_ssse3, m_has_sse41, m_has_sse42, m_has_avx, m_has_avx2, m_has_pclmulqdq; + + void init() + { + if (m_initialized) + return; + + int regs[4]; + +#ifdef _MSC_VER + __cpuid(regs, 0); +#else + do_cpuid(0, 0, (uint32_t*)regs); +#endif + + const uint32_t max_eax = regs[0]; + if (max_eax >= 1U) + { +#ifdef _MSC_VER + __cpuid(regs, 1); +#else + do_cpuid(1, 0, (uint32_t*)regs); +#endif + extract_x86_flags(regs[2], regs[3]); + } + + if (max_eax >= 7U) + { +#ifdef _MSC_VER + __cpuidex(regs, 7, 0); +#else + do_cpuid(7, 0, (uint32_t*)regs); +#endif + extract_x86_extended_flags(regs[1]); + } + + m_initialized = true; + } + + bool can_use_sse41() const { return m_has_sse && m_has_sse2 && m_has_sse3 && m_has_ssse3 && m_has_sse41; } + bool can_use_pclmul() const { return m_has_pclmulqdq && can_use_sse41(); } + + private: + void extract_x86_flags(uint32_t ecx, uint32_t edx) + { + m_has_fpu = (edx & (1 << 0)) != 0; m_has_mmx = (edx & (1 << 23)) != 0; m_has_sse = (edx & (1 << 25)) != 0; m_has_sse2 = (edx & (1 << 26)) != 0; + m_has_sse3 = (ecx & (1 << 0)) != 0; m_has_ssse3 = (ecx & (1 << 9)) != 0; m_has_sse41 = (ecx & (1 << 19)) != 0; m_has_sse42 = (ecx & (1 << 20)) != 0; + m_has_pclmulqdq = (ecx & (1 << 1)) != 0; m_has_avx = (ecx & (1 << 28)) != 0; + } + + void extract_x86_extended_flags(uint32_t ebx) { m_has_avx2 = (ebx & (1 << 5)) != 0; } + }; + + cpu_info g_cpu_info; + + void fpng_init() + { + g_cpu_info.init(); + } +#else + void fpng_init() + { + } +#endif + + bool fpng_cpu_supports_sse41() + { +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + assert(g_cpu_info.m_initialized); + return g_cpu_info.can_use_sse41(); +#else + return false; +#endif + } + + uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32) + { +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + if (g_cpu_info.can_use_pclmul()) + return crc32_sse41_simd(static_cast(pData), size, prev_crc32); +#endif + + return crc32_slice_by_4(pData, size, prev_crc32); + } + +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + // See "Fast Computation of Adler32 Checksums": + // https://www.intel.com/content/www/us/en/developer/articles/technical/fast-computation-of-adler32-checksums.html + // SSE 4.1, 16 bytes per iteration + static uint32_t adler32_sse_16(const uint8_t* p, size_t len, uint32_t initial) + { + uint32_t s1 = initial & 0xFFFF, s2 = initial >> 16; + const uint32_t K = 65521; + + while (len >= 16) + { + __m128i a = _mm_setr_epi32(s1, 0, 0, 0), b = _mm_setzero_si128(), c = _mm_setzero_si128(), d = _mm_setzero_si128(), + e = _mm_setzero_si128(), f = _mm_setzero_si128(), g = _mm_setzero_si128(), h = _mm_setzero_si128(); + + const size_t n = minimum(len >> 4, 5552); + + for (size_t i = 0; i < n; i++) + { + const __m128i v = _mm_loadu_si128((const __m128i*)(p + i * 16)); + a = _mm_add_epi32(a, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(0, 0, 0, 0)))); b = _mm_add_epi32(b, a); + c = _mm_add_epi32(c, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(1, 1, 1, 1)))); d = _mm_add_epi32(d, c); + e = _mm_add_epi32(e, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(2, 2, 2, 2)))); f = _mm_add_epi32(f, e); + g = _mm_add_epi32(g, _mm_cvtepu8_epi32(_mm_shuffle_epi32(v, _MM_SHUFFLE(3, 3, 3, 3)))); h = _mm_add_epi32(h, g); + } + + uint32_t sa[16], sb[16]; + _mm_storeu_si128((__m128i*)sa, a); _mm_storeu_si128((__m128i*)(sa + 4), c); + _mm_storeu_si128((__m128i*)sb, b); _mm_storeu_si128((__m128i*)(sb + 4), d); + _mm_storeu_si128((__m128i*)(sa + 8), e); _mm_storeu_si128((__m128i*)(sa + 12), g); + _mm_storeu_si128((__m128i*)(sb + 8), f); _mm_storeu_si128((__m128i*)(sb + 12), h); + + // This could be vectorized, but it's only executed every 5552*16 iterations. + uint64_t vs1 = 0; + for (uint32_t i = 0; i < 16; i++) + vs1 += sa[i]; + + uint64_t vs2_a = 0; + for (uint32_t i = 0; i < 16; i++) + vs2_a += sa[i] * (uint64_t)i; + uint64_t vs2_b = 0; + for (uint32_t i = 0; i < 16; i++) + vs2_b += sb[i]; + vs2_b *= 16U; + uint64_t vs2 = vs2_b - vs2_a + s2; + + s1 = (uint32_t)(vs1 % K); + s2 = (uint32_t)(vs2 % K); + + p += n * 16; + len -= n * 16; + } + + for (; len; len--) + { + s1 += *p++; + s2 += s1; + } + + return (s1 % K) | ((s2 % K) << 16); + } +#endif + + static uint32_t fpng_adler32_scalar(const uint8_t* ptr, size_t buf_len, uint32_t adler) + { + uint32_t i, s1 = (uint32_t)(adler & 0xffff), s2 = (uint32_t)(adler >> 16); uint32_t block_len = (uint32_t)(buf_len % 5552); + if (!ptr) return FPNG_ADLER32_INIT; + while (buf_len) { + for (i = 0; i + 7 < block_len; i += 8, ptr += 8) { + s1 += ptr[0], s2 += s1; s1 += ptr[1], s2 += s1; s1 += ptr[2], s2 += s1; s1 += ptr[3], s2 += s1; + s1 += ptr[4], s2 += s1; s1 += ptr[5], s2 += s1; s1 += ptr[6], s2 += s1; s1 += ptr[7], s2 += s1; + } + for (; i < block_len; ++i) s1 += *ptr++, s2 += s1; + s1 %= 65521U, s2 %= 65521U; buf_len -= block_len; block_len = 5552; + } + return (s2 << 16) + s1; + } + + uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler) + { +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + if (g_cpu_info.can_use_sse41()) + return adler32_sse_16((const uint8_t*)pData, size, adler); +#endif + return fpng_adler32_scalar((const uint8_t*)pData, size, adler); + } + + // Ensure we've been configured for endianness correctly. + static inline bool endian_check() + { + uint32_t endian_check = 0; + WRITE_LE32(&endian_check, 0x1234ABCD); + const uint32_t first_byte = reinterpret_cast(&endian_check)[0]; + return first_byte == 0xCD; + } + + static const uint16_t g_defl_len_sym[256] = { + 257,258,259,260,261,262,263,264,265,265,266,266,267,267,268,268,269,269,269,269,270,270,270,270,271,271,271,271,272,272,272,272, + 273,273,273,273,273,273,273,273,274,274,274,274,274,274,274,274,275,275,275,275,275,275,275,275,276,276,276,276,276,276,276,276, + 277,277,277,277,277,277,277,277,277,277,277,277,277,277,277,277,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278,278, + 279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,279,280,280,280,280,280,280,280,280,280,280,280,280,280,280,280,280, + 281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281,281, + 282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282,282, + 283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283,283, + 284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,284,285 }; + + static const uint8_t g_defl_len_extra[256] = { + 0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, + 4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,0 }; + + static const uint8_t g_defl_small_dist_sym[512] = { + 0,1,2,3,4,4,5,5,6,6,6,6,7,7,7,7,8,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,11,11,11,11,11,11, + 11,11,11,11,11,11,11,11,11,11,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,12,13, + 13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,13,14,14,14,14,14,14,14,14,14,14,14,14, + 14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14,14, + 14,14,14,14,14,14,14,14,14,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15, + 15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,16,16,16,16,16,16,16,16,16,16,16,16,16, + 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16, + 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16, + 16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,17,17,17,17,17,17,17,17,17,17,17, + 17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, + 17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17, + 17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17,17 }; + + static const uint32_t g_bitmasks[17] = { 0x0000, 0x0001, 0x0003, 0x0007, 0x000F, 0x001F, 0x003F, 0x007F, 0x00FF, 0x01FF, 0x03FF, 0x07FF, 0x0FFF, 0x1FFF, 0x3FFF, 0x7FFF, 0xFFFF }; + + // Huffman tables generated by fpng_test -t @filelist.txt. Total alpha files : 1440, Total opaque files : 5627. + // Feel free to retrain the encoder on your opaque/alpha PNG files by setting FPNG_TRAIN_HUFFMAN_TABLES and running fpng_test with the -t option. + static const uint8_t g_dyn_huff_3[] = { + 120, 1, 237, 195, 3, 176, 110, 89, 122, 128, 225, 247, 251, 214, 218, 248, 113, 124, 173, 190, 109, 12, 50, 201, 196, 182, 109, 219, 182, 109, 219, 182, + 109, 219, 201, 36, 147, 153, 105, 235, 246, 53, 142, 207, 143, 141, 181, 214, 151, 93, 117, 170, 78, 117, 117, 58, 206, 77, 210, 217, 169, 122 }; + const uint32_t DYN_HUFF_3_BITBUF = 30, DYN_HUFF_3_BITBUF_SIZE = 7; + static const struct { uint8_t m_code_size; uint16_t m_code; } g_dyn_huff_3_codes[288] = { + {2,0},{4,2},{4,10},{5,14},{5,30},{6,25},{6,57},{6,5},{6,37},{7,3},{7,67},{7,35},{7,99},{8,11},{8,139},{8,75},{8,203},{8,43},{8,171},{8,107},{9,135},{9,391},{9,71},{9,327},{9,199},{9,455},{9,39},{9,295},{9,167},{9,423},{9,103},{10,183}, + {9,359},{10,695},{10,439},{10,951},{10,119},{10,631},{10,375},{10,887},{10,247},{10,759},{10,503},{11,975},{11,1999},{11,47},{11,1071},{12,1199},{11,559},{12,3247},{12,687},{11,1583},{12,2735},{12,1711},{12,3759},{12,431},{12,2479},{12,1455},{12,3503},{12,943},{12,2991},{12,1967},{12,4015},{12,111}, + {12,2159},{12,1135},{12,3183},{12,623},{12,2671},{12,1647},{12,3695},{12,367},{12,2415},{12,1391},{12,3439},{12,879},{12,2927},{12,1903},{12,3951},{12,239},{12,2287},{12,1263},{12,3311},{12,751},{12,2799},{12,1775},{12,3823},{12,495},{12,2543},{12,1519},{12,3567},{12,1007},{12,3055},{12,2031},{12,4079},{12,31}, + {12,2079},{12,1055},{12,3103},{12,543},{12,2591},{12,1567},{12,3615},{12,287},{12,2335},{12,1311},{12,3359},{12,799},{12,2847},{12,1823},{12,3871},{12,159},{12,2207},{12,1183},{12,3231},{12,671},{12,2719},{12,1695},{12,3743},{12,415},{12,2463},{12,1439},{12,3487},{12,927},{12,2975},{12,1951},{12,3999},{12,95}, + {12,2143},{12,1119},{12,3167},{12,607},{12,2655},{12,1631},{12,3679},{12,351},{12,2399},{12,1375},{12,3423},{12,863},{12,2911},{12,1887},{12,3935},{12,223},{12,2271},{12,1247},{12,3295},{12,735},{12,2783},{12,1759},{12,3807},{12,479},{12,2527},{12,1503},{12,3551},{12,991},{12,3039},{12,2015},{12,4063},{12,63}, + {12,2111},{12,1087},{12,3135},{12,575},{12,2623},{12,1599},{12,3647},{12,319},{12,2367},{12,1343},{12,3391},{12,831},{12,2879},{12,1855},{12,3903},{12,191},{12,2239},{12,1215},{12,3263},{12,703},{12,2751},{12,1727},{12,3775},{12,447},{12,2495},{12,1471},{12,3519},{12,959},{12,3007},{12,1983},{12,4031},{12,127}, + {12,2175},{12,1151},{12,3199},{12,639},{12,2687},{12,1663},{12,3711},{12,383},{12,2431},{12,1407},{12,3455},{12,895},{12,2943},{11,303},{12,1919},{12,3967},{11,1327},{12,255},{11,815},{11,1839},{11,175},{10,1015},{10,15},{10,527},{10,271},{10,783},{10,143},{10,655},{10,399},{10,911},{10,79},{10,591}, + {9,231},{10,335},{9,487},{9,23},{9,279},{9,151},{9,407},{9,87},{9,343},{9,215},{9,471},{9,55},{8,235},{8,27},{8,155},{8,91},{8,219},{8,59},{8,187},{8,123},{7,19},{7,83},{7,51},{7,115},{6,21},{6,53},{6,13},{6,45},{5,1},{5,17},{5,9},{4,6}, + {12,2303},{6,29},{0,0},{0,0},{8,251},{0,0},{0,0},{8,7},{0,0},{10,847},{0,0},{10,207},{12,1279},{10,719},{12,3327},{12,767},{12,2815},{12,1791},{12,3839},{12,511},{12,2559},{12,1535},{9,311},{12,3583},{12,1023},{12,3071},{10,463},{12,2047},{6,61},{12,4095},{0,0},{0,0} + }; + + static const uint8_t g_dyn_huff_4[] = { + 120, 1, 229, 196, 99, 180, 37, 103, 218, 128, 225, 251, 121, 171, 106, 243, 216, 231, 180, 109, 196, 182, 51, 51, 73, 6, 201, 216, 182, 109, 219, 182, + 17, 140, 98, 219, 102, 219, 60, 125, 172, 205, 170, 122, 159, 111, 213, 143, 179, 214, 94, 189, 58, 153, 104, 166, 103, 190, 247, 199, 117 }; + const uint32_t DYN_HUFF_4_BITBUF = 1, DYN_HUFF_4_BITBUF_SIZE = 2; + static const struct { uint8_t m_code_size; uint16_t m_code; } g_dyn_huff_4_codes[288] = { + {2,0},{4,2},{5,6},{6,30},{6,62},{6,1},{7,41},{7,105},{7,25},{7,89},{7,57},{7,121},{8,117},{8,245},{8,13},{8,141},{8,77},{8,205},{8,45},{8,173},{8,109},{8,237},{8,29},{8,157},{8,93},{8,221},{8,61},{9,83},{9,339},{9,211},{9,467},{9,51}, + {9,307},{9,179},{9,435},{9,115},{9,371},{9,243},{9,499},{9,11},{9,267},{9,139},{9,395},{9,75},{9,331},{9,203},{9,459},{9,43},{9,299},{10,7},{10,519},{10,263},{10,775},{10,135},{10,647},{10,391},{10,903},{10,71},{10,583},{10,327},{10,839},{10,199},{10,711},{10,455}, + {10,967},{10,39},{10,551},{10,295},{10,807},{10,167},{10,679},{10,423},{10,935},{10,103},{10,615},{11,463},{11,1487},{11,975},{10,359},{10,871},{10,231},{11,1999},{11,47},{11,1071},{11,559},{10,743},{10,487},{11,1583},{11,303},{11,1327},{11,815},{11,1839},{11,175},{11,1199},{11,687},{11,1711}, + {11,431},{11,1455},{11,943},{11,1967},{11,111},{11,1135},{11,623},{11,1647},{11,367},{11,1391},{11,879},{11,1903},{11,239},{11,1263},{11,751},{11,1775},{11,495},{11,1519},{11,1007},{11,2031},{11,31},{11,1055},{11,543},{11,1567},{11,287},{11,1311},{11,799},{11,1823},{11,159},{11,1183},{11,671},{11,1695}, + {11,415},{11,1439},{11,927},{11,1951},{11,95},{11,1119},{11,607},{11,1631},{11,351},{11,1375},{11,863},{11,1887},{11,223},{11,1247},{11,735},{11,1759},{11,479},{11,1503},{11,991},{11,2015},{11,63},{11,1087},{11,575},{11,1599},{11,319},{11,1343},{11,831},{11,1855},{11,191},{11,1215},{11,703},{11,1727}, + {11,447},{11,1471},{11,959},{11,1983},{11,127},{11,1151},{11,639},{11,1663},{11,383},{10,999},{10,23},{10,535},{10,279},{11,1407},{11,895},{11,1919},{11,255},{11,1279},{10,791},{10,151},{10,663},{10,407},{10,919},{10,87},{10,599},{10,343},{10,855},{10,215},{10,727},{10,471},{10,983},{10,55}, + {10,567},{10,311},{10,823},{10,183},{10,695},{10,439},{10,951},{10,119},{10,631},{10,375},{10,887},{10,247},{10,759},{10,503},{10,1015},{10,15},{10,527},{10,271},{10,783},{10,143},{10,655},{10,399},{9,171},{9,427},{9,107},{9,363},{9,235},{9,491},{9,27},{9,283},{9,155},{9,411}, + {9,91},{9,347},{9,219},{9,475},{9,59},{9,315},{9,187},{9,443},{8,189},{9,123},{8,125},{8,253},{8,3},{8,131},{8,67},{8,195},{8,35},{8,163},{8,99},{8,227},{8,19},{7,5},{7,69},{7,37},{7,101},{7,21},{7,85},{6,33},{6,17},{6,49},{5,22},{4,10}, + {12,2047},{0,0},{6,9},{0,0},{0,0},{0,0},{8,147},{0,0},{0,0},{7,53},{0,0},{9,379},{0,0},{9,251},{10,911},{10,79},{11,767},{10,591},{10,335},{10,847},{10,207},{10,719},{11,1791},{11,511},{9,507},{11,1535},{11,1023},{12,4095},{5,14},{0,0},{0,0},{0,0} + }; + +#define PUT_BITS(bb, ll) do { uint32_t b = bb, l = ll; assert((l) >= 0 && (l) <= 16); assert((b) < (1ULL << (l))); bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); } while(0) +#define PUT_BITS_CZ(bb, ll) do { uint32_t b = bb, l = ll; assert((l) >= 1 && (l) <= 16); assert((b) < (1ULL << (l))); bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); } while(0) + +#define PUT_BITS_FLUSH do { \ + if ((dst_ofs + 8) > dst_buf_size) \ + return 0; \ + WRITE_LE64(pDst + dst_ofs, bit_buf); \ + uint32_t bits_to_shift = bit_buf_size & ~7; \ + dst_ofs += (bits_to_shift >> 3); \ + assert(bits_to_shift < 64); \ + bit_buf = bit_buf >> bits_to_shift; \ + bit_buf_size -= bits_to_shift; \ +} while(0) + +#define PUT_BITS_FORCE_FLUSH do { \ + while (bit_buf_size > 0) \ + { \ + if ((dst_ofs + 1) > dst_buf_size) \ + return 0; \ + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)bit_buf; \ + dst_ofs++; \ + bit_buf >>= 8; \ + bit_buf_size -= 8; \ + } \ +} while(0) + + enum + { + DEFL_MAX_HUFF_TABLES = 3, + DEFL_MAX_HUFF_SYMBOLS = 288, + DEFL_MAX_HUFF_SYMBOLS_0 = 288, + DEFL_MAX_HUFF_SYMBOLS_1 = 32, + DEFL_MAX_HUFF_SYMBOLS_2 = 19, + DEFL_LZ_DICT_SIZE = 32768, + DEFL_LZ_DICT_SIZE_MASK = DEFL_LZ_DICT_SIZE - 1, + DEFL_MIN_MATCH_LEN = 3, + DEFL_MAX_MATCH_LEN = 258 + }; + +#if FPNG_TRAIN_HUFFMAN_TABLES + uint64_t g_huff_counts[HUFF_COUNTS_SIZE]; +#endif + + struct defl_huff + { + uint16_t m_huff_count[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS]; + uint16_t m_huff_codes[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS]; + uint8_t m_huff_code_sizes[DEFL_MAX_HUFF_TABLES][DEFL_MAX_HUFF_SYMBOLS]; + }; + + struct defl_sym_freq + { + uint16_t m_key; + uint16_t m_sym_index; + }; + +#define DEFL_CLEAR_OBJ(obj) memset(&(obj), 0, sizeof(obj)) + + static defl_sym_freq* defl_radix_sort_syms(uint32_t num_syms, defl_sym_freq* pSyms0, defl_sym_freq* pSyms1) + { + uint32_t total_passes = 2, pass_shift, pass, i, hist[256 * 2]; defl_sym_freq* pCur_syms = pSyms0, * pNew_syms = pSyms1; DEFL_CLEAR_OBJ(hist); + for (i = 0; i < num_syms; i++) { uint32_t freq = pSyms0[i].m_key; hist[freq & 0xFF]++; hist[256 + ((freq >> 8) & 0xFF)]++; } + while ((total_passes > 1) && (num_syms == hist[(total_passes - 1) * 256])) total_passes--; + for (pass_shift = 0, pass = 0; pass < total_passes; pass++, pass_shift += 8) + { + const uint32_t* pHist = &hist[pass << 8]; + uint32_t offsets[256], cur_ofs = 0; + for (i = 0; i < 256; i++) { offsets[i] = cur_ofs; cur_ofs += pHist[i]; } + for (i = 0; i < num_syms; i++) pNew_syms[offsets[(pCur_syms[i].m_key >> pass_shift) & 0xFF]++] = pCur_syms[i]; + { defl_sym_freq* t = pCur_syms; pCur_syms = pNew_syms; pNew_syms = t; } + } + return pCur_syms; + } + + // defl_calculate_minimum_redundancy() originally written by: Alistair Moffat, alistair@cs.mu.oz.au, Jyrki Katajainen, jyrki@diku.dk, November 1996. + static void defl_calculate_minimum_redundancy(defl_sym_freq* A, int n) + { + int root, leaf, next, avbl, used, dpth; + if (n == 0) return; else if (n == 1) { A[0].m_key = 1; return; } + A[0].m_key += A[1].m_key; root = 0; leaf = 2; + for (next = 1; next < n - 1; next++) + { + if (leaf >= n || A[root].m_key < A[leaf].m_key) { A[next].m_key = A[root].m_key; A[root++].m_key = (uint16_t)next; } + else A[next].m_key = A[leaf++].m_key; + if (leaf >= n || (root < next && A[root].m_key < A[leaf].m_key)) { A[next].m_key = (uint16_t)(A[next].m_key + A[root].m_key); A[root++].m_key = (uint16_t)next; } + else A[next].m_key = (uint16_t)(A[next].m_key + A[leaf++].m_key); + } + A[n - 2].m_key = 0; for (next = n - 3; next >= 0; next--) A[next].m_key = A[A[next].m_key].m_key + 1; + avbl = 1; used = dpth = 0; root = n - 2; next = n - 1; + while (avbl > 0) + { + while (root >= 0 && (int)A[root].m_key == dpth) { used++; root--; } + while (avbl > used) { A[next--].m_key = (uint16_t)(dpth); avbl--; } + avbl = 2 * used; dpth++; used = 0; + } + } + + // Limits canonical Huffman code table's max code size. + enum { DEFL_MAX_SUPPORTED_HUFF_CODESIZE = 32 }; + static void defl_huffman_enforce_max_code_size(int* pNum_codes, int code_list_len, int max_code_size) + { + int i; uint32_t total = 0; if (code_list_len <= 1) return; + for (i = max_code_size + 1; i <= DEFL_MAX_SUPPORTED_HUFF_CODESIZE; i++) pNum_codes[max_code_size] += pNum_codes[i]; + for (i = max_code_size; i > 0; i--) total += (((uint32_t)pNum_codes[i]) << (max_code_size - i)); + while (total != (1UL << max_code_size)) + { + pNum_codes[max_code_size]--; + for (i = max_code_size - 1; i > 0; i--) if (pNum_codes[i]) { pNum_codes[i]--; pNum_codes[i + 1] += 2; break; } + total--; + } + } + + static void defl_optimize_huffman_table(defl_huff* d, int table_num, int table_len, int code_size_limit, int static_table) + { + int i, j, l, num_codes[1 + DEFL_MAX_SUPPORTED_HUFF_CODESIZE]; uint32_t next_code[DEFL_MAX_SUPPORTED_HUFF_CODESIZE + 1]; DEFL_CLEAR_OBJ(num_codes); + if (static_table) + { + for (i = 0; i < table_len; i++) num_codes[d->m_huff_code_sizes[table_num][i]]++; + } + else + { + defl_sym_freq syms0[DEFL_MAX_HUFF_SYMBOLS], syms1[DEFL_MAX_HUFF_SYMBOLS], * pSyms; + int num_used_syms = 0; + const uint16_t* pSym_count = &d->m_huff_count[table_num][0]; + for (i = 0; i < table_len; i++) if (pSym_count[i]) { syms0[num_used_syms].m_key = (uint16_t)pSym_count[i]; syms0[num_used_syms++].m_sym_index = (uint16_t)i; } + + pSyms = defl_radix_sort_syms(num_used_syms, syms0, syms1); defl_calculate_minimum_redundancy(pSyms, num_used_syms); + + for (i = 0; i < num_used_syms; i++) num_codes[pSyms[i].m_key]++; + + defl_huffman_enforce_max_code_size(num_codes, num_used_syms, code_size_limit); + + DEFL_CLEAR_OBJ(d->m_huff_code_sizes[table_num]); DEFL_CLEAR_OBJ(d->m_huff_codes[table_num]); + for (i = 1, j = num_used_syms; i <= code_size_limit; i++) + for (l = num_codes[i]; l > 0; l--) d->m_huff_code_sizes[table_num][pSyms[--j].m_sym_index] = (uint8_t)(i); + } + + next_code[1] = 0; for (j = 0, i = 2; i <= code_size_limit; i++) next_code[i] = j = ((j + num_codes[i - 1]) << 1); + + for (i = 0; i < table_len; i++) + { + uint32_t rev_code = 0, code, code_size; if ((code_size = d->m_huff_code_sizes[table_num][i]) == 0) continue; + code = next_code[code_size]++; for (l = code_size; l > 0; l--, code >>= 1) rev_code = (rev_code << 1) | (code & 1); + d->m_huff_codes[table_num][i] = (uint16_t)rev_code; + } + } + +#define DEFL_RLE_PREV_CODE_SIZE() { if (rle_repeat_count) { \ + if (rle_repeat_count < 3) { \ + d->m_huff_count[2][prev_code_size] = (uint16_t)(d->m_huff_count[2][prev_code_size] + rle_repeat_count); \ + while (rle_repeat_count--) packed_code_sizes[num_packed_code_sizes++] = prev_code_size; \ + } else { \ + d->m_huff_count[2][16] = (uint16_t)(d->m_huff_count[2][16] + 1); packed_code_sizes[num_packed_code_sizes++] = 16; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_repeat_count - 3); \ +} rle_repeat_count = 0; } } + +#define DEFL_RLE_ZERO_CODE_SIZE() { if (rle_z_count) { \ + if (rle_z_count < 3) { \ + d->m_huff_count[2][0] = (uint16_t)(d->m_huff_count[2][0] + rle_z_count); while (rle_z_count--) packed_code_sizes[num_packed_code_sizes++] = 0; \ + } else if (rle_z_count <= 10) { \ + d->m_huff_count[2][17] = (uint16_t)(d->m_huff_count[2][17] + 1); packed_code_sizes[num_packed_code_sizes++] = 17; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_z_count - 3); \ + } else { \ + d->m_huff_count[2][18] = (uint16_t)(d->m_huff_count[2][18] + 1); packed_code_sizes[num_packed_code_sizes++] = 18; packed_code_sizes[num_packed_code_sizes++] = (uint8_t)(rle_z_count - 11); \ +} rle_z_count = 0; } } + + static uint8_t g_defl_packed_code_size_syms_swizzle[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + +#define DEFL_DYN_PUT_BITS(bb, ll) \ +do { \ + uint32_t b = (bb), l = (ll); \ + assert((l) >= 1 && (l) <= 16); assert((b) < (1ULL << (l))); \ + bit_buf |= (((uint64_t)(b)) << bit_buf_size); bit_buf_size += (l); assert(bit_buf_size <= 64); \ + while (bit_buf_size >= 8) \ + { \ + if ((dst_ofs + 1) > dst_buf_size) \ + return false; \ + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)bit_buf; \ + dst_ofs++; \ + bit_buf >>= 8; \ + bit_buf_size -= 8; \ + } \ +} while(0) + + static bool defl_start_dynamic_block(defl_huff* d, uint8_t* pDst, uint32_t& dst_ofs, uint32_t dst_buf_size, uint64_t& bit_buf, int& bit_buf_size) + { + int num_lit_codes, num_dist_codes, num_bit_lengths; uint32_t i, total_code_sizes_to_pack, num_packed_code_sizes, rle_z_count, rle_repeat_count, packed_code_sizes_index; + uint8_t code_sizes_to_pack[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1], packed_code_sizes[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1], prev_code_size = 0xFF; + +#if FPNG_TRAIN_HUFFMAN_TABLES + assert(HUFF_COUNTS_SIZE == DEFL_MAX_HUFF_SYMBOLS_0); + for (uint32_t i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++) + g_huff_counts[i] += d->m_huff_count[0][i]; +#endif + + d->m_huff_count[0][256] = 1; + + defl_optimize_huffman_table(d, 0, DEFL_MAX_HUFF_SYMBOLS_0, 12, FPNG_FALSE); + defl_optimize_huffman_table(d, 1, DEFL_MAX_HUFF_SYMBOLS_1, 12, FPNG_FALSE); + + for (num_lit_codes = 286; num_lit_codes > 257; num_lit_codes--) if (d->m_huff_code_sizes[0][num_lit_codes - 1]) break; + for (num_dist_codes = 30; num_dist_codes > 1; num_dist_codes--) if (d->m_huff_code_sizes[1][num_dist_codes - 1]) break; + + memcpy(code_sizes_to_pack, &d->m_huff_code_sizes[0][0], num_lit_codes); + memcpy(code_sizes_to_pack + num_lit_codes, &d->m_huff_code_sizes[1][0], num_dist_codes); + total_code_sizes_to_pack = num_lit_codes + num_dist_codes; num_packed_code_sizes = 0; rle_z_count = 0; rle_repeat_count = 0; + + memset(&d->m_huff_count[2][0], 0, sizeof(d->m_huff_count[2][0]) * DEFL_MAX_HUFF_SYMBOLS_2); + for (i = 0; i < total_code_sizes_to_pack; i++) + { + uint8_t code_size = code_sizes_to_pack[i]; + if (!code_size) + { + DEFL_RLE_PREV_CODE_SIZE(); + if (++rle_z_count == 138) { DEFL_RLE_ZERO_CODE_SIZE(); } + } + else + { + DEFL_RLE_ZERO_CODE_SIZE(); + if (code_size != prev_code_size) + { + DEFL_RLE_PREV_CODE_SIZE(); + d->m_huff_count[2][code_size] = (uint16_t)(d->m_huff_count[2][code_size] + 1); packed_code_sizes[num_packed_code_sizes++] = code_size; + } + else if (++rle_repeat_count == 6) + { + DEFL_RLE_PREV_CODE_SIZE(); + } + } + prev_code_size = code_size; + } + if (rle_repeat_count) { DEFL_RLE_PREV_CODE_SIZE(); } + else { DEFL_RLE_ZERO_CODE_SIZE(); } + + defl_optimize_huffman_table(d, 2, DEFL_MAX_HUFF_SYMBOLS_2, 7, FPNG_FALSE); + + // max of 2+5+5+4+18*3+(288+32)*7=2310 bits + DEFL_DYN_PUT_BITS(2, 2); + + DEFL_DYN_PUT_BITS(num_lit_codes - 257, 5); + DEFL_DYN_PUT_BITS(num_dist_codes - 1, 5); + + for (num_bit_lengths = 18; num_bit_lengths >= 0; num_bit_lengths--) if (d->m_huff_code_sizes[2][g_defl_packed_code_size_syms_swizzle[num_bit_lengths]]) break; + num_bit_lengths = maximum(4, (num_bit_lengths + 1)); DEFL_DYN_PUT_BITS(num_bit_lengths - 4, 4); + for (i = 0; (int)i < num_bit_lengths; i++) DEFL_DYN_PUT_BITS(d->m_huff_code_sizes[2][g_defl_packed_code_size_syms_swizzle[i]], 3); + + for (packed_code_sizes_index = 0; packed_code_sizes_index < num_packed_code_sizes; ) + { + uint32_t code = packed_code_sizes[packed_code_sizes_index++]; assert(code < DEFL_MAX_HUFF_SYMBOLS_2); + DEFL_DYN_PUT_BITS(d->m_huff_codes[2][code], d->m_huff_code_sizes[2][code]); + if (code >= 16) DEFL_DYN_PUT_BITS(packed_code_sizes[packed_code_sizes_index++], "\02\03\07"[code - 16]); + } + + return true; + } + + static uint32_t write_raw_block(const uint8_t* pSrc, uint32_t src_len, uint8_t* pDst, uint32_t dst_buf_size) + { + if (dst_buf_size < 2) + return 0; + + pDst[0] = 0x78; + pDst[1] = 0x01; + + uint32_t dst_ofs = 2; + + uint32_t src_ofs = 0; + while (src_ofs < src_len) + { + const uint32_t src_remaining = src_len - src_ofs; + const uint32_t block_size = minimum(UINT16_MAX, src_remaining); + const bool final_block = (block_size == src_remaining); + + if ((dst_ofs + 5 + block_size) > dst_buf_size) + return 0; + + pDst[dst_ofs + 0] = final_block ? 1 : 0; + + pDst[dst_ofs + 1] = block_size & 0xFF; + pDst[dst_ofs + 2] = (block_size >> 8) & 0xFF; + + pDst[dst_ofs + 3] = (~block_size) & 0xFF; + pDst[dst_ofs + 4] = ((~block_size) >> 8) & 0xFF; + + memcpy(pDst + dst_ofs + 5, pSrc + src_ofs, block_size); + + src_ofs += block_size; + dst_ofs += 5 + block_size; + } + + uint32_t src_adler32 = fpng_adler32(pSrc, src_len, FPNG_ADLER32_INIT); + + for (uint32_t i = 0; i < 4; i++) + { + if (dst_ofs + 1 > dst_buf_size) + return 0; + + pDst[dst_ofs] = (uint8_t)(src_adler32 >> 24); + dst_ofs++; + + src_adler32 <<= 8; + } + + return dst_ofs; + } + + static void adjust_freq32(uint32_t num_freq, uint32_t* pFreq, uint16_t* pFreq16) + { + uint32_t total_freq = 0; + for (uint32_t i = 0; i < num_freq; i++) + total_freq += pFreq[i]; + + if (!total_freq) + { + memset(pFreq16, 0, num_freq * sizeof(uint16_t)); + return; + } + + uint32_t total_freq16 = 0; + for (uint32_t i = 0; i < num_freq; i++) + { + uint64_t f = pFreq[i]; + if (!f) + { + pFreq16[i] = 0; + continue; + } + + pFreq16[i] = (uint16_t)maximum(1, (uint32_t)((f * UINT16_MAX) / total_freq)); + + total_freq16 += pFreq16[i]; + } + + while (total_freq16 > UINT16_MAX) + { + total_freq16 = 0; + for (uint32_t i = 0; i < num_freq; i++) + { + if (pFreq[i]) + { + pFreq[i] = maximum(1, pFreq[i] >> 1); + total_freq16 += pFreq[i]; + } + } + } + } + +#if FPNG_TRAIN_HUFFMAN_TABLES + bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector& prefix, uint64_t& bit_buf, int &bit_buf_size, uint32_t* pCodes, uint8_t* pCodesizes) + { + assert((num_chans == 3) || (num_chans == 4)); + assert(HUFF_COUNTS_SIZE == DEFL_MAX_HUFF_SYMBOLS_0); // must be equal + + defl_huff dh; + memset(&dh, 0, sizeof(dh)); + + uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0]; + + uint32_t shift_len = 0; + for (; ; ) + { + uint32_t i; + for (i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++) + { + uint64_t f = pFreq[i]; + if (f) + f = maximum(1U, f >> shift_len); + + if (f > UINT32_MAX) + break; + + lit_freq[i] = (uint32_t)pFreq[i]; + } + + if (i == DEFL_MAX_HUFF_SYMBOLS_0) + break; + + shift_len++; + } + + // Ensure all valid Deflate literal/EOB/length syms are non-zero, so anything can be coded. + for (uint32_t i = 0; i <= 256; i++) + { + if (!lit_freq[i]) + lit_freq[i] = 1; + } + + for (uint32_t len = num_chans; len <= DEFL_MAX_MATCH_LEN; len += num_chans) + { + uint32_t sym = g_defl_len_sym[len - 3]; + if (!lit_freq[sym]) + lit_freq[sym] = 1; + } + + adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]); + + const uint32_t dist_sym = g_defl_small_dist_sym[num_chans - 1]; + dh.m_huff_count[1][dist_sym] = 1; + dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder + + prefix.resize(4096); + uint8_t* pDst = prefix.data(); + uint32_t dst_buf_size = (uint32_t)prefix.size(); + + uint32_t dst_ofs = 0; + + // zlib header + PUT_BITS(0x78, 8); + PUT_BITS(0x01, 8); + + // write BFINAL bit + PUT_BITS(1, 1); + + if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size)) + return false; + + prefix.resize(dst_ofs); + + for (uint32_t i = 0; i < DEFL_MAX_HUFF_SYMBOLS_0; i++) + { + pCodes[i] = dh.m_huff_codes[0][i]; + pCodesizes[i] = dh.m_huff_code_sizes[0][i]; + } + + return true; + } +#endif + + static uint32_t pixel_deflate_dyn_3_rle( + const uint8_t* pImg, uint32_t w, uint32_t h, + uint8_t* pDst, uint32_t dst_buf_size) + { + const uint32_t bpl = 1 + w * 3; + + uint64_t bit_buf = 0; + int bit_buf_size = 0; + + uint32_t dst_ofs = 0; + + // zlib header + PUT_BITS(0x78, 8); + PUT_BITS(0x01, 8); + + // write BFINAL bit + PUT_BITS(1, 1); + + std::vector codes((w + 1) * h); + uint32_t* pDst_codes = codes.data(); + + uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0]; + memset(lit_freq, 0, sizeof(lit_freq)); + + const uint8_t* pSrc = pImg; + uint32_t src_ofs = 0; + + uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT); + + const uint32_t dist_sym = g_defl_small_dist_sym[3 - 1]; + + for (uint32_t y = 0; y < h; y++) + { + const uint32_t end_src_ofs = src_ofs + bpl; + + const uint32_t filter_lit = pSrc[src_ofs++]; + *pDst_codes++ = 1 | (filter_lit << 8); + lit_freq[filter_lit]++; + + uint32_t prev_lits; + + { + uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs); + + *pDst_codes++ = lits << 8; + + lit_freq[lits & 0xFF]++; + lit_freq[(lits >> 8) & 0xFF]++; + lit_freq[lits >> 16]++; + + src_ofs += 3; + + prev_lits = lits; + } + + while (src_ofs < end_src_ofs) + { + uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs); + + if (lits == prev_lits) + { + uint32_t match_len = 3; + uint32_t max_match_len = minimum(255, (int)(end_src_ofs - src_ofs)); + + while (match_len < max_match_len) + { + if (READ_RGB_PIXEL(pSrc + src_ofs + match_len) != lits) + break; + match_len += 3; + } + + *pDst_codes++ = match_len - 1; + + uint32_t adj_match_len = match_len - 3; + + lit_freq[g_defl_len_sym[adj_match_len]]++; + + src_ofs += match_len; + } + else + { + *pDst_codes++ = lits << 8; + + lit_freq[lits & 0xFF]++; + lit_freq[(lits >> 8) & 0xFF]++; + lit_freq[lits >> 16]++; + + prev_lits = lits; + + src_ofs += 3; + } + + } // while (src_ofs < end_src_ofs) + + } // y + + assert(src_ofs == h * bpl); + const uint32_t total_codes = (uint32_t)(pDst_codes - codes.data()); + assert(total_codes <= codes.size()); + + defl_huff dh; + + lit_freq[256] = 1; + + adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]); + + memset(&dh.m_huff_count[1][0], 0, sizeof(dh.m_huff_count[1][0]) * DEFL_MAX_HUFF_SYMBOLS_1); + dh.m_huff_count[1][dist_sym] = 1; + dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder + + if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size)) + return 0; + + assert(bit_buf_size <= 7); + assert(dh.m_huff_codes[1][dist_sym] == 0 && dh.m_huff_code_sizes[1][dist_sym] == 1); + + for (uint32_t i = 0; i < total_codes; i++) + { + uint32_t c = codes[i]; + + uint32_t c_type = c & 0xFF; + if (c_type == 0) + { + uint32_t lits = c >> 8; + + PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]); + lits >>= 8; + + PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]); + lits >>= 8; + + PUT_BITS_CZ(dh.m_huff_codes[0][lits], dh.m_huff_code_sizes[0][lits]); + } + else if (c_type == 1) + { + uint32_t lit = c >> 8; + PUT_BITS_CZ(dh.m_huff_codes[0][lit], dh.m_huff_code_sizes[0][lit]); + } + else + { + uint32_t match_len = c_type + 1; + + uint32_t adj_match_len = match_len - 3; + + PUT_BITS_CZ(dh.m_huff_codes[0][g_defl_len_sym[adj_match_len]], dh.m_huff_code_sizes[0][g_defl_len_sym[adj_match_len]]); + PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0 + + // no need to write the distance code, it's always 0 + //PUT_BITS_CZ(dh.m_huff_codes[1][dist_sym], dh.m_huff_code_sizes[1][dist_sym]); + } + + // up to 55 bits + PUT_BITS_FLUSH; + } + + PUT_BITS_CZ(dh.m_huff_codes[0][256], dh.m_huff_code_sizes[0][256]); + + PUT_BITS_FORCE_FLUSH; + + // Write zlib adler32 + for (uint32_t i = 0; i < 4; i++) + { + if ((dst_ofs + 1) > dst_buf_size) + return 0; + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24); + dst_ofs++; + + src_adler32 <<= 8; + } + + return dst_ofs; + } + + static uint32_t pixel_deflate_dyn_3_rle_one_pass( + const uint8_t* pImg, uint32_t w, uint32_t h, + uint8_t* pDst, uint32_t dst_buf_size) + { + const uint32_t bpl = 1 + w * 3; + + if (dst_buf_size < sizeof(g_dyn_huff_3)) + return false; + memcpy(pDst, g_dyn_huff_3, sizeof(g_dyn_huff_3)); + uint32_t dst_ofs = sizeof(g_dyn_huff_3); + + uint64_t bit_buf = DYN_HUFF_3_BITBUF; + int bit_buf_size = DYN_HUFF_3_BITBUF_SIZE; + + const uint8_t* pSrc = pImg; + uint32_t src_ofs = 0; + + uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT); + + for (uint32_t y = 0; y < h; y++) + { + const uint32_t end_src_ofs = src_ofs + bpl; + + const uint32_t filter_lit = pSrc[src_ofs++]; + PUT_BITS_CZ(g_dyn_huff_3_codes[filter_lit].m_code, g_dyn_huff_3_codes[filter_lit].m_code_size); + + uint32_t prev_lits; + + { + uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs); + + PUT_BITS_CZ(g_dyn_huff_3_codes[lits & 0xFF].m_code, g_dyn_huff_3_codes[lits & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 16)].m_code, g_dyn_huff_3_codes[(lits >> 16)].m_code_size); + + src_ofs += 3; + + prev_lits = lits; + } + + PUT_BITS_FLUSH; + + while (src_ofs < end_src_ofs) + { + uint32_t lits = READ_RGB_PIXEL(pSrc + src_ofs); + + if (lits == prev_lits) + { + uint32_t match_len = 3; + uint32_t max_match_len = minimum(255, (int)(end_src_ofs - src_ofs)); + + while (match_len < max_match_len) + { + if (READ_RGB_PIXEL(pSrc + src_ofs + match_len) != lits) + break; + match_len += 3; + } + + uint32_t adj_match_len = match_len - 3; + + PUT_BITS_CZ(g_dyn_huff_3_codes[g_defl_len_sym[adj_match_len]].m_code, g_dyn_huff_3_codes[g_defl_len_sym[adj_match_len]].m_code_size); + PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0 + + src_ofs += match_len; + } + else + { + PUT_BITS_CZ(g_dyn_huff_3_codes[lits & 0xFF].m_code, g_dyn_huff_3_codes[lits & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_3_codes[(lits >> 8) & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_3_codes[(lits >> 16)].m_code, g_dyn_huff_3_codes[(lits >> 16)].m_code_size); + + prev_lits = lits; + + src_ofs += 3; + } + + PUT_BITS_FLUSH; + + } // while (src_ofs < end_src_ofs) + + } // y + + assert(src_ofs == h * bpl); + + assert(bit_buf_size <= 7); + + PUT_BITS_CZ(g_dyn_huff_3_codes[256].m_code, g_dyn_huff_3_codes[256].m_code_size); + + PUT_BITS_FORCE_FLUSH; + + // Write zlib adler32 + for (uint32_t i = 0; i < 4; i++) + { + if ((dst_ofs + 1) > dst_buf_size) + return 0; + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24); + dst_ofs++; + + src_adler32 <<= 8; + } + + return dst_ofs; + } + + static uint32_t pixel_deflate_dyn_4_rle( + const uint8_t* pImg, uint32_t w, uint32_t h, + uint8_t* pDst, uint32_t dst_buf_size) + { + const uint32_t bpl = 1 + w * 4; + + uint64_t bit_buf = 0; + int bit_buf_size = 0; + + uint32_t dst_ofs = 0; + + // zlib header + PUT_BITS(0x78, 8); + PUT_BITS(0x01, 8); + + // write BFINAL bit + PUT_BITS(1, 1); + + std::vector codes; + codes.resize((w + 1) * h); + uint64_t* pDst_codes = codes.data(); + + uint32_t lit_freq[DEFL_MAX_HUFF_SYMBOLS_0]; + memset(lit_freq, 0, sizeof(lit_freq)); + + const uint8_t* pSrc = pImg; + uint32_t src_ofs = 0; + + uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT); + + const uint32_t dist_sym = g_defl_small_dist_sym[4 - 1]; + + for (uint32_t y = 0; y < h; y++) + { + const uint32_t end_src_ofs = src_ofs + bpl; + + const uint32_t filter_lit = pSrc[src_ofs++]; + *pDst_codes++ = 1 | (filter_lit << 8); + lit_freq[filter_lit]++; + + uint32_t prev_lits; + { + uint32_t lits = READ_LE32(pSrc + src_ofs); + + *pDst_codes++ = (uint64_t)lits << 8; + + lit_freq[lits & 0xFF]++; + lit_freq[(lits >> 8) & 0xFF]++; + lit_freq[(lits >> 16) & 0xFF]++; + lit_freq[lits >> 24]++; + + src_ofs += 4; + + prev_lits = lits; + } + + while (src_ofs < end_src_ofs) + { + uint32_t lits = READ_LE32(pSrc + src_ofs); + + if (lits == prev_lits) + { + uint32_t match_len = 4; + uint32_t max_match_len = minimum(252, (int)(end_src_ofs - src_ofs)); + + while (match_len < max_match_len) + { + if (READ_LE32(pSrc + src_ofs + match_len) != lits) + break; + match_len += 4; + } + + *pDst_codes++ = match_len - 1; + + uint32_t adj_match_len = match_len - 3; + + lit_freq[g_defl_len_sym[adj_match_len]]++; + + src_ofs += match_len; + } + else + { + *pDst_codes++ = (uint64_t)lits << 8; + + lit_freq[lits & 0xFF]++; + lit_freq[(lits >> 8) & 0xFF]++; + lit_freq[(lits >> 16) & 0xFF]++; + lit_freq[lits >> 24]++; + + prev_lits = lits; + + src_ofs += 4; + } + + } // while (src_ofs < end_src_ofs) + + } // y + + assert(src_ofs == h * bpl); + const uint32_t total_codes = (uint32_t)(pDst_codes - codes.data()); + assert(total_codes <= codes.size()); + + defl_huff dh; + + lit_freq[256] = 1; + + adjust_freq32(DEFL_MAX_HUFF_SYMBOLS_0, lit_freq, &dh.m_huff_count[0][0]); + + memset(&dh.m_huff_count[1][0], 0, sizeof(dh.m_huff_count[1][0]) * DEFL_MAX_HUFF_SYMBOLS_1); + dh.m_huff_count[1][dist_sym] = 1; + dh.m_huff_count[1][dist_sym + 1] = 1; // to workaround a bug in wuffs decoder + + if (!defl_start_dynamic_block(&dh, pDst, dst_ofs, dst_buf_size, bit_buf, bit_buf_size)) + return 0; + + assert(bit_buf_size <= 7); + assert(dh.m_huff_codes[1][dist_sym] == 0 && dh.m_huff_code_sizes[1][dist_sym] == 1); + + for (uint32_t i = 0; i < total_codes; i++) + { + uint64_t c = codes[i]; + + uint32_t c_type = (uint32_t)(c & 0xFF); + if (c_type == 0) + { + uint32_t lits = (uint32_t)(c >> 8); + + PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]); + lits >>= 8; + + PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]); + lits >>= 8; + + PUT_BITS_CZ(dh.m_huff_codes[0][lits & 0xFF], dh.m_huff_code_sizes[0][lits & 0xFF]); + lits >>= 8; + + if (bit_buf_size >= 49) + { + PUT_BITS_FLUSH; + } + + PUT_BITS_CZ(dh.m_huff_codes[0][lits], dh.m_huff_code_sizes[0][lits]); + } + else if (c_type == 1) + { + uint32_t lit = (uint32_t)(c >> 8); + PUT_BITS_CZ(dh.m_huff_codes[0][lit], dh.m_huff_code_sizes[0][lit]); + } + else + { + uint32_t match_len = c_type + 1; + + uint32_t adj_match_len = match_len - 3; + + PUT_BITS_CZ(dh.m_huff_codes[0][g_defl_len_sym[adj_match_len]], dh.m_huff_code_sizes[0][g_defl_len_sym[adj_match_len]]); + PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], g_defl_len_extra[adj_match_len] + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0 + + // no need to write the distance code, it's always 0 + } + + // up to 55 bits + PUT_BITS_FLUSH; + } + + PUT_BITS_CZ(dh.m_huff_codes[0][256], dh.m_huff_code_sizes[0][256]); + + PUT_BITS_FORCE_FLUSH; + + // Write zlib adler32 + for (uint32_t i = 0; i < 4; i++) + { + if ((dst_ofs + 1) > dst_buf_size) + return 0; + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24); + dst_ofs++; + + src_adler32 <<= 8; + } + + return dst_ofs; + } + + static uint32_t pixel_deflate_dyn_4_rle_one_pass( + const uint8_t* pImg, uint32_t w, uint32_t h, + uint8_t* pDst, uint32_t dst_buf_size) + { + const uint32_t bpl = 1 + w * 4; + + if (dst_buf_size < sizeof(g_dyn_huff_4)) + return false; + memcpy(pDst, g_dyn_huff_4, sizeof(g_dyn_huff_4)); + uint32_t dst_ofs = sizeof(g_dyn_huff_4); + + uint64_t bit_buf = DYN_HUFF_4_BITBUF; + int bit_buf_size = DYN_HUFF_4_BITBUF_SIZE; + + const uint8_t* pSrc = pImg; + uint32_t src_ofs = 0; + + uint32_t src_adler32 = fpng_adler32(pImg, bpl * h, FPNG_ADLER32_INIT); + + for (uint32_t y = 0; y < h; y++) + { + const uint32_t end_src_ofs = src_ofs + bpl; + + const uint32_t filter_lit = pSrc[src_ofs++]; + PUT_BITS_CZ(g_dyn_huff_4_codes[filter_lit].m_code, g_dyn_huff_4_codes[filter_lit].m_code_size); + + PUT_BITS_FLUSH; + + uint32_t prev_lits; + { + uint32_t lits = READ_LE32(pSrc + src_ofs); + + PUT_BITS_CZ(g_dyn_huff_4_codes[lits & 0xFF].m_code, g_dyn_huff_4_codes[lits & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size); + + if (bit_buf_size >= 49) + { + PUT_BITS_FLUSH; + } + + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 24)].m_code, g_dyn_huff_4_codes[(lits >> 24)].m_code_size); + + src_ofs += 4; + + prev_lits = lits; + } + + PUT_BITS_FLUSH; + + while (src_ofs < end_src_ofs) + { + uint32_t lits = READ_LE32(pSrc + src_ofs); + + if (lits == prev_lits) + { + uint32_t match_len = 4; + uint32_t max_match_len = minimum(252, (int)(end_src_ofs - src_ofs)); + + while (match_len < max_match_len) + { + if (READ_LE32(pSrc + src_ofs + match_len) != lits) + break; + match_len += 4; + } + + uint32_t adj_match_len = match_len - 3; + + const uint32_t match_code_bits = g_dyn_huff_4_codes[g_defl_len_sym[adj_match_len]].m_code_size; + const uint32_t len_extra_bits = g_defl_len_extra[adj_match_len]; + + if (match_len == 4) + { + // This check is optional - see if just encoding 4 literals would be cheaper than using a short match. + uint32_t lit_bits = g_dyn_huff_4_codes[lits & 0xFF].m_code_size + g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size + + g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size + g_dyn_huff_4_codes[(lits >> 24)].m_code_size; + + if ((match_code_bits + len_extra_bits + 1) > lit_bits) + goto do_literals; + } + + PUT_BITS_CZ(g_dyn_huff_4_codes[g_defl_len_sym[adj_match_len]].m_code, match_code_bits); + PUT_BITS(adj_match_len & g_bitmasks[g_defl_len_extra[adj_match_len]], len_extra_bits + 1); // up to 6 bits, +1 for the match distance Huff code which is always 0 + + src_ofs += match_len; + } + else + { +do_literals: + PUT_BITS_CZ(g_dyn_huff_4_codes[lits & 0xFF].m_code, g_dyn_huff_4_codes[lits & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 8) & 0xFF].m_code_size); + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code, g_dyn_huff_4_codes[(lits >> 16) & 0xFF].m_code_size); + + if (bit_buf_size >= 49) + { + PUT_BITS_FLUSH; + } + + PUT_BITS_CZ(g_dyn_huff_4_codes[(lits >> 24)].m_code, g_dyn_huff_4_codes[(lits >> 24)].m_code_size); + + src_ofs += 4; + + prev_lits = lits; + } + + PUT_BITS_FLUSH; + + } // while (src_ofs < end_src_ofs) + + } // y + + assert(src_ofs == h * bpl); + + assert(bit_buf_size <= 7); + + PUT_BITS_CZ(g_dyn_huff_4_codes[256].m_code, g_dyn_huff_4_codes[256].m_code_size); + + PUT_BITS_FORCE_FLUSH; + + // Write zlib adler32 + for (uint32_t i = 0; i < 4; i++) + { + if ((dst_ofs + 1) > dst_buf_size) + return 0; + *(uint8_t*)(pDst + dst_ofs) = (uint8_t)(src_adler32 >> 24); + dst_ofs++; + + src_adler32 <<= 8; + } + + return dst_ofs; + } + + static void vector_append(std::vector& buf, const void* pData, size_t len) + { + if (len) + { + size_t l = buf.size(); + buf.resize(l + len); + memcpy(buf.data() + l, pData, len); + } + } + + static void apply_filter(uint32_t filter, int w, int h, uint32_t num_chans, uint32_t bpl, const uint8_t* pSrc, const uint8_t* pPrev_src, uint8_t* pDst) + { + (void)h; + + switch (filter) + { + case 0: + { + *pDst++ = 0; + + memcpy(pDst, pSrc, bpl); + break; + } + case 2: + { + assert(pPrev_src); + + // Previous scanline + *pDst++ = 2; + +#if FPNG_X86_OR_X64_CPU && !FPNG_NO_SSE + if (g_cpu_info.can_use_sse41()) + { + uint32_t bytes_to_process = w * num_chans, ofs = 0; + for (; bytes_to_process >= 16; bytes_to_process -= 16, ofs += 16) + _mm_storeu_si128((__m128i*)(pDst + ofs), _mm_sub_epi8(_mm_loadu_si128((const __m128i*)(pSrc + ofs)), _mm_loadu_si128((const __m128i*)(pPrev_src + ofs)))); + + for (; bytes_to_process; bytes_to_process--, ofs++) + pDst[ofs] = (uint8_t)(pSrc[ofs] - pPrev_src[ofs]); + } + else +#endif + { + if (num_chans == 3) + { + for (uint32_t x = 0; x < (uint32_t)w; x++) + { + pDst[0] = (uint8_t)(pSrc[0] - pPrev_src[0]); + pDst[1] = (uint8_t)(pSrc[1] - pPrev_src[1]); + pDst[2] = (uint8_t)(pSrc[2] - pPrev_src[2]); + + pSrc += 3; + pPrev_src += 3; + pDst += 3; + } + } + else + { + for (uint32_t x = 0; x < (uint32_t)w; x++) + { + pDst[0] = (uint8_t)(pSrc[0] - pPrev_src[0]); + pDst[1] = (uint8_t)(pSrc[1] - pPrev_src[1]); + pDst[2] = (uint8_t)(pSrc[2] - pPrev_src[2]); + pDst[3] = (uint8_t)(pSrc[3] - pPrev_src[3]); + + pSrc += 4; + pPrev_src += 4; + pDst += 4; + } + } + } + + break; + } + default: + assert(0); + break; + } + } + + bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector& out_buf, uint32_t flags) + { + if (!endian_check()) + { + assert(0); + return false; + } + + if ((w < 1) || (h < 1) || (w * (uint64_t)h > UINT32_MAX) || (w > FPNG_MAX_SUPPORTED_DIM) || (h > FPNG_MAX_SUPPORTED_DIM)) + { + assert(0); + return false; + } + + if ((num_chans != 3) && (num_chans != 4)) + { + assert(0); + return false; + } + + int i, bpl = w * num_chans; + uint32_t y; + + std::vector temp_buf; + temp_buf.resize((bpl + 1) * h + 7); + uint32_t temp_buf_ofs = 0; + + for (y = 0; y < h; ++y) + { + const uint8_t* pSrc = (uint8_t*)pImage + y * bpl; + const uint8_t* pPrev_src = y ? ((uint8_t*)pImage + (y - 1) * bpl) : nullptr; + + uint8_t* pDst = &temp_buf[temp_buf_ofs]; + + apply_filter(y ? 2 : 0, w, h, num_chans, bpl, pSrc, pPrev_src, pDst); + + temp_buf_ofs += 1 + bpl; + } + + const uint32_t PNG_HEADER_SIZE = 58; + + uint32_t out_ofs = PNG_HEADER_SIZE; + + out_buf.resize((out_ofs + (bpl + 1) * h + 7) & ~7); + + uint32_t defl_size = 0; + if ((flags & FPNG_FORCE_UNCOMPRESSED) == 0) + { + if (num_chans == 3) + { + if (flags & FPNG_ENCODE_SLOWER) + defl_size = pixel_deflate_dyn_3_rle(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs); + else + defl_size = pixel_deflate_dyn_3_rle_one_pass(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs); + } + else + { + if (flags & FPNG_ENCODE_SLOWER) + defl_size = pixel_deflate_dyn_4_rle(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs); + else + defl_size = pixel_deflate_dyn_4_rle_one_pass(temp_buf.data(), w, h, &out_buf[out_ofs], (uint32_t)out_buf.size() - out_ofs); + } + } + + uint32_t zlib_size = defl_size; + + if (!defl_size) + { + // Dynamic block failed to compress - fall back to uncompressed blocks, filter 0. + + temp_buf_ofs = 0; + + for (y = 0; y < h; ++y) + { + const uint8_t* pSrc = (uint8_t*)pImage + y * bpl; + + uint8_t* pDst = &temp_buf[temp_buf_ofs]; + + apply_filter(0, w, h, num_chans, bpl, pSrc, nullptr, pDst); + + temp_buf_ofs += 1 + bpl; + } + + assert(temp_buf_ofs <= temp_buf.size()); + + out_buf.resize(out_ofs + 6 + temp_buf_ofs + ((temp_buf_ofs + 65534) / 65535) * 5); + + uint32_t raw_size = write_raw_block(temp_buf.data(), (uint32_t)temp_buf_ofs, out_buf.data() + out_ofs, (uint32_t)out_buf.size() - out_ofs); + if (!raw_size) + { + // Somehow we miscomputed the size of the output buffer. + assert(0); + return false; + } + + zlib_size = raw_size; + } + + assert((out_ofs + zlib_size) <= out_buf.size()); + + out_buf.resize(out_ofs + zlib_size); + + const uint32_t idat_len = (uint32_t)out_buf.size() - PNG_HEADER_SIZE; + + // Write real PNG header, fdEC chunk, and the beginning of the IDAT chunk + { + static const uint8_t s_color_type[] = { 0x00, 0x00, 0x04, 0x02, 0x06 }; + + uint8_t pnghdr[58] = { + 0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a, // PNG sig + 0x00,0x00,0x00,0x0d, 'I','H','D','R', // IHDR chunk len, type + 0,0,(uint8_t)(w >> 8),(uint8_t)w, // width + 0,0,(uint8_t)(h >> 8),(uint8_t)h, // height + 8, //bit_depth + s_color_type[num_chans], // color_type + 0, // compression + 0, // filter + 0, // interlace + 0, 0, 0, 0, // IHDR crc32 + 0, 0, 0, 5, 'f', 'd', 'E', 'C', 82, 36, 147, 227, FPNG_FDEC_VERSION, 0xE5, 0xAB, 0x62, 0x99, // our custom private, ancillary, do not copy, fdEC chunk + (uint8_t)(idat_len >> 24),(uint8_t)(idat_len >> 16),(uint8_t)(idat_len >> 8),(uint8_t)idat_len, 'I','D','A','T' // IDATA chunk len, type + }; + + // Compute IHDR CRC32 + uint32_t c = (uint32_t)fpng_crc32(pnghdr + 12, 17, FPNG_CRC32_INIT); + for (i = 0; i < 4; ++i, c <<= 8) + ((uint8_t*)(pnghdr + 29))[i] = (uint8_t)(c >> 24); + + memcpy(out_buf.data(), pnghdr, PNG_HEADER_SIZE); + } + + // Write IDAT chunk's CRC32 and a 0 length IEND chunk + vector_append(out_buf, "\0\0\0\0\0\0\0\0\x49\x45\x4e\x44\xae\x42\x60\x82", 16); // IDAT CRC32, followed by the IEND chunk + + // Compute IDAT crc32 + uint32_t c = (uint32_t)fpng_crc32(out_buf.data() + PNG_HEADER_SIZE - 4, idat_len + 4, FPNG_CRC32_INIT); + + for (i = 0; i < 4; ++i, c <<= 8) + (out_buf.data() + out_buf.size() - 16)[i] = (uint8_t)(c >> 24); + + return true; + } + +#ifndef FPNG_NO_STDIO + bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags) + { + std::vector out_buf; + if (!fpng_encode_image_to_memory(pImage, w, h, num_chans, out_buf, flags)) + return false; + + FILE* pFile = nullptr; +#ifdef _MSC_VER + fopen_s(&pFile, pFilename, "wb"); +#else + pFile = fopen(pFilename, "wb"); +#endif + if (!pFile) + return false; + + if (fwrite(out_buf.data(), 1, out_buf.size(), pFile) != out_buf.size()) + { + fclose(pFile); + return false; + } + + return (fclose(pFile) != EOF); + } +#endif + + // Decompression + + const uint32_t FPNG_DECODER_TABLE_BITS = 12; + const uint32_t FPNG_DECODER_TABLE_SIZE = 1 << FPNG_DECODER_TABLE_BITS; + + static bool build_decoder_table(uint32_t num_syms, uint8_t* pCode_sizes, uint32_t* pTable) + { + uint32_t num_codes[16]; + + memset(num_codes, 0, sizeof(num_codes)); + for (uint32_t i = 0; i < num_syms; i++) + { + assert(pCode_sizes[i] <= FPNG_DECODER_TABLE_SIZE); + num_codes[pCode_sizes[i]]++; + } + + uint32_t next_code[17]; + next_code[0] = next_code[1] = 0; + uint32_t total = 0; + for (uint32_t i = 1; i <= 15; i++) + next_code[i + 1] = (uint32_t)(total = ((total + ((uint32_t)num_codes[i])) << 1)); + + if (total != 0x10000) + { + uint32_t j = 0; + + for (uint32_t i = 15; i != 0; i--) + if ((j += num_codes[i]) > 1) + return false; + + if (j != 1) + return false; + } + + uint32_t rev_codes[DEFL_MAX_HUFF_SYMBOLS]; + + for (uint32_t i = 0; i < num_syms; i++) + rev_codes[i] = next_code[pCode_sizes[i]]++; + + memset(pTable, 0, sizeof(uint32_t) * FPNG_DECODER_TABLE_SIZE); + + for (uint32_t i = 0; i < num_syms; i++) + { + const uint32_t code_size = pCode_sizes[i]; + if (!code_size) + continue; + + uint32_t old_code = rev_codes[i], new_code = 0; + for (uint32_t j = code_size; j != 0; j--) + { + new_code = (new_code << 1) | (old_code & 1); + old_code >>= 1; + } + + uint32_t j = 1 << code_size; + + while (new_code < FPNG_DECODER_TABLE_SIZE) + { + pTable[new_code] = i | (code_size << 9); + new_code += j; + } + } + + return true; + } + + static const uint16_t g_run_len3_to_4[259] = + { + 0, + 0, 0, 4, 0, 0, 8, 0, 0, 12, 0, 0, 16, 0, 0, 20, 0, 0, 24, 0, 0, 28, 0, 0, + 32, 0, 0, 36, 0, 0, 40, 0, 0, 44, 0, 0, 48, 0, 0, 52, 0, 0, 56, 0, 0, + 60, 0, 0, 64, 0, 0, 68, 0, 0, 72, 0, 0, 76, 0, 0, 80, 0, 0, 84, 0, 0, + 88, 0, 0, 92, 0, 0, 96, 0, 0, 100, 0, 0, 104, 0, 0, 108, 0, 0, 112, 0, 0, + 116, 0, 0, 120, 0, 0, 124, 0, 0, 128, 0, 0, 132, 0, 0, 136, 0, 0, 140, 0, 0, + 144, 0, 0, 148, 0, 0, 152, 0, 0, 156, 0, 0, 160, 0, 0, 164, 0, 0, 168, 0, 0, + 172, 0, 0, 176, 0, 0, 180, 0, 0, 184, 0, 0, 188, 0, 0, 192, 0, 0, 196, 0, 0, + 200, 0, 0, 204, 0, 0, 208, 0, 0, 212, 0, 0, 216, 0, 0, 220, 0, 0, 224, 0, 0, + 228, 0, 0, 232, 0, 0, 236, 0, 0, 240, 0, 0, 244, 0, 0, 248, 0, 0, 252, 0, 0, + 256, 0, 0, 260, 0, 0, 264, 0, 0, 268, 0, 0, 272, 0, 0, 276, 0, 0, 280, 0, 0, + 284, 0, 0, 288, 0, 0, 292, 0, 0, 296, 0, 0, 300, 0, 0, 304, 0, 0, 308, 0, 0, + 312, 0, 0, 316, 0, 0, 320, 0, 0, 324, 0, 0, 328, 0, 0, 332, 0, 0, 336, 0, 0, + 340, 0, 0, + 344, + }; + + static const int s_length_extra[] = { 0,0,0,0, 0,0,0,0, 1,1,1,1, 2,2,2,2, 3,3,3,3, 4,4,4,4, 5,5,5,5, 0, 0,0 }; + static const int s_length_range[] = { 3,4,5,6, 7,8,9,10, 11,13,15,17, 19,23,27,31, 35,43,51,59, 67,83,99,115, 131,163,195,227, 258, 0,0 }; + +#define ENSURE_32BITS() do { \ + if (bit_buf_size < 32) { \ + if ((src_ofs + 4) > src_len) return false; \ + bit_buf |= ((uint64_t)READ_LE32(pSrc + src_ofs)) << bit_buf_size; \ + src_ofs += 4; bit_buf_size += 32; } \ + } while(0) + +#define GET_BITS(b, ll) do { \ + uint32_t l = ll; assert(l && (l <= 32)); \ + b = (uint32_t)(bit_buf & g_bitmasks[l]); \ + bit_buf >>= l; \ + bit_buf_size -= l; \ + ENSURE_32BITS(); \ + } while(0) + +#define SKIP_BITS(ll) do { \ + uint32_t l = ll; assert(l <= 32); \ + bit_buf >>= l; \ + bit_buf_size -= l; \ + ENSURE_32BITS(); \ + } while(0) + +#define GET_BITS_NE(b, ll) do { \ + uint32_t l = ll; assert(l && (l <= 32) && (bit_buf_size >= l)); \ + b = (uint32_t)(bit_buf & g_bitmasks[l]); \ + bit_buf >>= l; \ + bit_buf_size -= l; \ + } while(0) + +#define SKIP_BITS_NE(ll) do { \ + uint32_t l = ll; assert(l <= 32 && (bit_buf_size >= l)); \ + bit_buf >>= l; \ + bit_buf_size -= l; \ + } while(0) + + static bool prepare_dynamic_block( + const uint8_t* pSrc, uint32_t src_len, uint32_t& src_ofs, + uint32_t& bit_buf_size, uint64_t& bit_buf, + uint32_t* pLit_table, uint32_t num_chans) + { + static const uint8_t s_bit_length_order[] = { 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }; + + uint32_t num_lit_codes, num_dist_codes, num_clen_codes; + + GET_BITS(num_lit_codes, 5); + num_lit_codes += 257; + + GET_BITS(num_dist_codes, 5); + num_dist_codes += 1; + + uint32_t total_codes = num_lit_codes + num_dist_codes; + if (total_codes > (DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1)) + return false; + + uint8_t code_sizes[DEFL_MAX_HUFF_SYMBOLS_0 + DEFL_MAX_HUFF_SYMBOLS_1]; + memset(code_sizes, 0, sizeof(code_sizes)); + + GET_BITS(num_clen_codes, 4); + num_clen_codes += 4; + + uint8_t clen_codesizes[DEFL_MAX_HUFF_SYMBOLS_2]; + memset(clen_codesizes, 0, sizeof(clen_codesizes)); + + for (uint32_t i = 0; i < num_clen_codes; i++) + { + uint32_t len = 0; + GET_BITS(len, 3); + clen_codesizes[s_bit_length_order[i]] = (uint8_t)len; + } + + uint32_t clen_table[FPNG_DECODER_TABLE_SIZE]; + if (!build_decoder_table(DEFL_MAX_HUFF_SYMBOLS_2, clen_codesizes, clen_table)) + return false; + + uint32_t min_code_size = 15; + + for (uint32_t cur_code = 0; cur_code < total_codes; ) + { + uint32_t sym = clen_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t sym_len = sym >> 9; + if (!sym_len) + return false; + SKIP_BITS(sym_len); + sym &= 511; + + if (sym <= 15) + { + // Can't be a fpng Huffman table + if (sym > FPNG_DECODER_TABLE_BITS) + return false; + + if (sym) + min_code_size = minimum(min_code_size, sym); + + code_sizes[cur_code++] = (uint8_t)sym; + continue; + } + + uint32_t rep_len = 0, rep_code_size = 0; + + switch (sym) + { + case 16: + { + GET_BITS(rep_len, 2); + rep_len += 3; + if (!cur_code) + return false; + rep_code_size = code_sizes[cur_code - 1]; + break; + } + case 17: + { + GET_BITS(rep_len, 3); + rep_len += 3; + rep_code_size = 0; + break; + } + case 18: + { + GET_BITS(rep_len, 7); + rep_len += 11; + rep_code_size = 0; + break; + } + } + + if ((cur_code + rep_len) > total_codes) + return false; + + for (; rep_len; rep_len--) + code_sizes[cur_code++] = (uint8_t)rep_code_size; + } + + uint8_t lit_codesizes[DEFL_MAX_HUFF_SYMBOLS_0]; + + memcpy(lit_codesizes, code_sizes, num_lit_codes); + memset(lit_codesizes + num_lit_codes, 0, DEFL_MAX_HUFF_SYMBOLS_0 - num_lit_codes); + + uint32_t total_valid_distcodes = 0; + for (uint32_t i = 0; i < num_dist_codes; i++) + total_valid_distcodes += (code_sizes[num_lit_codes + i] == 1); + + // 1 or 2 because the first version of FPNG only issued 1 valid distance code, but that upset wuffs. So we let 1 or 2 through. + if ((total_valid_distcodes < 1) || (total_valid_distcodes > 2)) + return false; + + if (code_sizes[num_lit_codes + (num_chans - 1)] != 1) + return false; + + if (total_valid_distcodes == 2) + { + // If there are two valid distance codes, make sure the first is 1 bit. + if (code_sizes[num_lit_codes + num_chans] != 1) + return false; + } + + if (!build_decoder_table(num_lit_codes, lit_codesizes, pLit_table)) + return false; + + // Add next symbol to decoder table, when it fits + for (uint32_t i = 0; i < FPNG_DECODER_TABLE_SIZE; i++) + { + uint32_t sym = pLit_table[i] & 511; + if (sym >= 256) + continue; + + uint32_t sym_bits = (pLit_table[i] >> 9) & 15; + if (!sym_bits) + continue; + assert(sym_bits <= FPNG_DECODER_TABLE_BITS); + + uint32_t bits_left = FPNG_DECODER_TABLE_BITS - sym_bits; + if (bits_left < min_code_size) + continue; + + uint32_t next_bits = i >> sym_bits; + uint32_t next_sym = pLit_table[next_bits] & 511; + uint32_t next_sym_bits = (pLit_table[next_bits] >> 9) & 15; + if ((!next_sym_bits) || (bits_left < next_sym_bits)) + continue; + + pLit_table[i] |= (next_sym << 16) | (next_sym_bits << (16 + 9)); + } + + return true; + } + + static bool fpng_pixel_zlib_raw_decompress( + const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len, + uint8_t* pDst, uint32_t w, uint32_t h, + uint32_t src_chans, uint32_t dst_chans) + { + assert((src_chans == 3) || (src_chans == 4)); + assert((dst_chans == 3) || (dst_chans == 4)); + + const uint32_t src_bpl = w * src_chans; + const uint32_t dst_bpl = w * dst_chans; + const uint32_t dst_len = dst_bpl * h; + + uint32_t src_ofs = 2; + uint32_t dst_ofs = 0; + uint32_t raster_ofs = 0; + uint32_t comp_ofs = 0; + + for (; ; ) + { + if ((src_ofs + 1) > src_len) + return false; + + const bool bfinal = (pSrc[src_ofs] & 1) != 0; + const uint32_t btype = (pSrc[src_ofs] >> 1) & 3; + if (btype != 0) + return false; + + src_ofs++; + + if ((src_ofs + 4) > src_len) + return false; + uint32_t len = pSrc[src_ofs + 0] | (pSrc[src_ofs + 1] << 8); + uint32_t nlen = pSrc[src_ofs + 2] | (pSrc[src_ofs + 3] << 8); + src_ofs += 4; + + if (len != (~nlen & 0xFFFF)) + return false; + + if ((src_ofs + len) > src_len) + return false; + + // Raw blocks are a relatively uncommon case so this isn't well optimized. + // Supports 3->4 and 4->3 byte/pixel conversion. + for (uint32_t i = 0; i < len; i++) + { + uint32_t c = pSrc[src_ofs + i]; + + if (!raster_ofs) + { + // Check filter type + if (c != 0) + return false; + + assert(!comp_ofs); + } + else + { + if (comp_ofs < dst_chans) + { + if (dst_ofs == dst_len) + return false; + + pDst[dst_ofs++] = (uint8_t)c; + } + + if (++comp_ofs == src_chans) + { + if (dst_chans > src_chans) + { + if (dst_ofs == dst_len) + return false; + + pDst[dst_ofs++] = (uint8_t)0xFF; + } + + comp_ofs = 0; + } + } + + if (++raster_ofs == (src_bpl + 1)) + { + assert(!comp_ofs); + raster_ofs = 0; + } + } + + src_ofs += len; + + if (bfinal) + break; + } + + if (comp_ofs != 0) + return false; + + // Check for zlib adler32 + if ((src_ofs + 4) != zlib_len) + return false; + + return (dst_ofs == dst_len); + } + + template + static bool fpng_pixel_zlib_decompress_3( + const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len, + uint8_t* pDst, uint32_t w, uint32_t h) + { + assert(src_len >= (zlib_len + 4)); + + const uint32_t dst_bpl = w * dst_comps; + //const uint32_t dst_len = dst_bpl * h; + + if (zlib_len < 7) + return false; + + // check zlib header + if ((pSrc[0] != 0x78) || (pSrc[1] != 0x01)) + return false; + + uint32_t src_ofs = 2; + + if ((pSrc[src_ofs] & 6) == 0) + return fpng_pixel_zlib_raw_decompress(pSrc, src_len, zlib_len, pDst, w, h, 3, dst_comps); + + if ((src_ofs + 4) > src_len) + return false; + uint64_t bit_buf = READ_LE32(pSrc + src_ofs); + src_ofs += 4; + + uint32_t bit_buf_size = 32; + + uint32_t bfinal, btype; + GET_BITS(bfinal, 1); + GET_BITS(btype, 2); + + // Must be the final block or it's not valid, and type=2 (dynamic) + if ((bfinal != 1) || (btype != 2)) + return false; + + uint32_t lit_table[FPNG_DECODER_TABLE_SIZE]; + if (!prepare_dynamic_block(pSrc, src_len, src_ofs, bit_buf_size, bit_buf, lit_table, 3)) + return false; + + const uint8_t* pPrev_scanline = nullptr; + uint8_t* pCur_scanline = pDst; + + for (uint32_t y = 0; y < h; y++) + { + // At start of PNG scanline, so read the filter literal + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t filter = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t filter_len = (filter >> 9) & 15; + if (!filter_len) + return false; + SKIP_BITS(filter_len); + filter &= 511; + + uint32_t expected_filter = (y ? 2 : 0); + if (filter != expected_filter) + return false; + + uint32_t x_ofs = 0; + uint8_t prev_delta_r = 0, prev_delta_g = 0, prev_delta_b = 0; + do + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t lit0_tab = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + + uint32_t lit0 = lit0_tab; + uint32_t lit0_len = (lit0_tab >> 9) & 15; + if (!lit0_len) + return false; + SKIP_BITS(lit0_len); + + if (lit0 & 256) + { + lit0 &= 511; + + // Can't be EOB - we still have more pixels to decompress. + if (lit0 == 256) + return false; + + // Must be an RLE match against the previous pixel. + uint32_t run_len = s_length_range[lit0 - 257]; + if (lit0 >= 265) + { + uint32_t e; + GET_BITS_NE(e, s_length_extra[lit0 - 257]); + + run_len += e; + } + + // Skip match distance - it's always the same (3) + SKIP_BITS_NE(1); + + // Matches must always be a multiple of 3/4 bytes + assert((run_len % 3) == 0); + + if (dst_comps == 4) + { + const uint32_t x_ofs_end = x_ofs + g_run_len3_to_4[run_len]; + + // Check for valid run lengths + if (x_ofs == x_ofs_end) + return false; + + // Matches cannot cross scanlines. + if (x_ofs_end > dst_bpl) + return false; + + if (pPrev_scanline) + { + if ((prev_delta_r | prev_delta_g | prev_delta_b) == 0) + { + memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, x_ofs_end - x_ofs); + x_ofs = x_ofs_end; + } + else + { + do + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b); + pCur_scanline[x_ofs + 3] = 0xFF; + x_ofs += 4; + } while (x_ofs < x_ofs_end); + } + } + else + { + do + { + pCur_scanline[x_ofs] = prev_delta_r; + pCur_scanline[x_ofs + 1] = prev_delta_g; + pCur_scanline[x_ofs + 2] = prev_delta_b; + pCur_scanline[x_ofs + 3] = 0xFF; + x_ofs += 4; + } while (x_ofs < x_ofs_end); + } + } + else + { + // Check for valid run lengths + if (!g_run_len3_to_4[run_len]) + return false; + + const uint32_t x_ofs_end = x_ofs + run_len; + + // Matches cannot cross scanlines. + if (x_ofs_end > dst_bpl) + return false; + + if (pPrev_scanline) + { + if ((prev_delta_r | prev_delta_g | prev_delta_b) == 0) + { + memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len); + x_ofs = x_ofs_end; + } + else + { + do + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b); + x_ofs += 3; + } while (x_ofs < x_ofs_end); + } + } + else + { + do + { + pCur_scanline[x_ofs] = prev_delta_r; + pCur_scanline[x_ofs + 1] = prev_delta_g; + pCur_scanline[x_ofs + 2] = prev_delta_b; + x_ofs += 3; + } while (x_ofs < x_ofs_end); + } + } + } + else + { + uint32_t lit1, lit2; + + uint32_t lit1_spec_len = (lit0_tab >> (16 + 9)); + uint32_t lit2_len; + if (lit1_spec_len) + { + lit1 = (lit0_tab >> 16) & 511; + SKIP_BITS_NE(lit1_spec_len); + + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit2_len = (lit2 >> 9) & 15; + if (!lit2_len) + return false; + } + else + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t lit1_len = (lit1 >> 9) & 15; + if (!lit1_len) + return false; + SKIP_BITS_NE(lit1_len); + + lit2_len = (lit1 >> (16 + 9)); + if (lit2_len) + lit2 = lit1 >> 16; + else + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit2_len = (lit2 >> 9) & 15; + if (!lit2_len) + return false; + } + } + + SKIP_BITS(lit2_len); + + // Check for matches + if ((lit1 | lit2) & 256) + return false; + + if (dst_comps == 4) + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + pCur_scanline[x_ofs + 3] = 0xFF; + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + pCur_scanline[x_ofs + 3] = 0xFF; + } + x_ofs += 4; + } + else + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + } + x_ofs += 3; + } + + prev_delta_r = (uint8_t)lit0; + prev_delta_g = (uint8_t)lit1; + prev_delta_b = (uint8_t)lit2; + + // See if we can decode one more pixel. + uint32_t spec_next_len0_len = lit2 >> (16 + 9); + if ((spec_next_len0_len) && (x_ofs < dst_bpl)) + { + lit0 = (lit2 >> 16) & 511; + if (lit0 < 256) + { + SKIP_BITS_NE(spec_next_len0_len); + + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t lit1_len = (lit1 >> 9) & 15; + if (!lit1_len) + return false; + SKIP_BITS(lit1_len); + + lit2_len = (lit1 >> (16 + 9)); + if (lit2_len) + lit2 = lit1 >> 16; + else + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit2_len = (lit2 >> 9) & 15; + if (!lit2_len) + return false; + } + + SKIP_BITS_NE(lit2_len); + + // Check for matches + if ((lit1 | lit2) & 256) + return false; + + if (dst_comps == 4) + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + pCur_scanline[x_ofs + 3] = 0xFF; + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + pCur_scanline[x_ofs + 3] = 0xFF; + } + x_ofs += 4; + } + else + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + } + x_ofs += 3; + } + + prev_delta_r = (uint8_t)lit0; + prev_delta_g = (uint8_t)lit1; + prev_delta_b = (uint8_t)lit2; + + } // if (lit0 < 256) + + } // if ((spec_next_len0_len) && (x_ofs < bpl)) + } + + } while (x_ofs < dst_bpl); + + pPrev_scanline = pCur_scanline; + pCur_scanline += dst_bpl; + + } // y + + // The last symbol should be EOB + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t lit0 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t lit0_len = (lit0 >> 9) & 15; + if (!lit0_len) + return false; + lit0 &= 511; + if (lit0 != 256) + return false; + + bit_buf_size -= lit0_len; + bit_buf >>= lit0_len; + + uint32_t align_bits = bit_buf_size & 7; + bit_buf_size -= align_bits; + bit_buf >>= align_bits; + + if (src_ofs < (bit_buf_size >> 3)) + return false; + src_ofs -= (bit_buf_size >> 3); + + // We should be at the very end, because the bit buf reads ahead 32-bits (which contains the zlib adler32). + if ((src_ofs + 4) != zlib_len) + return false; + + return true; + } + + template + static bool fpng_pixel_zlib_decompress_4( + const uint8_t* pSrc, uint32_t src_len, uint32_t zlib_len, + uint8_t* pDst, uint32_t w, uint32_t h) + { + assert(src_len >= (zlib_len + 4)); + + const uint32_t dst_bpl = w * dst_comps; + //const uint32_t dst_len = dst_bpl * h; + + if (zlib_len < 7) + return false; + + // check zlib header + if ((pSrc[0] != 0x78) || (pSrc[1] != 0x01)) + return false; + + uint32_t src_ofs = 2; + + if ((pSrc[src_ofs] & 6) == 0) + return fpng_pixel_zlib_raw_decompress(pSrc, src_len, zlib_len, pDst, w, h, 4, dst_comps); + + if ((src_ofs + 4) > src_len) + return false; + uint64_t bit_buf = READ_LE32(pSrc + src_ofs); + src_ofs += 4; + + uint32_t bit_buf_size = 32; + + uint32_t bfinal, btype; + GET_BITS(bfinal, 1); + GET_BITS(btype, 2); + + // Must be the final block or it's not valid, and type=2 (dynamic) + if ((bfinal != 1) || (btype != 2)) + return false; + + uint32_t lit_table[FPNG_DECODER_TABLE_SIZE]; + if (!prepare_dynamic_block(pSrc, src_len, src_ofs, bit_buf_size, bit_buf, lit_table, 4)) + return false; + + const uint8_t* pPrev_scanline = nullptr; + uint8_t* pCur_scanline = pDst; + + for (uint32_t y = 0; y < h; y++) + { + // At start of PNG scanline, so read the filter literal + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t filter = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t filter_len = (filter >> 9) & 15; + if (!filter_len) + return false; + SKIP_BITS(filter_len); + filter &= 511; + + uint32_t expected_filter = (y ? 2 : 0); + if (filter != expected_filter) + return false; + + uint32_t x_ofs = 0; + uint8_t prev_delta_r = 0, prev_delta_g = 0, prev_delta_b = 0, prev_delta_a = 0; + do + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t lit0_tab = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + + uint32_t lit0 = lit0_tab; + uint32_t lit0_len = (lit0_tab >> 9) & 15; + if (!lit0_len) + return false; + SKIP_BITS(lit0_len); + + if (lit0 & 256) + { + lit0 &= 511; + + // Can't be EOB - we still have more pixels to decompress. + if (lit0 == 256) + return false; + + // Must be an RLE match against the previous pixel. + uint32_t run_len = s_length_range[lit0 - 257]; + if (lit0 >= 265) + { + uint32_t e; + GET_BITS_NE(e, s_length_extra[lit0 - 257]); + + run_len += e; + } + + // Skip match distance - it's always the same (4) + SKIP_BITS_NE(1); + + // Matches must always be a multiple of 3/4 bytes + if (run_len & 3) + return false; + + if (dst_comps == 3) + { + const uint32_t run_len3 = (run_len >> 2) * 3; + const uint32_t x_ofs_end = x_ofs + run_len3; + + // Matches cannot cross scanlines. + if (x_ofs_end > dst_bpl) + return false; + + if (pPrev_scanline) + { + if ((prev_delta_r | prev_delta_g | prev_delta_b | prev_delta_a) == 0) + { + memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len3); + x_ofs = x_ofs_end; + } + else + { + do + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b); + x_ofs += 3; + } while (x_ofs < x_ofs_end); + } + } + else + { + do + { + pCur_scanline[x_ofs] = prev_delta_r; + pCur_scanline[x_ofs + 1] = prev_delta_g; + pCur_scanline[x_ofs + 2] = prev_delta_b; + x_ofs += 3; + } while (x_ofs < x_ofs_end); + } + } + else + { + const uint32_t x_ofs_end = x_ofs + run_len; + + // Matches cannot cross scanlines. + if (x_ofs_end > dst_bpl) + return false; + + if (pPrev_scanline) + { + if ((prev_delta_r | prev_delta_g | prev_delta_b | prev_delta_a) == 0) + { + memcpy(pCur_scanline + x_ofs, pPrev_scanline + x_ofs, run_len); + x_ofs = x_ofs_end; + } + else + { + do + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + prev_delta_r); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + prev_delta_g); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + prev_delta_b); + pCur_scanline[x_ofs + 3] = (uint8_t)(pPrev_scanline[x_ofs + 3] + prev_delta_a); + x_ofs += 4; + } while (x_ofs < x_ofs_end); + } + } + else + { + do + { + pCur_scanline[x_ofs] = prev_delta_r; + pCur_scanline[x_ofs + 1] = prev_delta_g; + pCur_scanline[x_ofs + 2] = prev_delta_b; + pCur_scanline[x_ofs + 3] = prev_delta_a; + x_ofs += 4; + } while (x_ofs < x_ofs_end); + } + } + } + else + { + uint32_t lit1, lit2; + + uint32_t lit1_spec_len = (lit0_tab >> (16 + 9)); + uint32_t lit2_len; + if (lit1_spec_len) + { + lit1 = (lit0_tab >> 16) & 511; + SKIP_BITS_NE(lit1_spec_len); + + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit2_len = (lit2 >> 9) & 15; + if (!lit2_len) + return false; + } + else + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit1 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t lit1_len = (lit1 >> 9) & 15; + if (!lit1_len) + return false; + SKIP_BITS_NE(lit1_len); + + lit2_len = (lit1 >> (16 + 9)); + if (lit2_len) + lit2 = lit1 >> 16; + else + { + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit2 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit2_len = (lit2 >> 9) & 15; + if (!lit2_len) + return false; + } + } + + uint32_t lit3; + uint32_t lit3_len = lit2 >> (16 + 9); + + if (lit3_len) + { + lit3 = (lit2 >> 16); + SKIP_BITS(lit2_len + lit3_len); + } + else + { + SKIP_BITS(lit2_len); + + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + lit3 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + lit3_len = (lit3 >> 9) & 15; + if (!lit3_len) + return false; + + SKIP_BITS_NE(lit3_len); + } + + // Check for matches + if ((lit1 | lit2 | lit3) & 256) + return false; + + if (dst_comps == 3) + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + } + + x_ofs += 3; + } + else + { + if (pPrev_scanline) + { + pCur_scanline[x_ofs] = (uint8_t)(pPrev_scanline[x_ofs] + lit0); + pCur_scanline[x_ofs + 1] = (uint8_t)(pPrev_scanline[x_ofs + 1] + lit1); + pCur_scanline[x_ofs + 2] = (uint8_t)(pPrev_scanline[x_ofs + 2] + lit2); + pCur_scanline[x_ofs + 3] = (uint8_t)(pPrev_scanline[x_ofs + 3] + lit3); + } + else + { + pCur_scanline[x_ofs] = (uint8_t)lit0; + pCur_scanline[x_ofs + 1] = (uint8_t)lit1; + pCur_scanline[x_ofs + 2] = (uint8_t)lit2; + pCur_scanline[x_ofs + 3] = (uint8_t)lit3; + } + + x_ofs += 4; + } + + prev_delta_r = (uint8_t)lit0; + prev_delta_g = (uint8_t)lit1; + prev_delta_b = (uint8_t)lit2; + prev_delta_a = (uint8_t)lit3; + } + + } while (x_ofs < dst_bpl); + + pPrev_scanline = pCur_scanline; + pCur_scanline += dst_bpl; + } // y + + // The last symbol should be EOB + assert(bit_buf_size >= FPNG_DECODER_TABLE_BITS); + uint32_t lit0 = lit_table[bit_buf & (FPNG_DECODER_TABLE_SIZE - 1)]; + uint32_t lit0_len = (lit0 >> 9) & 15; + if (!lit0_len) + return false; + lit0 &= 511; + if (lit0 != 256) + return false; + + bit_buf_size -= lit0_len; + bit_buf >>= lit0_len; + + uint32_t align_bits = bit_buf_size & 7; + bit_buf_size -= align_bits; + bit_buf >>= align_bits; + + if (src_ofs < (bit_buf_size >> 3)) + return false; + src_ofs -= (bit_buf_size >> 3); + + // We should be at the very end, because the bit buf reads ahead 32-bits (which contains the zlib adler32). + if ((src_ofs + 4) != zlib_len) + return false; + + return true; + } + +#pragma pack(push) +#pragma pack(1) + struct png_chunk_prefix + { + uint32_t m_length; + uint8_t m_type[4]; + }; + struct png_ihdr + { + png_chunk_prefix m_prefix; + uint32_t m_width; + uint32_t m_height; + uint8_t m_bitdepth; + uint8_t m_color_type; + uint8_t m_comp_method; + uint8_t m_filter_method; + uint8_t m_interlace_method; + uint32_t m_crc32; + }; + const uint32_t IHDR_EXPECTED_LENGTH = 13; + struct png_iend + { + png_chunk_prefix m_prefix; + uint32_t m_crc32; + }; +#pragma pack(pop) + + static int fpng_get_info_internal(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t &idat_ofs, uint32_t &idat_len) + { + static const uint8_t s_png_sig[8] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + + if (!endian_check()) + { + assert(0); + return false; + } + + width = 0; + height = 0; + channels_in_file = 0; + idat_ofs = 0, idat_len = 0; + + // Ensure the file has at least a minimum possible size + if (image_size < (sizeof(s_png_sig) + sizeof(png_ihdr) + sizeof(png_chunk_prefix) + 1 + sizeof(uint32_t) + sizeof(png_iend))) + return FPNG_DECODE_FAILED_NOT_PNG; + + if (memcmp(pImage, s_png_sig, 8) != 0) + return FPNG_DECODE_FAILED_NOT_PNG; + + const uint8_t* pImage_u8 = static_cast(pImage) + 8; + + const png_ihdr& ihdr = *reinterpret_cast(pImage_u8); + pImage_u8 += sizeof(png_ihdr); + + if (READ_BE32(&ihdr.m_prefix.m_length) != IHDR_EXPECTED_LENGTH) + return FPNG_DECODE_FAILED_NOT_PNG; + + if (fpng_crc32(ihdr.m_prefix.m_type, 4 + IHDR_EXPECTED_LENGTH, FPNG_CRC32_INIT) != READ_BE32(&ihdr.m_crc32)) + return FPNG_DECODE_FAILED_HEADER_CRC32; + + width = READ_BE32(&ihdr.m_width); + height = READ_BE32(&ihdr.m_height); + + if (!width || !height || (width > FPNG_MAX_SUPPORTED_DIM) || (height > FPNG_MAX_SUPPORTED_DIM)) + return FPNG_DECODE_FAILED_INVALID_DIMENSIONS; + + uint64_t total_pixels = (uint64_t)width * height; + if (total_pixels > (1 << 30)) + return FPNG_DECODE_FAILED_INVALID_DIMENSIONS; + + if ((ihdr.m_comp_method) || (ihdr.m_filter_method) || (ihdr.m_interlace_method) || (ihdr.m_bitdepth != 8)) + return FPNG_DECODE_NOT_FPNG; + + if (ihdr.m_color_type == 2) + channels_in_file = 3; + else if (ihdr.m_color_type == 6) + channels_in_file = 4; + + if (!channels_in_file) + return FPNG_DECODE_NOT_FPNG; + + // Scan all the chunks. Look for one IDAT, IEND, and our custom fdEC chunk that indicates the file was compressed by us. Skip any ancillary chunks. + bool found_fdec_chunk = false; + + for (; ; ) + { + const size_t src_ofs = pImage_u8 - static_cast(pImage); + if (src_ofs >= image_size) + return FPNG_DECODE_FAILED_CHUNK_PARSING; + + const uint32_t bytes_remaining = image_size - (uint32_t)src_ofs; + if (bytes_remaining < sizeof(uint32_t) * 3) + return FPNG_DECODE_FAILED_CHUNK_PARSING; + + const png_chunk_prefix* pChunk = reinterpret_cast(pImage_u8); + + const uint32_t chunk_len = READ_BE32(&pChunk->m_length); + if ((src_ofs + sizeof(uint32_t) * 2 + (uint64_t)chunk_len + sizeof(uint32_t)) > image_size) + return FPNG_DECODE_FAILED_CHUNK_PARSING; + + for (uint32_t i = 0; i < 4; i++) + { + const uint8_t c = pChunk->m_type[i]; + const bool is_upper = (c >= 65) && (c <= 90), is_lower = (c >= 97) && (c <= 122); + if ((!is_upper) && (!is_lower)) + return FPNG_DECODE_FAILED_CHUNK_PARSING; + } + + const uint32_t expected_crc32 = READ_BE32(pImage_u8 + sizeof(uint32_t) * 2 + chunk_len); + + char chunk_type[5] = { (char)pChunk->m_type[0], (char)pChunk->m_type[1], (char)pChunk->m_type[2], (char)pChunk->m_type[3], 0 }; + const bool is_idat = strcmp(chunk_type, "IDAT") == 0; + +#if !FPNG_DISABLE_DECODE_CRC32_CHECKS + if (!is_idat) + { + uint32_t actual_crc32 = fpng_crc32(pImage_u8 + sizeof(uint32_t), sizeof(uint32_t) + chunk_len, FPNG_CRC32_INIT); + if (actual_crc32 != expected_crc32) + return FPNG_DECODE_FAILED_HEADER_CRC32; + } +#endif + + const uint8_t* pChunk_data = pImage_u8 + sizeof(uint32_t) * 2; + + if (strcmp(chunk_type, "IEND") == 0) + break; + else if (is_idat) + { + // If there were multiple IDAT's, or we didn't find the fdEC chunk, then it's not FPNG. + if ((idat_ofs) || (!found_fdec_chunk)) + return FPNG_DECODE_NOT_FPNG; + + idat_ofs = (uint32_t)src_ofs; + idat_len = chunk_len; + + // Sanity check the IDAT chunk length + if (idat_len < 7) + return FPNG_DECODE_FAILED_INVALID_IDAT; + } + else if (strcmp(chunk_type, "fdEC") == 0) + { + if (found_fdec_chunk) + return FPNG_DECODE_NOT_FPNG; + + // We've got our fdEC chunk. Now make sure it's big enough and check its contents. + if (chunk_len != 5) + return FPNG_DECODE_NOT_FPNG; + + // Check fdEC chunk sig + if ((pChunk_data[0] != 82) || (pChunk_data[1] != 36) || (pChunk_data[2] != 147) || (pChunk_data[3] != 227)) + return FPNG_DECODE_NOT_FPNG; + + // Check fdEC version + if (pChunk_data[4] != FPNG_FDEC_VERSION) + return FPNG_DECODE_NOT_FPNG; + + found_fdec_chunk = true; + } + else + { + // Bail if it's a critical chunk - can't be FPNG + if ((chunk_type[0] & 32) == 0) + return FPNG_DECODE_NOT_FPNG; + + // ancillary chunk - skip it + } + + pImage_u8 += sizeof(png_chunk_prefix) + chunk_len + sizeof(uint32_t); + } + + if ((!found_fdec_chunk) || (!idat_ofs)) + return FPNG_DECODE_NOT_FPNG; + + return FPNG_DECODE_SUCCESS; + } + + int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file) + { + uint32_t idat_ofs = 0, idat_len = 0; + return fpng_get_info_internal(pImage, image_size, width, height, channels_in_file, idat_ofs, idat_len); + } + + int fpng_decode_memory(const void *pImage, uint32_t image_size, std::vector &out, uint32_t& width, uint32_t& height, uint32_t &channels_in_file, uint32_t desired_channels) + { + out.resize(0); + width = 0; + height = 0; + channels_in_file = 0; + + if ((!pImage) || (!image_size) || ((desired_channels != 3) && (desired_channels != 4))) + { + assert(0); + return FPNG_DECODE_INVALID_ARG; + } + + uint32_t idat_ofs = 0, idat_len = 0; + int status = fpng_get_info_internal(pImage, image_size, width, height, channels_in_file, idat_ofs, idat_len); + if (status) + return status; + + const uint64_t mem_needed = (uint64_t)width * height * desired_channels; + if (mem_needed > UINT32_MAX) + return FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE; + + // On 32-bit systems do a quick sanity check before we try to resize the output buffer. + if ((sizeof(size_t) == sizeof(uint32_t)) && (mem_needed >= 0x80000000)) + return FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE; + + out.resize(mem_needed); + + const uint8_t* pIDAT_data = static_cast(pImage) + idat_ofs + sizeof(uint32_t) * 2; + const uint32_t src_len = image_size - (idat_ofs + sizeof(uint32_t) * 2); + + bool decomp_status; + if (desired_channels == 3) + { + if (channels_in_file == 3) + decomp_status = fpng_pixel_zlib_decompress_3<3>(pIDAT_data, src_len, idat_len, out.data(), width, height); + else + decomp_status = fpng_pixel_zlib_decompress_4<3>(pIDAT_data, src_len, idat_len, out.data(), width, height); + } + else + { + if (channels_in_file == 3) + decomp_status = fpng_pixel_zlib_decompress_3<4>(pIDAT_data, src_len, idat_len, out.data(), width, height); + else + decomp_status = fpng_pixel_zlib_decompress_4<4>(pIDAT_data, src_len, idat_len, out.data(), width, height); + } + if (!decomp_status) + { + // Something went wrong. Either the file data was corrupted, or it doesn't conform to one of our zlib/Deflate constraints. + // The conservative thing to do is indicate it wasn't written by us, and let the general purpose PNG decoder handle it. + return FPNG_DECODE_NOT_FPNG; + } + + return FPNG_DECODE_SUCCESS; + } + +#ifndef FPNG_NO_STDIO + int fpng_decode_file(const char* pFilename, std::vector& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels) + { + FILE* pFile = nullptr; + +#ifdef _MSC_VER + fopen_s(&pFile, pFilename, "rb"); +#else + pFile = fopen(pFilename, "rb"); +#endif + + if (!pFile) + return FPNG_DECODE_FILE_OPEN_FAILED; + + if (fseek(pFile, 0, SEEK_END) != 0) + { + fclose(pFile); + return FPNG_DECODE_FILE_SEEK_FAILED; + } + +#ifdef _WIN32 + int64_t filesize = _ftelli64(pFile); +#else + int64_t filesize = ftello(pFile); +#endif + + if (fseek(pFile, 0, SEEK_SET) != 0) + { + fclose(pFile); + return FPNG_DECODE_FILE_SEEK_FAILED; + } + + if ( (filesize < 0) || (filesize > UINT32_MAX) || ( (sizeof(size_t) == sizeof(uint32_t)) && (filesize > 0x70000000) ) ) + { + fclose(pFile); + return FPNG_DECODE_FILE_TOO_LARGE; + } + + std::vector buf((size_t)filesize); + if (fread(buf.data(), 1, buf.size(), pFile) != buf.size()) + { + fclose(pFile); + return FPNG_DECODE_FILE_READ_FAILED; + } + + fclose(pFile); + + return fpng_decode_memory(buf.data(), (uint32_t)buf.size(), out, width, height, channels_in_file, desired_channels); + } +#endif + +} // namespace fpng + +/* + This is free and unencumbered software released into the public domain. + + Anyone is free to copy, modify, publish, use, compile, sell, or + distribute this software, either in source code form or as a compiled + binary, for any purpose, commercial or non-commercial, and by any + means. + + In jurisdictions that recognize copyright laws, the author or authors + of this software dedicate any and all copyright interest in the + software to the public domain. We make this dedication for the benefit + of the public at large and to the detriment of our heirs and + successors. We intend this dedication to be an overt act of + relinquishment in perpetuity of all present and future rights to this + software under copyright law. + + 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 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. + + For more information, please refer to + + Richard Geldreich, Jr. + 12/30/2021 +*/ diff --git a/src/spice2x/external/fpng/fpng.h b/src/spice2x/external/fpng/fpng.h new file mode 100644 index 0000000..4d55e3a --- /dev/null +++ b/src/spice2x/external/fpng/fpng.h @@ -0,0 +1,122 @@ +// fpng.h - unlicense (see end of fpng.cpp) +#pragma once + +#include +#include +#include + +#ifndef FPNG_TRAIN_HUFFMAN_TABLES + // Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder. + #define FPNG_TRAIN_HUFFMAN_TABLES (0) +#endif + +namespace fpng +{ + // ---- Library initialization - call once to identify if the processor supports SSE. + // Otherwise you'll only get scalar fallbacks. + void fpng_init(); + + // ---- Useful Utilities + + // Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1. + // fpng_init() must have been called first, or it'll assert and return false. + bool fpng_cpu_supports_sse41(); + + // Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4) + const uint32_t FPNG_CRC32_INIT = 0; + uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT); + + // Fast Adler32 SSE4.1 Adler-32 with a scalar fallback. + const uint32_t FPNG_ADLER32_INIT = 1; + uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT); + + // ---- Compression + enum + { + // Enables computing custom Huffman tables for each file, instead of using the custom global tables. + // Results in roughly 6% smaller files on average, but compression is around 40% slower. + FPNG_ENCODE_SLOWER = 1, + + // Only use raw Deflate blocks (no compression at all). Intended for testing. + FPNG_FORCE_UNCOMPRESSED = 2, + }; + + // Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below. + // pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last. + // w/h - image dimensions. Image's row pitch in bytes must is w*num_chans. + // num_chans must be 3 or 4. + bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector& out_buf, uint32_t flags = 0); + +#ifndef FPNG_NO_STDIO + // Fast PNG encoding to the specified file. + bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0); +#endif + + // ---- Decompression + + enum + { + FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded + + FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder + + FPNG_DECODE_INVALID_ARG, // invalid function parameter + + FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file + FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG + FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large) + FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds) + FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted + FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug + + // fpng_decode_file() specific errors + FPNG_DECODE_FILE_OPEN_FAILED, + FPNG_DECODE_FILE_TOO_LARGE, + FPNG_DECODE_FILE_READ_FAILED, + FPNG_DECODE_FILE_SEEK_FAILED + }; + + // Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file(). + // If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder. + // + // fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast. + // + // pImage, image_size: Pointer to PNG image data and its size + // width, height: output image's dimensions + // channels_in_file: will be 3 or 4 + // + // Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above. + // If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder. + // If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail). + int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file); + + // fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module. + // If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.) + // + // pImage, image_size: Pointer to PNG image data and its size + // out: Output 24/32bpp image buffer + // width, height: output image's dimensions + // channels_in_file: will be 3 or 4 + // desired_channels: must be 3 or 4 + // + // If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF. + // If the image is 32bpp and 24bpp is requested, the alpha values will be discarded. + // + // Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above. + // If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder. + // If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail). + int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels); + +#ifndef FPNG_NO_STDIO + int fpng_decode_file(const char* pFilename, std::vector& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels); +#endif + + // ---- Internal API used for Huffman table training purposes + +#if FPNG_TRAIN_HUFFMAN_TABLES + const uint32_t HUFF_COUNTS_SIZE = 288; + extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE]; + bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes); +#endif + +} // namespace fpng diff --git a/src/spice2x/external/imgui/imgui_memory_editor.h b/src/spice2x/external/imgui/imgui_memory_editor.h index 8819ef3..57c4e52 100644 --- a/src/spice2x/external/imgui/imgui_memory_editor.h +++ b/src/spice2x/external/imgui/imgui_memory_editor.h @@ -1,836 +1,836 @@ -// Mini memory editor for Dear ImGui (to embed in your game/tools) -// Get latest version at http://www.github.com/ocornut/imgui_club -// Licensed under The MIT License (MIT) - -// Right-click anywhere to access the Options menu! -// You can adjust the keyboard repeat delay/rate in ImGuiIO. -// The code assume a mono-space font for simplicity! -// If you don't use the default font, use ImGui::PushFont()/PopFont() to switch to a mono-space font before calling this. -// -// Usage: -// // Create a window and draw memory editor inside it: -// static MemoryEditor mem_edit_1; -// static char data[0x10000]; -// size_t data_size = 0x10000; -// mem_edit_1.DrawWindow("Memory Editor", data, data_size); -// -// Usage: -// // If you already have a window, use DrawContents() instead: -// static MemoryEditor mem_edit_2; -// ImGui::Begin("MyWindow") -// mem_edit_2.DrawContents(this, sizeof(*this), (size_t)this); -// ImGui::End(); -// -// Changelog: -// - v0.10: initial version -// - v0.23 (2017/08/17): added to github. fixed right-arrow triggering a byte write. -// - v0.24 (2018/06/02): changed DragInt("Rows" to use a %d data format (which is desirable since imgui 1.61). -// - v0.25 (2018/07/11): fixed wording: all occurrences of "Rows" renamed to "Columns". -// - v0.26 (2018/08/02): fixed clicking on hex region -// - v0.30 (2018/08/02): added data preview for common data types -// - v0.31 (2018/10/10): added OptUpperCaseHex option to select lower/upper casing display [@samhocevar] -// - v0.32 (2018/10/10): changed signatures to use void* instead of unsigned char* -// - v0.33 (2018/10/10): added OptShowOptions option to hide all the interactive option setting. -// - v0.34 (2019/05/07): binary preview now applies endianness setting [@nicolasnoble] -// - v0.35 (2020/01/29): using ImGuiDataType available since Dear ImGui 1.69. -// - v0.36 (2020/05/05): minor tweaks, minor refactor. -// - v0.40 (2020/10/04): fix misuse of ImGuiListClipper API, broke with Dear ImGui 1.79. made cursor position appears on left-side of edit box. option popup appears on mouse release. fix MSVC warnings where _CRT_SECURE_NO_WARNINGS wasn't working in recent versions. -// - v0.41 (2020/10/05): fix when using with keyboard/gamepad navigation enabled. -// - v0.42 (2020/10/14): fix for . character in ASCII view always being greyed out. -// - v0.43 (2021/03/12): added OptFooterExtraHeight to allow for custom drawing at the bottom of the editor [@leiradel] -// - v0.44 (2021/03/12): use ImGuiInputTextFlags_AlwaysOverwrite in 1.82 + fix hardcoded width. -// - v0.50 (2021/11/12): various fixes for recent dear imgui versions (fixed misuse of clipper, relying on SetKeyboardFocusHere() handling scrolling from 1.85). added default size. -// - v0.51 (2024/02/22): fix for layout change in 1.89 when using IMGUI_DISABLE_OBSOLETE_FUNCTIONS. (#34) -// - v0.52 (2024/03/08): removed unnecessary GetKeyIndex() calls, they are a no-op since 1.87. -// - v0.53 (2024/05/27): fixed right-click popup from not appearing when using DrawContents(). warning fixes. (#35) -// - v0.54 (2024/07/29): allow ReadOnly mode to still select and preview data. (#46) [@DeltaGW2]) -// - v0.55 (2024/08/19): added BgColorFn to allow setting background colors independently from highlighted selection. (#27) [@StrikerX3] -// added MouseHoveredAddr public readable field. (#47, #27) [@StrikerX3] -// fixed a data preview crash with 1.91.0 WIP. fixed contiguous highlight color when using data preview. -// *BREAKING* added UserData field passed to all optional function handlers: ReadFn, WriteFn, HighlightFn, BgColorFn. (#50) [@silverweed] -// - v0.56 (2024/11/04): fixed MouseHovered, MouseHoveredAddr not being set when hovering a byte being edited. (#54) -// - v0.57 (2025/03/26): fixed warnings. using ImGui's ImSXX/ImUXX types instead of e.g. int32_t/uint32_t. (#56) -// - v0.58 (2025/03/31): fixed extraneous footer spacing (added in 0.51) breaking vertical auto-resize. (#53) -// - v0.59 (2025/04/08): fixed GotoAddrAndHighlight() not working if OptShowOptions is disabled. -// -// TODO: -// - This is generally old/crappy code, it should work but isn't very good.. to be rewritten some day. -// - PageUp/PageDown are not supported because we use _NoNav. This is a good test scenario for working out idioms of how to mix natural nav and our own... -// - Arrows are being sent to the InputText() about to disappear which for LeftArrow makes the text cursor appear at position 1 for one frame. -// - Using InputText() is awkward and maybe overkill here, consider implementing something custom. - -#pragma once - -#include // sprintf, scanf -#include // uint8_t, etc. - -#if defined(_MSC_VER) && !defined(snprintf) -#define ImSnprintf _snprintf -#else -#define ImSnprintf snprintf -#endif -#if defined(_MSC_VER) && !defined(__clang__) -#define _PRISizeT "I" -#else -#define _PRISizeT "z" -#endif - -#if defined(_MSC_VER) || defined(_UCRT) -#pragma warning (push) -#pragma warning (disable: 4996) // warning C4996: 'sprintf': This function or variable may be unsafe. -#endif - -struct MemoryEditor -{ - enum DataFormat - { - DataFormat_Bin = 0, - DataFormat_Dec = 1, - DataFormat_Hex = 2, - DataFormat_COUNT - }; - - // Settings - bool Open; // = true // set to false when DrawWindow() was closed. ignore if not using DrawWindow(). - bool ReadOnly; // = false // disable any editing. - int Cols; // = 16 // number of columns to display. - bool OptShowOptions; // = true // display options button/context menu. when disabled, options will be locked unless you provide your own UI for them. - bool OptShowDataPreview; // = false // display a footer previewing the decimal/binary/hex/float representation of the currently selected bytes. - bool OptShowHexII; // = false // display values in HexII representation instead of regular hexadecimal: hide null/zero bytes, ascii values as ".X". - bool OptShowAscii; // = true // display ASCII representation on the right side. - bool OptGreyOutZeroes; // = true // display null/zero bytes using the TextDisabled color. - bool OptUpperCaseHex; // = true // display hexadecimal values as "FF" instead of "ff". - int OptMidColsCount; // = 8 // set to 0 to disable extra spacing between every mid-cols. - int OptAddrDigitsCount; // = 0 // number of addr digits to display (default calculated based on maximum displayed addr). - float OptFooterExtraHeight; // = 0 // space to reserve at the bottom of the widget to add custom widgets - ImU32 HighlightColor; // // background color of highlighted bytes. - - // Function handlers - ImU8 (*ReadFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to read bytes. - void (*WriteFn)(ImU8* mem, size_t off, ImU8 d, void* user_data); // = 0 // optional handler to write bytes. - bool (*HighlightFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return Highlight property (to support non-contiguous highlighting). - ImU32 (*BgColorFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return custom background color of individual bytes. - void* UserData; // = NULL // user data forwarded to the function handlers - - // Public read-only data - bool MouseHovered; // set when mouse is hovering a value. - size_t MouseHoveredAddr; // the address currently being hovered if MouseHovered is set. - - // [Internal State] - bool ContentsWidthChanged; - size_t DataPreviewAddr; - size_t DataEditingAddr; - bool DataEditingTakeFocus; - char DataInputBuf[32]; - char AddrInputBuf[32]; - size_t GotoAddr; - size_t HighlightMin, HighlightMax; - int PreviewEndianness; - ImGuiDataType PreviewDataType; - - MemoryEditor() - { - // Settings - Open = true; - ReadOnly = false; - Cols = 16; - OptShowOptions = true; - OptShowDataPreview = false; - OptShowHexII = false; - OptShowAscii = true; - OptGreyOutZeroes = true; - OptUpperCaseHex = true; - OptMidColsCount = 8; - OptAddrDigitsCount = 0; - OptFooterExtraHeight = 0.0f; - HighlightColor = IM_COL32(255, 255, 255, 50); - ReadFn = nullptr; - WriteFn = nullptr; - HighlightFn = nullptr; - BgColorFn = nullptr; - UserData = nullptr; - - // State/Internals - ContentsWidthChanged = false; - DataPreviewAddr = DataEditingAddr = (size_t)-1; - DataEditingTakeFocus = false; - memset(DataInputBuf, 0, sizeof(DataInputBuf)); - memset(AddrInputBuf, 0, sizeof(AddrInputBuf)); - GotoAddr = (size_t)-1; - MouseHovered = false; - MouseHoveredAddr = 0; - HighlightMin = HighlightMax = (size_t)-1; - PreviewEndianness = 0; - PreviewDataType = ImGuiDataType_S32; - } - - void GotoAddrAndHighlight(size_t addr_min, size_t addr_max) - { - GotoAddr = addr_min; - HighlightMin = addr_min; - HighlightMax = addr_max; - } - - struct Sizes - { - int AddrDigitsCount; // Number of digits required to represent maximum address. - float LineHeight; // Height of each line (no spacing). - float GlyphWidth; // Glyph width (assume mono-space). - float HexCellWidth; // Width of a hex edit cell ~2.5f * GlypHWidth. - float SpacingBetweenMidCols; // Spacing between each columns section (OptMidColsCount). - float OffsetHexMinX; - float OffsetHexMaxX; - float OffsetAsciiMinX; - float OffsetAsciiMaxX; - float WindowWidth; // Ideal window width. - - Sizes() { memset(this, 0, sizeof(*this)); } - }; - - void CalcSizes(Sizes& s, size_t mem_size, size_t base_display_addr) - { - ImGuiStyle& style = ImGui::GetStyle(); - s.AddrDigitsCount = OptAddrDigitsCount; - if (s.AddrDigitsCount == 0) - for (size_t n = base_display_addr + mem_size - 1; n > 0; n >>= 4) - s.AddrDigitsCount++; - s.LineHeight = ImGui::GetTextLineHeight(); - s.GlyphWidth = ImGui::CalcTextSize("F").x + 1; // We assume the font is mono-space - s.HexCellWidth = (float)(int)(s.GlyphWidth * 2.5f); // "FF " we include trailing space in the width to easily catch clicks everywhere - s.SpacingBetweenMidCols = (float)(int)(s.HexCellWidth * 0.25f); // Every OptMidColsCount columns we add a bit of extra spacing - s.OffsetHexMinX = (s.AddrDigitsCount + 2) * s.GlyphWidth; - s.OffsetHexMaxX = s.OffsetHexMinX + (s.HexCellWidth * Cols); - s.OffsetAsciiMinX = s.OffsetAsciiMaxX = s.OffsetHexMaxX; - if (OptShowAscii) - { - s.OffsetAsciiMinX = s.OffsetHexMaxX + s.GlyphWidth * 1; - if (OptMidColsCount > 0) - s.OffsetAsciiMinX += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; - s.OffsetAsciiMaxX = s.OffsetAsciiMinX + Cols * s.GlyphWidth; - } - s.WindowWidth = s.OffsetAsciiMaxX + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth; - } - - // Standalone Memory Editor window - void DrawWindow(const char* title, void* mem_data, size_t mem_size, size_t base_display_addr = 0x0000) - { - Sizes s; - CalcSizes(s, mem_size, base_display_addr); - ImGui::SetNextWindowSize(ImVec2(s.WindowWidth, s.WindowWidth * 0.60f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(s.WindowWidth, FLT_MAX)); - - Open = true; - if (ImGui::Begin(title, &Open, ImGuiWindowFlags_NoScrollbar)) - { - DrawContents(mem_data, mem_size, base_display_addr); - if (ContentsWidthChanged) - { - CalcSizes(s, mem_size, base_display_addr); - ImGui::SetWindowSize(ImVec2(s.WindowWidth, ImGui::GetWindowSize().y)); - } - } - ImGui::End(); - } - - // Memory Editor contents only - void DrawContents(void* mem_data_void, size_t mem_size, size_t base_display_addr = 0x0000) - { - if (Cols < 1) - Cols = 1; - - ImU8* mem_data = (ImU8*)mem_data_void; - Sizes s; - CalcSizes(s, mem_size, base_display_addr); - ImGuiStyle& style = ImGui::GetStyle(); - - const ImVec2 contents_pos_start = ImGui::GetCursorScreenPos(); - - // We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in order to prevent click from moving the window. - // This is used as a facility since our main click detection code doesn't assign an ActiveId so the click would normally be caught as a window-move. - const float height_separator = style.ItemSpacing.y; - float footer_height = OptFooterExtraHeight; - if (OptShowOptions) - footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1; - if (OptShowDataPreview) - footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1 + ImGui::GetTextLineHeightWithSpacing() * 3; - ImGui::BeginChild("##scrolling", ImVec2(-FLT_MIN, -footer_height), ImGuiChildFlags_None, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav); - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); - - // We are not really using the clipper API correctly here, because we rely on visible_start_addr/visible_end_addr for our scrolling function. - const ImVec2 avail_size = ImGui::GetContentRegionAvail(); - const int line_total_count = (int)((mem_size + Cols - 1) / Cols); - ImGuiListClipper clipper; - clipper.Begin(line_total_count, s.LineHeight); - - bool data_next = false; - - if (DataEditingAddr >= mem_size) - DataEditingAddr = (size_t)-1; - if (DataPreviewAddr >= mem_size) - DataPreviewAddr = (size_t)-1; - - size_t preview_data_type_size = OptShowDataPreview ? DataTypeGetSize(PreviewDataType) : 0; - - size_t data_editing_addr_next = (size_t)-1; - if (DataEditingAddr != (size_t)-1) - { - // Move cursor but only apply on next frame so scrolling with be synchronized (because currently we can't change the scrolling while the window is being rendered) - if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && (ptrdiff_t)DataEditingAddr >= (ptrdiff_t)Cols) { data_editing_addr_next = DataEditingAddr - Cols; } - else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - Cols){ data_editing_addr_next = DataEditingAddr + Cols; } - else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && (ptrdiff_t)DataEditingAddr > (ptrdiff_t)0) { data_editing_addr_next = DataEditingAddr - 1; } - else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - 1) { data_editing_addr_next = DataEditingAddr + 1; } - } - - // Draw vertical separator - ImVec2 window_pos = ImGui::GetWindowPos(); - if (OptShowAscii) - draw_list->AddLine(ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border)); - - const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text); - const ImU32 color_disabled = OptGreyOutZeroes ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : color_text; - - const char* format_address = OptUpperCaseHex ? "%0*" _PRISizeT "X: " : "%0*" _PRISizeT "x: "; - const char* format_data = OptUpperCaseHex ? "%0*" _PRISizeT "X" : "%0*" _PRISizeT "x"; - const char* format_byte = OptUpperCaseHex ? "%02X" : "%02x"; - const char* format_byte_space = OptUpperCaseHex ? "%02X " : "%02x "; - - MouseHovered = false; - MouseHoveredAddr = 0; - - while (clipper.Step()) - for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++) // display only visible lines - { - size_t addr = (size_t)line_i * Cols; - ImGui::Text(format_address, s.AddrDigitsCount, base_display_addr + addr); - - // Draw Hexadecimal - for (int n = 0; n < Cols && addr < mem_size; n++, addr++) - { - float byte_pos_x = s.OffsetHexMinX + s.HexCellWidth * n; - if (OptMidColsCount > 0) - byte_pos_x += (float)(n / OptMidColsCount) * s.SpacingBetweenMidCols; - ImGui::SameLine(byte_pos_x); - - // Draw highlight or custom background color - const bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax); - const bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr, UserData)); - const bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr < DataPreviewAddr + preview_data_type_size); - - ImU32 bg_color = 0; - bool is_next_byte_highlighted = false; - if (is_highlight_from_user_range || is_highlight_from_user_func || is_highlight_from_preview) - { - is_next_byte_highlighted = (addr + 1 < mem_size) && ((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) || (HighlightFn && HighlightFn(mem_data, addr + 1, UserData)) || (addr + 1 < DataPreviewAddr + preview_data_type_size)); - bg_color = HighlightColor; - } - else if (BgColorFn != nullptr) - { - is_next_byte_highlighted = (addr + 1 < mem_size) && ((BgColorFn(mem_data, addr + 1, UserData) & IM_COL32_A_MASK) != 0); - bg_color = BgColorFn(mem_data, addr, UserData); - } - if (bg_color != 0) - { - float bg_width = s.GlyphWidth * 2; - if (is_next_byte_highlighted || (n + 1 == Cols)) - { - bg_width = s.HexCellWidth; - if (OptMidColsCount > 0 && n > 0 && (n + 1) < Cols && ((n + 1) % OptMidColsCount) == 0) - bg_width += s.SpacingBetweenMidCols; - } - ImVec2 pos = ImGui::GetCursorScreenPos(); - draw_list->AddRectFilled(pos, ImVec2(pos.x + bg_width, pos.y + s.LineHeight), bg_color); - } - - if (DataEditingAddr == addr) - { - // Display text input on current byte - bool data_write = false; - ImGui::PushID((void*)addr); - if (DataEditingTakeFocus) - { - ImGui::SetKeyboardFocusHere(0); - ImSnprintf(AddrInputBuf, 32, format_data, s.AddrDigitsCount, base_display_addr + addr); - ImSnprintf(DataInputBuf, 32, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]); - } - struct InputTextUserData - { - // FIXME: We should have a way to retrieve the text edit cursor position more easily in the API, this is rather tedious. This is such a ugly mess we may be better off not using InputText() at all here. - static int Callback(ImGuiInputTextCallbackData* data) - { - InputTextUserData* user_data = (InputTextUserData*)data->UserData; - if (!data->HasSelection()) - user_data->CursorPos = data->CursorPos; -#if IMGUI_VERSION_NUM < 19102 - if (data->Flags & ImGuiInputTextFlags_ReadOnly) - return 0; -#endif - if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) - { - // When not editing a byte, always refresh its InputText content pulled from underlying memory data - // (this is a bit tricky, since InputText technically "owns" the master copy of the buffer we edit it in there) - data->DeleteChars(0, data->BufTextLen); - data->InsertChars(0, user_data->CurrentBufOverwrite); - data->SelectionStart = 0; - data->SelectionEnd = 2; - data->CursorPos = 0; - } - return 0; - } - char CurrentBufOverwrite[3]; // Input - int CursorPos; // Output - }; - InputTextUserData input_text_user_data; - input_text_user_data.CursorPos = -1; - ImSnprintf(input_text_user_data.CurrentBufOverwrite, 3, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]); - ImGuiInputTextFlags flags = ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoHorizontalScroll | ImGuiInputTextFlags_CallbackAlways; - if (ReadOnly) - flags |= ImGuiInputTextFlags_ReadOnly; - flags |= ImGuiInputTextFlags_AlwaysOverwrite; // was ImGuiInputTextFlags_AlwaysInsertMode - ImGui::SetNextItemWidth(s.GlyphWidth * 2); - if (ImGui::InputText("##data", DataInputBuf, IM_ARRAYSIZE(DataInputBuf), flags, InputTextUserData::Callback, &input_text_user_data)) - data_write = data_next = true; - else if (!DataEditingTakeFocus && !ImGui::IsItemActive()) - DataEditingAddr = data_editing_addr_next = (size_t)-1; - DataEditingTakeFocus = false; - if (input_text_user_data.CursorPos >= 2) - data_write = data_next = true; - if (data_editing_addr_next != (size_t)-1) - data_write = data_next = false; - unsigned int data_input_value = 0; - if (!ReadOnly && data_write && sscanf(DataInputBuf, "%X", &data_input_value) == 1) - { - if (WriteFn) - WriteFn(mem_data, addr, (ImU8)data_input_value, UserData); - else - mem_data[addr] = (ImU8)data_input_value; - } - if (ImGui::IsItemHovered()) - { - MouseHovered = true; - MouseHoveredAddr = addr; - } - ImGui::PopID(); - } - else - { - // NB: The trailing space is not visible but ensure there's no gap that the mouse cannot click on. - ImU8 b = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]; - - if (OptShowHexII) - { - if ((b >= 32 && b < 128)) - ImGui::Text(".%c ", b); - else if (b == 0xFF && OptGreyOutZeroes) - ImGui::TextDisabled("## "); - else if (b == 0x00) - ImGui::Text(" "); - else - ImGui::Text(format_byte_space, b); - } - else - { - if (b == 0 && OptGreyOutZeroes) - ImGui::TextDisabled("00 "); - else - ImGui::Text(format_byte_space, b); - } - if (ImGui::IsItemHovered()) - { - MouseHovered = true; - MouseHoveredAddr = addr; - if (ImGui::IsMouseClicked(0)) - { - DataEditingTakeFocus = true; - data_editing_addr_next = addr; - } - } - } - } - - if (OptShowAscii) - { - // Draw ASCII values - ImGui::SameLine(s.OffsetAsciiMinX); - ImVec2 pos = ImGui::GetCursorScreenPos(); - addr = (size_t)line_i * Cols; - - const float mouse_off_x = ImGui::GetIO().MousePos.x - pos.x; - const size_t mouse_addr = (mouse_off_x >= 0.0f && mouse_off_x < s.OffsetAsciiMaxX - s.OffsetAsciiMinX) ? addr + (size_t)(mouse_off_x / s.GlyphWidth) : (size_t)-1; - - ImGui::PushID(line_i); - if (ImGui::InvisibleButton("ascii", ImVec2(s.OffsetAsciiMaxX - s.OffsetAsciiMinX, s.LineHeight))) - { - DataEditingAddr = DataPreviewAddr = mouse_addr; - DataEditingTakeFocus = true; - } - if (ImGui::IsItemHovered()) - { - MouseHovered = true; - MouseHoveredAddr = mouse_addr; - } - ImGui::PopID(); - for (int n = 0; n < Cols && addr < mem_size; n++, addr++) - { - if (addr == DataEditingAddr) - { - draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_FrameBg)); - draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_TextSelectedBg)); - } - else if (BgColorFn) - { - draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), BgColorFn(mem_data, addr, UserData)); - } - unsigned char c = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]; - char display_c = (c < 32 || c >= 128) ? '.' : c; - draw_list->AddText(pos, (display_c == c) ? color_text : color_disabled, &display_c, &display_c + 1); - pos.x += s.GlyphWidth; - } - } - } - ImGui::PopStyleVar(2); - const float child_width = ImGui::GetWindowSize().x; - ImGui::EndChild(); - - // Notify the main window of our ideal child content size (FIXME: we are missing an API to get the contents size from the child) - ImVec2 backup_pos = ImGui::GetCursorScreenPos(); - ImGui::SetCursorPosX(s.WindowWidth); - ImGui::Dummy(ImVec2(0.0f, 0.0f)); - ImGui::SetCursorScreenPos(backup_pos); - - if (data_next && DataEditingAddr + 1 < mem_size) - { - DataEditingAddr = DataPreviewAddr = DataEditingAddr + 1; - DataEditingTakeFocus = true; - } - else if (data_editing_addr_next != (size_t)-1) - { - DataEditingAddr = DataPreviewAddr = data_editing_addr_next; - DataEditingTakeFocus = true; - } - - const bool lock_show_data_preview = OptShowDataPreview; - if (OptShowOptions) - { - ImGui::Separator(); - DrawOptionsLine(s, mem_data, mem_size, base_display_addr); - } - - if (lock_show_data_preview) - { - ImGui::Separator(); - DrawPreviewLine(s, mem_data, mem_size, base_display_addr); - } - - if (GotoAddr != (size_t)-1) - { - if (GotoAddr < mem_size) - { - ImGui::BeginChild("##scrolling"); - ImGui::SetScrollY((GotoAddr / Cols) * ImGui::GetTextLineHeight() - avail_size.y * 0.5f); - ImGui::EndChild(); - DataEditingAddr = DataPreviewAddr = GotoAddr; - DataEditingTakeFocus = true; - } - GotoAddr = (size_t)-1; - } - - const ImVec2 contents_pos_end(contents_pos_start.x + child_width, ImGui::GetCursorScreenPos().y); - //ImGui::GetForegroundDrawList()->AddRect(contents_pos_start, contents_pos_end, IM_COL32(255, 0, 0, 255)); - if (OptShowOptions) - if (ImGui::IsMouseHoveringRect(contents_pos_start, contents_pos_end)) - if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) - ImGui::OpenPopup("OptionsPopup"); - - if (ImGui::BeginPopup("OptionsPopup")) - { - ImGui::SetNextItemWidth(s.GlyphWidth * 7 + style.FramePadding.x * 2.0f); - if (ImGui::DragInt("##cols", &Cols, 0.2f, 4, 32, "%d cols")) { ContentsWidthChanged = true; if (Cols < 1) Cols = 1; } - ImGui::Checkbox("Show Data Preview", &OptShowDataPreview); - ImGui::Checkbox("Show HexII", &OptShowHexII); - if (ImGui::Checkbox("Show Ascii", &OptShowAscii)) { ContentsWidthChanged = true; } - ImGui::Checkbox("Grey out zeroes", &OptGreyOutZeroes); - ImGui::Checkbox("Uppercase Hex", &OptUpperCaseHex); - - ImGui::EndPopup(); - } - } - - void DrawOptionsLine(const Sizes& s, void* mem_data, size_t mem_size, size_t base_display_addr) - { - IM_UNUSED(mem_data); - ImGuiStyle& style = ImGui::GetStyle(); - const char* format_range = OptUpperCaseHex ? "Range %0*" _PRISizeT "X..%0*" _PRISizeT "X" : "Range %0*" _PRISizeT "x..%0*" _PRISizeT "x"; - - // Options menu - if (ImGui::Button("Options")) - ImGui::OpenPopup("OptionsPopup"); - - ImGui::SameLine(); - ImGui::Text(format_range, s.AddrDigitsCount, base_display_addr, s.AddrDigitsCount, base_display_addr + mem_size - 1); - ImGui::SameLine(); - ImGui::SetNextItemWidth((s.AddrDigitsCount + 1) * s.GlyphWidth + style.FramePadding.x * 2.0f); - if (ImGui::InputText("##addr", AddrInputBuf, IM_ARRAYSIZE(AddrInputBuf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue)) - { - size_t goto_addr; - if (sscanf(AddrInputBuf, "%" _PRISizeT "X", &goto_addr) == 1) - { - GotoAddr = goto_addr - base_display_addr; - HighlightMin = HighlightMax = (size_t)-1; - } - } - - //if (MouseHovered) - //{ - // ImGui::SameLine(); - // ImGui::Text("Hovered: %p", MouseHoveredAddr); - //} - } - - void DrawPreviewLine(const Sizes& s, void* mem_data_void, size_t mem_size, size_t base_display_addr) - { - IM_UNUSED(base_display_addr); - ImU8* mem_data = (ImU8*)mem_data_void; - ImGuiStyle& style = ImGui::GetStyle(); - ImGui::AlignTextToFramePadding(); - ImGui::Text("Preview as:"); - ImGui::SameLine(); - ImGui::SetNextItemWidth((s.GlyphWidth * 10.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x); - - static const ImGuiDataType supported_data_types[] = { ImGuiDataType_S8, ImGuiDataType_U8, ImGuiDataType_S16, ImGuiDataType_U16, ImGuiDataType_S32, ImGuiDataType_U32, ImGuiDataType_S64, ImGuiDataType_U64, ImGuiDataType_Float, ImGuiDataType_Double }; - if (ImGui::BeginCombo("##combo_type", DataTypeGetDesc(PreviewDataType), ImGuiComboFlags_HeightLargest)) - { - for (int n = 0; n < IM_ARRAYSIZE(supported_data_types); n++) - { - ImGuiDataType data_type = supported_data_types[n]; - if (ImGui::Selectable(DataTypeGetDesc(data_type), PreviewDataType == data_type)) - PreviewDataType = data_type; - } - ImGui::EndCombo(); - } - ImGui::SameLine(); - ImGui::SetNextItemWidth((s.GlyphWidth * 6.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x); - ImGui::Combo("##combo_endianness", &PreviewEndianness, "LE\0BE\0\0"); - - char buf[128] = ""; - float x = s.GlyphWidth * 6.0f; - bool has_value = DataPreviewAddr != (size_t)-1; - if (has_value) - DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Dec, buf, (size_t)IM_ARRAYSIZE(buf)); - ImGui::Text("Dec"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); - if (has_value) - DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Hex, buf, (size_t)IM_ARRAYSIZE(buf)); - ImGui::Text("Hex"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); - if (has_value) - DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Bin, buf, (size_t)IM_ARRAYSIZE(buf)); - buf[IM_ARRAYSIZE(buf) - 1] = 0; - ImGui::Text("Bin"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); - } - - // Utilities for Data Preview (since we don't access imgui_internal.h) - // FIXME: This technically depends on ImGuiDataType order. - const char* DataTypeGetDesc(ImGuiDataType data_type) const - { - const char* descs[] = { "Int8", "Uint8", "Int16", "Uint16", "Int32", "Uint32", "Int64", "Uint64", "Float", "Double" }; - IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(descs)); - return descs[data_type]; - } - - size_t DataTypeGetSize(ImGuiDataType data_type) const - { - const size_t sizes[] = { 1, 1, 2, 2, 4, 4, 8, 8, sizeof(float), sizeof(double) }; - IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(sizes)); - return sizes[data_type]; - } - - const char* DataFormatGetDesc(DataFormat data_format) const - { - const char* descs[] = { "Bin", "Dec", "Hex" }; - IM_ASSERT(data_format >= 0 && data_format < DataFormat_COUNT); - return descs[data_format]; - } - - bool IsBigEndian() const - { - ImU16 x = 1; - char c[2]; - memcpy(c, &x, 2); - return c[0] != 0; - } - - static void* EndiannessCopyBigEndian(void* _dst, void* _src, size_t s, int is_little_endian) - { - if (is_little_endian) - { - ImU8* dst = (ImU8*)_dst; - ImU8* src = (ImU8*)_src + s - 1; - for (int i = 0, n = (int)s; i < n; ++i) - memcpy(dst++, src--, 1); - return _dst; - } - else - { - return memcpy(_dst, _src, s); - } - } - - static void* EndiannessCopyLittleEndian(void* _dst, void* _src, size_t s, int is_little_endian) - { - if (is_little_endian) - { - return memcpy(_dst, _src, s); - } - else - { - ImU8* dst = (ImU8*)_dst; - ImU8* src = (ImU8*)_src + s - 1; - for (int i = 0, n = (int)s; i < n; ++i) - memcpy(dst++, src--, 1); - return _dst; - } - } - - void* EndiannessCopy(void* dst, void* src, size_t size) const - { - static void* (*fp)(void*, void*, size_t, int) = nullptr; - if (fp == nullptr) - fp = IsBigEndian() ? EndiannessCopyBigEndian : EndiannessCopyLittleEndian; - return fp(dst, src, size, PreviewEndianness); - } - - const char* FormatBinary(const ImU8* buf, int width) const - { - IM_ASSERT(width <= 64); - size_t out_n = 0; - static char out_buf[64 + 8 + 1]; - int n = width / 8; - for (int j = n - 1; j >= 0; --j) - { - for (int i = 0; i < 8; ++i) - out_buf[out_n++] = (buf[j] & (1 << (7 - i))) ? '1' : '0'; - out_buf[out_n++] = ' '; - } - IM_ASSERT(out_n < IM_ARRAYSIZE(out_buf)); - out_buf[out_n] = 0; - return out_buf; - } - - // [Internal] - void DrawPreviewData(size_t addr, const ImU8* mem_data, size_t mem_size, ImGuiDataType data_type, DataFormat data_format, char* out_buf, size_t out_buf_size) const - { - ImU8 buf[8]; - size_t elem_size = DataTypeGetSize(data_type); - size_t size = addr + elem_size > mem_size ? mem_size - addr : elem_size; - if (ReadFn) - for (int i = 0, n = (int)size; i < n; ++i) - buf[i] = ReadFn(mem_data, addr + i, UserData); - else - memcpy(buf, mem_data + addr, size); - - if (data_format == DataFormat_Bin) - { - ImU8 binbuf[8]; - EndiannessCopy(binbuf, buf, size); - ImSnprintf(out_buf, out_buf_size, "%s", FormatBinary(binbuf, (int)size * 8)); - return; - } - - out_buf[0] = 0; - switch (data_type) - { - case ImGuiDataType_S8: - { - ImS8 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhd", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0xFF); return; } - break; - } - case ImGuiDataType_U8: - { - ImU8 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhu", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0XFF); return; } - break; - } - case ImGuiDataType_S16: - { - ImS16 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hd", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; } - break; - } - case ImGuiDataType_U16: - { - ImU16 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hu", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; } - break; - } - case ImGuiDataType_S32: - { - ImS32 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%d", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; } - break; - } - case ImGuiDataType_U32: - { - ImU32 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%u", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; } - break; - } - case ImGuiDataType_S64: - { - ImS64 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%lld", (long long)data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; } - break; - } - case ImGuiDataType_U64: - { - ImU64 data = 0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%llu", (long long)data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; } - break; - } - case ImGuiDataType_Float: - { - float data = 0.0f; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; } - break; - } - case ImGuiDataType_Double: - { - double data = 0.0; - EndiannessCopy(&data, buf, size); - if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; } - if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; } - break; - } - default: - case ImGuiDataType_COUNT: - break; - } // Switch - IM_ASSERT(0); // Shouldn't reach - } -}; - -#undef _PRISizeT -#undef ImSnprintf - -#ifdef _MSC_VER -#pragma warning (pop) +// Mini memory editor for Dear ImGui (to embed in your game/tools) +// Get latest version at http://www.github.com/ocornut/imgui_club +// Licensed under The MIT License (MIT) + +// Right-click anywhere to access the Options menu! +// You can adjust the keyboard repeat delay/rate in ImGuiIO. +// The code assume a mono-space font for simplicity! +// If you don't use the default font, use ImGui::PushFont()/PopFont() to switch to a mono-space font before calling this. +// +// Usage: +// // Create a window and draw memory editor inside it: +// static MemoryEditor mem_edit_1; +// static char data[0x10000]; +// size_t data_size = 0x10000; +// mem_edit_1.DrawWindow("Memory Editor", data, data_size); +// +// Usage: +// // If you already have a window, use DrawContents() instead: +// static MemoryEditor mem_edit_2; +// ImGui::Begin("MyWindow") +// mem_edit_2.DrawContents(this, sizeof(*this), (size_t)this); +// ImGui::End(); +// +// Changelog: +// - v0.10: initial version +// - v0.23 (2017/08/17): added to github. fixed right-arrow triggering a byte write. +// - v0.24 (2018/06/02): changed DragInt("Rows" to use a %d data format (which is desirable since imgui 1.61). +// - v0.25 (2018/07/11): fixed wording: all occurrences of "Rows" renamed to "Columns". +// - v0.26 (2018/08/02): fixed clicking on hex region +// - v0.30 (2018/08/02): added data preview for common data types +// - v0.31 (2018/10/10): added OptUpperCaseHex option to select lower/upper casing display [@samhocevar] +// - v0.32 (2018/10/10): changed signatures to use void* instead of unsigned char* +// - v0.33 (2018/10/10): added OptShowOptions option to hide all the interactive option setting. +// - v0.34 (2019/05/07): binary preview now applies endianness setting [@nicolasnoble] +// - v0.35 (2020/01/29): using ImGuiDataType available since Dear ImGui 1.69. +// - v0.36 (2020/05/05): minor tweaks, minor refactor. +// - v0.40 (2020/10/04): fix misuse of ImGuiListClipper API, broke with Dear ImGui 1.79. made cursor position appears on left-side of edit box. option popup appears on mouse release. fix MSVC warnings where _CRT_SECURE_NO_WARNINGS wasn't working in recent versions. +// - v0.41 (2020/10/05): fix when using with keyboard/gamepad navigation enabled. +// - v0.42 (2020/10/14): fix for . character in ASCII view always being greyed out. +// - v0.43 (2021/03/12): added OptFooterExtraHeight to allow for custom drawing at the bottom of the editor [@leiradel] +// - v0.44 (2021/03/12): use ImGuiInputTextFlags_AlwaysOverwrite in 1.82 + fix hardcoded width. +// - v0.50 (2021/11/12): various fixes for recent dear imgui versions (fixed misuse of clipper, relying on SetKeyboardFocusHere() handling scrolling from 1.85). added default size. +// - v0.51 (2024/02/22): fix for layout change in 1.89 when using IMGUI_DISABLE_OBSOLETE_FUNCTIONS. (#34) +// - v0.52 (2024/03/08): removed unnecessary GetKeyIndex() calls, they are a no-op since 1.87. +// - v0.53 (2024/05/27): fixed right-click popup from not appearing when using DrawContents(). warning fixes. (#35) +// - v0.54 (2024/07/29): allow ReadOnly mode to still select and preview data. (#46) [@DeltaGW2]) +// - v0.55 (2024/08/19): added BgColorFn to allow setting background colors independently from highlighted selection. (#27) [@StrikerX3] +// added MouseHoveredAddr public readable field. (#47, #27) [@StrikerX3] +// fixed a data preview crash with 1.91.0 WIP. fixed contiguous highlight color when using data preview. +// *BREAKING* added UserData field passed to all optional function handlers: ReadFn, WriteFn, HighlightFn, BgColorFn. (#50) [@silverweed] +// - v0.56 (2024/11/04): fixed MouseHovered, MouseHoveredAddr not being set when hovering a byte being edited. (#54) +// - v0.57 (2025/03/26): fixed warnings. using ImGui's ImSXX/ImUXX types instead of e.g. int32_t/uint32_t. (#56) +// - v0.58 (2025/03/31): fixed extraneous footer spacing (added in 0.51) breaking vertical auto-resize. (#53) +// - v0.59 (2025/04/08): fixed GotoAddrAndHighlight() not working if OptShowOptions is disabled. +// +// TODO: +// - This is generally old/crappy code, it should work but isn't very good.. to be rewritten some day. +// - PageUp/PageDown are not supported because we use _NoNav. This is a good test scenario for working out idioms of how to mix natural nav and our own... +// - Arrows are being sent to the InputText() about to disappear which for LeftArrow makes the text cursor appear at position 1 for one frame. +// - Using InputText() is awkward and maybe overkill here, consider implementing something custom. + +#pragma once + +#include // sprintf, scanf +#include // uint8_t, etc. + +#if defined(_MSC_VER) && !defined(snprintf) +#define ImSnprintf _snprintf +#else +#define ImSnprintf snprintf +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define _PRISizeT "I" +#else +#define _PRISizeT "z" +#endif + +#if defined(_MSC_VER) || defined(_UCRT) +#pragma warning (push) +#pragma warning (disable: 4996) // warning C4996: 'sprintf': This function or variable may be unsafe. +#endif + +struct MemoryEditor +{ + enum DataFormat + { + DataFormat_Bin = 0, + DataFormat_Dec = 1, + DataFormat_Hex = 2, + DataFormat_COUNT + }; + + // Settings + bool Open; // = true // set to false when DrawWindow() was closed. ignore if not using DrawWindow(). + bool ReadOnly; // = false // disable any editing. + int Cols; // = 16 // number of columns to display. + bool OptShowOptions; // = true // display options button/context menu. when disabled, options will be locked unless you provide your own UI for them. + bool OptShowDataPreview; // = false // display a footer previewing the decimal/binary/hex/float representation of the currently selected bytes. + bool OptShowHexII; // = false // display values in HexII representation instead of regular hexadecimal: hide null/zero bytes, ascii values as ".X". + bool OptShowAscii; // = true // display ASCII representation on the right side. + bool OptGreyOutZeroes; // = true // display null/zero bytes using the TextDisabled color. + bool OptUpperCaseHex; // = true // display hexadecimal values as "FF" instead of "ff". + int OptMidColsCount; // = 8 // set to 0 to disable extra spacing between every mid-cols. + int OptAddrDigitsCount; // = 0 // number of addr digits to display (default calculated based on maximum displayed addr). + float OptFooterExtraHeight; // = 0 // space to reserve at the bottom of the widget to add custom widgets + ImU32 HighlightColor; // // background color of highlighted bytes. + + // Function handlers + ImU8 (*ReadFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to read bytes. + void (*WriteFn)(ImU8* mem, size_t off, ImU8 d, void* user_data); // = 0 // optional handler to write bytes. + bool (*HighlightFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return Highlight property (to support non-contiguous highlighting). + ImU32 (*BgColorFn)(const ImU8* mem, size_t off, void* user_data); // = 0 // optional handler to return custom background color of individual bytes. + void* UserData; // = NULL // user data forwarded to the function handlers + + // Public read-only data + bool MouseHovered; // set when mouse is hovering a value. + size_t MouseHoveredAddr; // the address currently being hovered if MouseHovered is set. + + // [Internal State] + bool ContentsWidthChanged; + size_t DataPreviewAddr; + size_t DataEditingAddr; + bool DataEditingTakeFocus; + char DataInputBuf[32]; + char AddrInputBuf[32]; + size_t GotoAddr; + size_t HighlightMin, HighlightMax; + int PreviewEndianness; + ImGuiDataType PreviewDataType; + + MemoryEditor() + { + // Settings + Open = true; + ReadOnly = false; + Cols = 16; + OptShowOptions = true; + OptShowDataPreview = false; + OptShowHexII = false; + OptShowAscii = true; + OptGreyOutZeroes = true; + OptUpperCaseHex = true; + OptMidColsCount = 8; + OptAddrDigitsCount = 0; + OptFooterExtraHeight = 0.0f; + HighlightColor = IM_COL32(255, 255, 255, 50); + ReadFn = nullptr; + WriteFn = nullptr; + HighlightFn = nullptr; + BgColorFn = nullptr; + UserData = nullptr; + + // State/Internals + ContentsWidthChanged = false; + DataPreviewAddr = DataEditingAddr = (size_t)-1; + DataEditingTakeFocus = false; + memset(DataInputBuf, 0, sizeof(DataInputBuf)); + memset(AddrInputBuf, 0, sizeof(AddrInputBuf)); + GotoAddr = (size_t)-1; + MouseHovered = false; + MouseHoveredAddr = 0; + HighlightMin = HighlightMax = (size_t)-1; + PreviewEndianness = 0; + PreviewDataType = ImGuiDataType_S32; + } + + void GotoAddrAndHighlight(size_t addr_min, size_t addr_max) + { + GotoAddr = addr_min; + HighlightMin = addr_min; + HighlightMax = addr_max; + } + + struct Sizes + { + int AddrDigitsCount; // Number of digits required to represent maximum address. + float LineHeight; // Height of each line (no spacing). + float GlyphWidth; // Glyph width (assume mono-space). + float HexCellWidth; // Width of a hex edit cell ~2.5f * GlypHWidth. + float SpacingBetweenMidCols; // Spacing between each columns section (OptMidColsCount). + float OffsetHexMinX; + float OffsetHexMaxX; + float OffsetAsciiMinX; + float OffsetAsciiMaxX; + float WindowWidth; // Ideal window width. + + Sizes() { memset(this, 0, sizeof(*this)); } + }; + + void CalcSizes(Sizes& s, size_t mem_size, size_t base_display_addr) + { + ImGuiStyle& style = ImGui::GetStyle(); + s.AddrDigitsCount = OptAddrDigitsCount; + if (s.AddrDigitsCount == 0) + for (size_t n = base_display_addr + mem_size - 1; n > 0; n >>= 4) + s.AddrDigitsCount++; + s.LineHeight = ImGui::GetTextLineHeight(); + s.GlyphWidth = ImGui::CalcTextSize("F").x + 1; // We assume the font is mono-space + s.HexCellWidth = (float)(int)(s.GlyphWidth * 2.5f); // "FF " we include trailing space in the width to easily catch clicks everywhere + s.SpacingBetweenMidCols = (float)(int)(s.HexCellWidth * 0.25f); // Every OptMidColsCount columns we add a bit of extra spacing + s.OffsetHexMinX = (s.AddrDigitsCount + 2) * s.GlyphWidth; + s.OffsetHexMaxX = s.OffsetHexMinX + (s.HexCellWidth * Cols); + s.OffsetAsciiMinX = s.OffsetAsciiMaxX = s.OffsetHexMaxX; + if (OptShowAscii) + { + s.OffsetAsciiMinX = s.OffsetHexMaxX + s.GlyphWidth * 1; + if (OptMidColsCount > 0) + s.OffsetAsciiMinX += (float)((Cols + OptMidColsCount - 1) / OptMidColsCount) * s.SpacingBetweenMidCols; + s.OffsetAsciiMaxX = s.OffsetAsciiMinX + Cols * s.GlyphWidth; + } + s.WindowWidth = s.OffsetAsciiMaxX + style.ScrollbarSize + style.WindowPadding.x * 2 + s.GlyphWidth; + } + + // Standalone Memory Editor window + void DrawWindow(const char* title, void* mem_data, size_t mem_size, size_t base_display_addr = 0x0000) + { + Sizes s; + CalcSizes(s, mem_size, base_display_addr); + ImGui::SetNextWindowSize(ImVec2(s.WindowWidth, s.WindowWidth * 0.60f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSizeConstraints(ImVec2(0.0f, 0.0f), ImVec2(s.WindowWidth, FLT_MAX)); + + Open = true; + if (ImGui::Begin(title, &Open, ImGuiWindowFlags_NoScrollbar)) + { + DrawContents(mem_data, mem_size, base_display_addr); + if (ContentsWidthChanged) + { + CalcSizes(s, mem_size, base_display_addr); + ImGui::SetWindowSize(ImVec2(s.WindowWidth, ImGui::GetWindowSize().y)); + } + } + ImGui::End(); + } + + // Memory Editor contents only + void DrawContents(void* mem_data_void, size_t mem_size, size_t base_display_addr = 0x0000) + { + if (Cols < 1) + Cols = 1; + + ImU8* mem_data = (ImU8*)mem_data_void; + Sizes s; + CalcSizes(s, mem_size, base_display_addr); + ImGuiStyle& style = ImGui::GetStyle(); + + const ImVec2 contents_pos_start = ImGui::GetCursorScreenPos(); + + // We begin into our scrolling region with the 'ImGuiWindowFlags_NoMove' in order to prevent click from moving the window. + // This is used as a facility since our main click detection code doesn't assign an ActiveId so the click would normally be caught as a window-move. + const float height_separator = style.ItemSpacing.y; + float footer_height = OptFooterExtraHeight; + if (OptShowOptions) + footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1; + if (OptShowDataPreview) + footer_height += height_separator + ImGui::GetFrameHeightWithSpacing() * 1 + ImGui::GetTextLineHeightWithSpacing() * 3; + ImGui::BeginChild("##scrolling", ImVec2(-FLT_MIN, -footer_height), ImGuiChildFlags_None, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoNav); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + + // We are not really using the clipper API correctly here, because we rely on visible_start_addr/visible_end_addr for our scrolling function. + const ImVec2 avail_size = ImGui::GetContentRegionAvail(); + const int line_total_count = (int)((mem_size + Cols - 1) / Cols); + ImGuiListClipper clipper; + clipper.Begin(line_total_count, s.LineHeight); + + bool data_next = false; + + if (DataEditingAddr >= mem_size) + DataEditingAddr = (size_t)-1; + if (DataPreviewAddr >= mem_size) + DataPreviewAddr = (size_t)-1; + + size_t preview_data_type_size = OptShowDataPreview ? DataTypeGetSize(PreviewDataType) : 0; + + size_t data_editing_addr_next = (size_t)-1; + if (DataEditingAddr != (size_t)-1) + { + // Move cursor but only apply on next frame so scrolling with be synchronized (because currently we can't change the scrolling while the window is being rendered) + if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) && (ptrdiff_t)DataEditingAddr >= (ptrdiff_t)Cols) { data_editing_addr_next = DataEditingAddr - Cols; } + else if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - Cols){ data_editing_addr_next = DataEditingAddr + Cols; } + else if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow) && (ptrdiff_t)DataEditingAddr > (ptrdiff_t)0) { data_editing_addr_next = DataEditingAddr - 1; } + else if (ImGui::IsKeyPressed(ImGuiKey_RightArrow) && (ptrdiff_t)DataEditingAddr < (ptrdiff_t)mem_size - 1) { data_editing_addr_next = DataEditingAddr + 1; } + } + + // Draw vertical separator + ImVec2 window_pos = ImGui::GetWindowPos(); + if (OptShowAscii) + draw_list->AddLine(ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y), ImVec2(window_pos.x + s.OffsetAsciiMinX - s.GlyphWidth, window_pos.y + 9999), ImGui::GetColorU32(ImGuiCol_Border)); + + const ImU32 color_text = ImGui::GetColorU32(ImGuiCol_Text); + const ImU32 color_disabled = OptGreyOutZeroes ? ImGui::GetColorU32(ImGuiCol_TextDisabled) : color_text; + + const char* format_address = OptUpperCaseHex ? "%0*" _PRISizeT "X: " : "%0*" _PRISizeT "x: "; + const char* format_data = OptUpperCaseHex ? "%0*" _PRISizeT "X" : "%0*" _PRISizeT "x"; + const char* format_byte = OptUpperCaseHex ? "%02X" : "%02x"; + const char* format_byte_space = OptUpperCaseHex ? "%02X " : "%02x "; + + MouseHovered = false; + MouseHoveredAddr = 0; + + while (clipper.Step()) + for (int line_i = clipper.DisplayStart; line_i < clipper.DisplayEnd; line_i++) // display only visible lines + { + size_t addr = (size_t)line_i * Cols; + ImGui::Text(format_address, s.AddrDigitsCount, base_display_addr + addr); + + // Draw Hexadecimal + for (int n = 0; n < Cols && addr < mem_size; n++, addr++) + { + float byte_pos_x = s.OffsetHexMinX + s.HexCellWidth * n; + if (OptMidColsCount > 0) + byte_pos_x += (float)(n / OptMidColsCount) * s.SpacingBetweenMidCols; + ImGui::SameLine(byte_pos_x); + + // Draw highlight or custom background color + const bool is_highlight_from_user_range = (addr >= HighlightMin && addr < HighlightMax); + const bool is_highlight_from_user_func = (HighlightFn && HighlightFn(mem_data, addr, UserData)); + const bool is_highlight_from_preview = (addr >= DataPreviewAddr && addr < DataPreviewAddr + preview_data_type_size); + + ImU32 bg_color = 0; + bool is_next_byte_highlighted = false; + if (is_highlight_from_user_range || is_highlight_from_user_func || is_highlight_from_preview) + { + is_next_byte_highlighted = (addr + 1 < mem_size) && ((HighlightMax != (size_t)-1 && addr + 1 < HighlightMax) || (HighlightFn && HighlightFn(mem_data, addr + 1, UserData)) || (addr + 1 < DataPreviewAddr + preview_data_type_size)); + bg_color = HighlightColor; + } + else if (BgColorFn != nullptr) + { + is_next_byte_highlighted = (addr + 1 < mem_size) && ((BgColorFn(mem_data, addr + 1, UserData) & IM_COL32_A_MASK) != 0); + bg_color = BgColorFn(mem_data, addr, UserData); + } + if (bg_color != 0) + { + float bg_width = s.GlyphWidth * 2; + if (is_next_byte_highlighted || (n + 1 == Cols)) + { + bg_width = s.HexCellWidth; + if (OptMidColsCount > 0 && n > 0 && (n + 1) < Cols && ((n + 1) % OptMidColsCount) == 0) + bg_width += s.SpacingBetweenMidCols; + } + ImVec2 pos = ImGui::GetCursorScreenPos(); + draw_list->AddRectFilled(pos, ImVec2(pos.x + bg_width, pos.y + s.LineHeight), bg_color); + } + + if (DataEditingAddr == addr) + { + // Display text input on current byte + bool data_write = false; + ImGui::PushID((void*)addr); + if (DataEditingTakeFocus) + { + ImGui::SetKeyboardFocusHere(0); + ImSnprintf(AddrInputBuf, 32, format_data, s.AddrDigitsCount, base_display_addr + addr); + ImSnprintf(DataInputBuf, 32, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]); + } + struct InputTextUserData + { + // FIXME: We should have a way to retrieve the text edit cursor position more easily in the API, this is rather tedious. This is such a ugly mess we may be better off not using InputText() at all here. + static int Callback(ImGuiInputTextCallbackData* data) + { + InputTextUserData* user_data = (InputTextUserData*)data->UserData; + if (!data->HasSelection()) + user_data->CursorPos = data->CursorPos; +#if IMGUI_VERSION_NUM < 19102 + if (data->Flags & ImGuiInputTextFlags_ReadOnly) + return 0; +#endif + if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) + { + // When not editing a byte, always refresh its InputText content pulled from underlying memory data + // (this is a bit tricky, since InputText technically "owns" the master copy of the buffer we edit it in there) + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, user_data->CurrentBufOverwrite); + data->SelectionStart = 0; + data->SelectionEnd = 2; + data->CursorPos = 0; + } + return 0; + } + char CurrentBufOverwrite[3]; // Input + int CursorPos; // Output + }; + InputTextUserData input_text_user_data; + input_text_user_data.CursorPos = -1; + ImSnprintf(input_text_user_data.CurrentBufOverwrite, 3, format_byte, ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]); + ImGuiInputTextFlags flags = ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll | ImGuiInputTextFlags_NoHorizontalScroll | ImGuiInputTextFlags_CallbackAlways; + if (ReadOnly) + flags |= ImGuiInputTextFlags_ReadOnly; + flags |= ImGuiInputTextFlags_AlwaysOverwrite; // was ImGuiInputTextFlags_AlwaysInsertMode + ImGui::SetNextItemWidth(s.GlyphWidth * 2); + if (ImGui::InputText("##data", DataInputBuf, IM_ARRAYSIZE(DataInputBuf), flags, InputTextUserData::Callback, &input_text_user_data)) + data_write = data_next = true; + else if (!DataEditingTakeFocus && !ImGui::IsItemActive()) + DataEditingAddr = data_editing_addr_next = (size_t)-1; + DataEditingTakeFocus = false; + if (input_text_user_data.CursorPos >= 2) + data_write = data_next = true; + if (data_editing_addr_next != (size_t)-1) + data_write = data_next = false; + unsigned int data_input_value = 0; + if (!ReadOnly && data_write && sscanf(DataInputBuf, "%X", &data_input_value) == 1) + { + if (WriteFn) + WriteFn(mem_data, addr, (ImU8)data_input_value, UserData); + else + mem_data[addr] = (ImU8)data_input_value; + } + if (ImGui::IsItemHovered()) + { + MouseHovered = true; + MouseHoveredAddr = addr; + } + ImGui::PopID(); + } + else + { + // NB: The trailing space is not visible but ensure there's no gap that the mouse cannot click on. + ImU8 b = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]; + + if (OptShowHexII) + { + if ((b >= 32 && b < 128)) + ImGui::Text(".%c ", b); + else if (b == 0xFF && OptGreyOutZeroes) + ImGui::TextDisabled("## "); + else if (b == 0x00) + ImGui::Text(" "); + else + ImGui::Text(format_byte_space, b); + } + else + { + if (b == 0 && OptGreyOutZeroes) + ImGui::TextDisabled("00 "); + else + ImGui::Text(format_byte_space, b); + } + if (ImGui::IsItemHovered()) + { + MouseHovered = true; + MouseHoveredAddr = addr; + if (ImGui::IsMouseClicked(0)) + { + DataEditingTakeFocus = true; + data_editing_addr_next = addr; + } + } + } + } + + if (OptShowAscii) + { + // Draw ASCII values + ImGui::SameLine(s.OffsetAsciiMinX); + ImVec2 pos = ImGui::GetCursorScreenPos(); + addr = (size_t)line_i * Cols; + + const float mouse_off_x = ImGui::GetIO().MousePos.x - pos.x; + const size_t mouse_addr = (mouse_off_x >= 0.0f && mouse_off_x < s.OffsetAsciiMaxX - s.OffsetAsciiMinX) ? addr + (size_t)(mouse_off_x / s.GlyphWidth) : (size_t)-1; + + ImGui::PushID(line_i); + if (ImGui::InvisibleButton("ascii", ImVec2(s.OffsetAsciiMaxX - s.OffsetAsciiMinX, s.LineHeight))) + { + DataEditingAddr = DataPreviewAddr = mouse_addr; + DataEditingTakeFocus = true; + } + if (ImGui::IsItemHovered()) + { + MouseHovered = true; + MouseHoveredAddr = mouse_addr; + } + ImGui::PopID(); + for (int n = 0; n < Cols && addr < mem_size; n++, addr++) + { + if (addr == DataEditingAddr) + { + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_FrameBg)); + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), ImGui::GetColorU32(ImGuiCol_TextSelectedBg)); + } + else if (BgColorFn) + { + draw_list->AddRectFilled(pos, ImVec2(pos.x + s.GlyphWidth, pos.y + s.LineHeight), BgColorFn(mem_data, addr, UserData)); + } + unsigned char c = ReadFn ? ReadFn(mem_data, addr, UserData) : mem_data[addr]; + char display_c = (c < 32 || c >= 128) ? '.' : c; + draw_list->AddText(pos, (display_c == c) ? color_text : color_disabled, &display_c, &display_c + 1); + pos.x += s.GlyphWidth; + } + } + } + ImGui::PopStyleVar(2); + const float child_width = ImGui::GetWindowSize().x; + ImGui::EndChild(); + + // Notify the main window of our ideal child content size (FIXME: we are missing an API to get the contents size from the child) + ImVec2 backup_pos = ImGui::GetCursorScreenPos(); + ImGui::SetCursorPosX(s.WindowWidth); + ImGui::Dummy(ImVec2(0.0f, 0.0f)); + ImGui::SetCursorScreenPos(backup_pos); + + if (data_next && DataEditingAddr + 1 < mem_size) + { + DataEditingAddr = DataPreviewAddr = DataEditingAddr + 1; + DataEditingTakeFocus = true; + } + else if (data_editing_addr_next != (size_t)-1) + { + DataEditingAddr = DataPreviewAddr = data_editing_addr_next; + DataEditingTakeFocus = true; + } + + const bool lock_show_data_preview = OptShowDataPreview; + if (OptShowOptions) + { + ImGui::Separator(); + DrawOptionsLine(s, mem_data, mem_size, base_display_addr); + } + + if (lock_show_data_preview) + { + ImGui::Separator(); + DrawPreviewLine(s, mem_data, mem_size, base_display_addr); + } + + if (GotoAddr != (size_t)-1) + { + if (GotoAddr < mem_size) + { + ImGui::BeginChild("##scrolling"); + ImGui::SetScrollY((GotoAddr / Cols) * ImGui::GetTextLineHeight() - avail_size.y * 0.5f); + ImGui::EndChild(); + DataEditingAddr = DataPreviewAddr = GotoAddr; + DataEditingTakeFocus = true; + } + GotoAddr = (size_t)-1; + } + + const ImVec2 contents_pos_end(contents_pos_start.x + child_width, ImGui::GetCursorScreenPos().y); + //ImGui::GetForegroundDrawList()->AddRect(contents_pos_start, contents_pos_end, IM_COL32(255, 0, 0, 255)); + if (OptShowOptions) + if (ImGui::IsMouseHoveringRect(contents_pos_start, contents_pos_end)) + if (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows) && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) + ImGui::OpenPopup("OptionsPopup"); + + if (ImGui::BeginPopup("OptionsPopup")) + { + ImGui::SetNextItemWidth(s.GlyphWidth * 7 + style.FramePadding.x * 2.0f); + if (ImGui::DragInt("##cols", &Cols, 0.2f, 4, 32, "%d cols")) { ContentsWidthChanged = true; if (Cols < 1) Cols = 1; } + ImGui::Checkbox("Show Data Preview", &OptShowDataPreview); + ImGui::Checkbox("Show HexII", &OptShowHexII); + if (ImGui::Checkbox("Show Ascii", &OptShowAscii)) { ContentsWidthChanged = true; } + ImGui::Checkbox("Grey out zeroes", &OptGreyOutZeroes); + ImGui::Checkbox("Uppercase Hex", &OptUpperCaseHex); + + ImGui::EndPopup(); + } + } + + void DrawOptionsLine(const Sizes& s, void* mem_data, size_t mem_size, size_t base_display_addr) + { + IM_UNUSED(mem_data); + ImGuiStyle& style = ImGui::GetStyle(); + const char* format_range = OptUpperCaseHex ? "Range %0*" _PRISizeT "X..%0*" _PRISizeT "X" : "Range %0*" _PRISizeT "x..%0*" _PRISizeT "x"; + + // Options menu + if (ImGui::Button("Options")) + ImGui::OpenPopup("OptionsPopup"); + + ImGui::SameLine(); + ImGui::Text(format_range, s.AddrDigitsCount, base_display_addr, s.AddrDigitsCount, base_display_addr + mem_size - 1); + ImGui::SameLine(); + ImGui::SetNextItemWidth((s.AddrDigitsCount + 1) * s.GlyphWidth + style.FramePadding.x * 2.0f); + if (ImGui::InputText("##addr", AddrInputBuf, IM_ARRAYSIZE(AddrInputBuf), ImGuiInputTextFlags_CharsHexadecimal | ImGuiInputTextFlags_EnterReturnsTrue)) + { + size_t goto_addr; + if (sscanf(AddrInputBuf, "%" _PRISizeT "X", &goto_addr) == 1) + { + GotoAddr = goto_addr - base_display_addr; + HighlightMin = HighlightMax = (size_t)-1; + } + } + + //if (MouseHovered) + //{ + // ImGui::SameLine(); + // ImGui::Text("Hovered: %p", MouseHoveredAddr); + //} + } + + void DrawPreviewLine(const Sizes& s, void* mem_data_void, size_t mem_size, size_t base_display_addr) + { + IM_UNUSED(base_display_addr); + ImU8* mem_data = (ImU8*)mem_data_void; + ImGuiStyle& style = ImGui::GetStyle(); + ImGui::AlignTextToFramePadding(); + ImGui::Text("Preview as:"); + ImGui::SameLine(); + ImGui::SetNextItemWidth((s.GlyphWidth * 10.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x); + + static const ImGuiDataType supported_data_types[] = { ImGuiDataType_S8, ImGuiDataType_U8, ImGuiDataType_S16, ImGuiDataType_U16, ImGuiDataType_S32, ImGuiDataType_U32, ImGuiDataType_S64, ImGuiDataType_U64, ImGuiDataType_Float, ImGuiDataType_Double }; + if (ImGui::BeginCombo("##combo_type", DataTypeGetDesc(PreviewDataType), ImGuiComboFlags_HeightLargest)) + { + for (int n = 0; n < IM_ARRAYSIZE(supported_data_types); n++) + { + ImGuiDataType data_type = supported_data_types[n]; + if (ImGui::Selectable(DataTypeGetDesc(data_type), PreviewDataType == data_type)) + PreviewDataType = data_type; + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + ImGui::SetNextItemWidth((s.GlyphWidth * 6.0f) + style.FramePadding.x * 2.0f + style.ItemInnerSpacing.x); + ImGui::Combo("##combo_endianness", &PreviewEndianness, "LE\0BE\0\0"); + + char buf[128] = ""; + float x = s.GlyphWidth * 6.0f; + bool has_value = DataPreviewAddr != (size_t)-1; + if (has_value) + DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Dec, buf, (size_t)IM_ARRAYSIZE(buf)); + ImGui::Text("Dec"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); + if (has_value) + DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Hex, buf, (size_t)IM_ARRAYSIZE(buf)); + ImGui::Text("Hex"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); + if (has_value) + DrawPreviewData(DataPreviewAddr, mem_data, mem_size, PreviewDataType, DataFormat_Bin, buf, (size_t)IM_ARRAYSIZE(buf)); + buf[IM_ARRAYSIZE(buf) - 1] = 0; + ImGui::Text("Bin"); ImGui::SameLine(x); ImGui::TextUnformatted(has_value ? buf : "N/A"); + } + + // Utilities for Data Preview (since we don't access imgui_internal.h) + // FIXME: This technically depends on ImGuiDataType order. + const char* DataTypeGetDesc(ImGuiDataType data_type) const + { + const char* descs[] = { "Int8", "Uint8", "Int16", "Uint16", "Int32", "Uint32", "Int64", "Uint64", "Float", "Double" }; + IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(descs)); + return descs[data_type]; + } + + size_t DataTypeGetSize(ImGuiDataType data_type) const + { + const size_t sizes[] = { 1, 1, 2, 2, 4, 4, 8, 8, sizeof(float), sizeof(double) }; + IM_ASSERT(data_type >= 0 && data_type < IM_ARRAYSIZE(sizes)); + return sizes[data_type]; + } + + const char* DataFormatGetDesc(DataFormat data_format) const + { + const char* descs[] = { "Bin", "Dec", "Hex" }; + IM_ASSERT(data_format >= 0 && data_format < DataFormat_COUNT); + return descs[data_format]; + } + + bool IsBigEndian() const + { + ImU16 x = 1; + char c[2]; + memcpy(c, &x, 2); + return c[0] != 0; + } + + static void* EndiannessCopyBigEndian(void* _dst, void* _src, size_t s, int is_little_endian) + { + if (is_little_endian) + { + ImU8* dst = (ImU8*)_dst; + ImU8* src = (ImU8*)_src + s - 1; + for (int i = 0, n = (int)s; i < n; ++i) + memcpy(dst++, src--, 1); + return _dst; + } + else + { + return memcpy(_dst, _src, s); + } + } + + static void* EndiannessCopyLittleEndian(void* _dst, void* _src, size_t s, int is_little_endian) + { + if (is_little_endian) + { + return memcpy(_dst, _src, s); + } + else + { + ImU8* dst = (ImU8*)_dst; + ImU8* src = (ImU8*)_src + s - 1; + for (int i = 0, n = (int)s; i < n; ++i) + memcpy(dst++, src--, 1); + return _dst; + } + } + + void* EndiannessCopy(void* dst, void* src, size_t size) const + { + static void* (*fp)(void*, void*, size_t, int) = nullptr; + if (fp == nullptr) + fp = IsBigEndian() ? EndiannessCopyBigEndian : EndiannessCopyLittleEndian; + return fp(dst, src, size, PreviewEndianness); + } + + const char* FormatBinary(const ImU8* buf, int width) const + { + IM_ASSERT(width <= 64); + size_t out_n = 0; + static char out_buf[64 + 8 + 1]; + int n = width / 8; + for (int j = n - 1; j >= 0; --j) + { + for (int i = 0; i < 8; ++i) + out_buf[out_n++] = (buf[j] & (1 << (7 - i))) ? '1' : '0'; + out_buf[out_n++] = ' '; + } + IM_ASSERT(out_n < IM_ARRAYSIZE(out_buf)); + out_buf[out_n] = 0; + return out_buf; + } + + // [Internal] + void DrawPreviewData(size_t addr, const ImU8* mem_data, size_t mem_size, ImGuiDataType data_type, DataFormat data_format, char* out_buf, size_t out_buf_size) const + { + ImU8 buf[8]; + size_t elem_size = DataTypeGetSize(data_type); + size_t size = addr + elem_size > mem_size ? mem_size - addr : elem_size; + if (ReadFn) + for (int i = 0, n = (int)size; i < n; ++i) + buf[i] = ReadFn(mem_data, addr + i, UserData); + else + memcpy(buf, mem_data + addr, size); + + if (data_format == DataFormat_Bin) + { + ImU8 binbuf[8]; + EndiannessCopy(binbuf, buf, size); + ImSnprintf(out_buf, out_buf_size, "%s", FormatBinary(binbuf, (int)size * 8)); + return; + } + + out_buf[0] = 0; + switch (data_type) + { + case ImGuiDataType_S8: + { + ImS8 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhd", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0xFF); return; } + break; + } + case ImGuiDataType_U8: + { + ImU8 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hhu", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%02x", data & 0XFF); return; } + break; + } + case ImGuiDataType_S16: + { + ImS16 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hd", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; } + break; + } + case ImGuiDataType_U16: + { + ImU16 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%hu", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%04x", data & 0xFFFF); return; } + break; + } + case ImGuiDataType_S32: + { + ImS32 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%d", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; } + break; + } + case ImGuiDataType_U32: + { + ImU32 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%u", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%08x", data); return; } + break; + } + case ImGuiDataType_S64: + { + ImS64 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%lld", (long long)data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; } + break; + } + case ImGuiDataType_U64: + { + ImU64 data = 0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%llu", (long long)data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "0x%016llx", (long long)data); return; } + break; + } + case ImGuiDataType_Float: + { + float data = 0.0f; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; } + break; + } + case ImGuiDataType_Double: + { + double data = 0.0; + EndiannessCopy(&data, buf, size); + if (data_format == DataFormat_Dec) { ImSnprintf(out_buf, out_buf_size, "%f", data); return; } + if (data_format == DataFormat_Hex) { ImSnprintf(out_buf, out_buf_size, "%a", data); return; } + break; + } + default: + case ImGuiDataType_COUNT: + break; + } // Switch + IM_ASSERT(0); // Shouldn't reach + } +}; + +#undef _PRISizeT +#undef ImSnprintf + +#ifdef _MSC_VER +#pragma warning (pop) #endif \ No newline at end of file diff --git a/src/spice2x/external/toojpeg/LICENSE b/src/spice2x/external/toojpeg/LICENSE deleted file mode 100644 index 76d0362..0000000 --- a/src/spice2x/external/toojpeg/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -zlib License - -Copyright (c) 2011-2016 Stephan Brumme - -This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. -Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: -1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. - If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. -2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. diff --git a/src/spice2x/external/toojpeg/toojpeg.cpp b/src/spice2x/external/toojpeg/toojpeg.cpp deleted file mode 100644 index 866072c..0000000 --- a/src/spice2x/external/toojpeg/toojpeg.cpp +++ /dev/null @@ -1,665 +0,0 @@ -// ////////////////////////////////////////////////////////// -// toojpeg.cpp -// written by Stephan Brumme, 2018-2019 -// see https://create.stephan-brumme.com/toojpeg/ -// - -#include "toojpeg.h" - -// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf -// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format -// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h -// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4), -// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG) -// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1) -// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing - -namespace // anonymous namespace to hide local functions / constants / etc. -{ -// //////////////////////////////////////// -// data types -using uint8_t = unsigned char; -using uint16_t = unsigned short; -using int16_t = short; -using int32_t = int; // at least four bytes - -// //////////////////////////////////////// -// constants - -// quantization tables from JPEG Standard, Annex K -const uint8_t DefaultQuantLuminance[8*8] = - { 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values, - 12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333 - 14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image - 14, 17, 22, 29, 51, 87, 80, 62, - 18, 22, 37, 56, 68,109,103, 77, - 24, 35, 55, 64, 81,104,113, 92, - 49, 64, 78, 87,103,121,120,101, - 72, 92, 95, 98,112,100,103, 99 }; -const uint8_t DefaultQuantChrominance[8*8] = - { 17, 18, 24, 47, 99, 99, 99, 99, - 18, 21, 26, 66, 99, 99, 99, 99, - 24, 26, 56, 99, 99, 99, 99, 99, - 47, 66, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99, - 99, 99, 99, 99, 99, 99, 99, 99 }; - -// 8x8 blocks are processed in zig-zag order -// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons -// note: ZigZagInv[ZigZag[i]] = i -const uint8_t ZigZagInv[8*8] = - { 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28, - 17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42, - 12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43, - 27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53, - 35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54, - 29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60, - 58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61, - 53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63 - -// static Huffman code tables from JPEG standard Annex K -// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits), -// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long -// - Values tables are a list of values ordered by their Huffman code bitsize, -// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ... - -// Huffman definitions for first DC/AC tables (luminance / Y channel) -const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12 -const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes -const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162 -const uint8_t AcLuminanceValues [162] = // => 162 codes - { 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because - 0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F - 0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A - 0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0 - 0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee - 0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2, - 0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; -// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels) -const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12 -const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues) -const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162 -const uint8_t AcChrominanceValues [162] = // => 162 codes - { 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order - 0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding) - 0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58, - 0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87, - 0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4, - 0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA, - 0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA }; -const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT - -// //////////////////////////////////////// -// structs - -// represent a single Huffman code -struct BitCode -{ - BitCode() = default; // undefined state, must be initialized at a later time - BitCode(uint16_t code_, uint8_t numBits_) - : code(code_), numBits(numBits_) {} - uint16_t code; // JPEG's Huffman codes are limited to 16 bits - uint8_t numBits; // number of valid bits -}; - -// wrapper for bit output operations -struct BitWriter -{ - // user-supplied callback that writes/stores one byte - TooJpeg::WRITE_ONE_BYTE output; - // initialize writer - explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {} - - // store the most recently encoded bits that are not written yet - struct BitBuffer - { - int32_t data = 0; // actually only at most 24 bits are used - uint8_t numBits = 0; // number of valid bits (the right-most bits) - } buffer; - - // write Huffman bits stored in BitCode, keep excess bits in BitBuffer - BitWriter& operator<<(const BitCode& data) - { - // append the new bits to those bits leftover from previous call(s) - buffer.numBits += data.numBits; - buffer.data <<= data.numBits; - buffer.data |= data.code; - - // write all "full" bytes - while (buffer.numBits >= 8) - { - // extract highest 8 bits - buffer.numBits -= 8; - auto oneByte = uint8_t(buffer.data >> buffer.numBits); - output(oneByte); - - if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker) - output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence" - - // note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits - // if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line - //buffer.bits &= (1 << buffer.numBits) - 1; - } - return *this; - } - - // write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing) - void flush() - { - // at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111 - *this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it - } - - // NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output ! - // write a single byte - BitWriter& operator<<(uint8_t oneByte) - { - output(oneByte); - return *this; - } - - // write an array of bytes - template - BitWriter& operator<<(T (&manyBytes)[Size]) - { - for (auto c : manyBytes) - output(c); - return *this; - } - - // start a new JFIF block - void addMarker(uint8_t id, uint16_t length) - { - output(0xFF); output(id); // ID, always preceded by 0xFF - output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well) - output(uint8_t(length & 0xFF)); - } -}; - -// //////////////////////////////////////// -// functions / templates - -// same as std::min() -template -Number minimum(Number value, Number maximum) -{ - return value <= maximum ? value : maximum; -} - -// restrict a value to the interval [minimum, maximum] -template -Number clamp(Number value, Limit minValue, Limit maxValue) -{ - if (value <= minValue) return minValue; // never smaller than the minimum - if (value >= maxValue) return maxValue; // never bigger than the maximum - return value; // value was inside interval, keep it -} - -// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion -float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; } -float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; } -float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; } - -// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images") -void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical) -{ - const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2) - const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8) - const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8) - const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2) - - // modify in-place - auto& block0 = block[0 ]; - auto& block1 = block[1 * stride]; - auto& block2 = block[2 * stride]; - auto& block3 = block[3 * stride]; - auto& block4 = block[4 * stride]; - auto& block5 = block[5 * stride]; - auto& block6 = block[6 * stride]; - auto& block7 = block[7 * stride]; - - // based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments - auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7 - auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6 - auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5 - auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4 - - auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2") - auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12 - - block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3" - - auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names - block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5" - - auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2") - auto sub12_56 = sub16 + sub25; // tmp11 - auto sub01_67 = sub16 + sub07; // tmp12 - - auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt; - auto z2 = sub23_45 * InvSqrtSqrt + z5; - auto z3 = sub12_56 * InvSqrt; - auto z4 = sub01_67 * SqrtHalfSqrt + z5; - auto z6 = sub07 + z3; // z11 ("phase 5") - auto z7 = sub07 - z3; // z13 - block1 = z6 + z4; block7 = z6 - z4; // "phase 6" - block5 = z7 + z2; block3 = z7 - z2; -} - -// run DCT, quantize and write Huffman bit codes -int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC, - const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords) -{ - // "linearize" the 8x8 block, treat it as a flat array of 64 floats - auto block64 = (float*) block; - - // DCT: rows - for (auto offset = 0; offset < 8; offset++) - DCT(block64 + offset*8, 1); - // DCT: columns - for (auto offset = 0; offset < 8; offset++) - DCT(block64 + offset*1, 8); - - // scale - for (auto i = 0; i < 8*8; i++) - block64[i] *= scaled[i]; - - // encode DC (the first coefficient is the "average color" of the 8x8 block) - auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect - - // quantize and zigzag the other 63 coefficients - auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently) - int16_t quantized[8*8]; - for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed - { - auto value = block64[ZigZagInv[i]]; - // round to nearest integer - quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect - // remember offset of last non-zero coefficient - if (quantized[i] != 0) - posNonZero = i; - } - - // same "average color" as previous block ? - auto diff = DC - lastDC; - if (diff == 0) - writer << huffmanDC[0x00]; // yes, write a special short symbol - else - { - auto bits = codewords[diff]; // nope, encode the difference to previous block's average color - writer << huffmanDC[bits.numBits] << bits; - } - - // encode ACs (quantized[1..63]) - auto offset = 0; // upper 4 bits count the number of consecutive zeros - for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too - { - // zeros are encoded in a special way - while (quantized[i] == 0) // found another zero ? - { - offset += 0x10; // add 1 to the upper 4 bits - // split into blocks of at most 16 consecutive zeros - if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15 - { - writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros" - offset = 0; - } - i++; - } - - auto encoded = codewords[quantized[i]]; - // combine number of zeros with the number of bits of the next non-zero value - writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself - offset = 0; - } - - // send end-of-block code (0x00), only needed if there are trailing zeros - if (posNonZero < 8*8 - 1) // = 63 - writer << huffmanAC[0x00]; - - return DC; -} - -// Jon's code includes the pre-generated Huffman codes -// I don't like these "magic constants" and compute them on my own :-) -void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256]) -{ - // process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits - auto huffmanCode = 0; - for (auto numBits = 1; numBits <= 16; numBits++) - { - // ... and each code of these bitsizes - for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1 - result[*values++] = BitCode(huffmanCode++, numBits); - - // next Huffman code needs to be one bit wider - huffmanCode <<= 1; - } -} - -} // end of anonymous namespace - -// -------------------- externally visible code -------------------- - -namespace TooJpeg -{ -// the only exported function ... -bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height, - bool isRGB, unsigned char quality_, bool downsample, const char* comment) -{ - // reject invalid pointers - if (output == nullptr || pixels_ == nullptr) - return false; - // check image format - if (width == 0 || height == 0) - return false; - - // number of components - const auto numComponents = isRGB ? 3 : 1; - // note: if there is just one component (=grayscale), then only luminance needs to be stored in the file - // thus everything related to chrominance need not to be written to the JPEG - // I still compute a few things, like quantization tables to avoid a complete code mess - - // grayscale images can't be downsampled (because there are no Cb + Cr channels) - if (!isRGB) - downsample = false; - - // wrapper for all output operations - BitWriter bitWriter(output); - - // //////////////////////////////////////// - // JFIF headers - const uint8_t HeaderJfif[2+2+16] = - { 0xFF,0xD8, // SOI marker (start of image) - 0xFF,0xE0, // JFIF APP0 tag - 0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field) - 'J','F','I','F',0, // JFIF identifier, zero-terminated - 1,1, // JFIF version 1.1 - 0, // no density units specified - 0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically - 0,0 }; // no thumbnail (size 0 x 0) - bitWriter << HeaderJfif; - - // //////////////////////////////////////// - // comment (optional) - if (comment != nullptr) - { - // look for zero terminator - auto length = 0; // = strlen(comment); - while (comment[length] != 0) - length++; - - // write COM marker - bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field - // ... and write the comment itself - for (auto i = 0; i < length; i++) - bitWriter << comment[i]; - } - - // //////////////////////////////////////// - // adjust quantization tables to desired quality - - // quality level must be in 1 ... 100 - auto quality = clamp(quality_, 1, 100); - // convert to an internal JPEG quality factor, formula taken from libjpeg - quality = quality < 50 ? 5000 / quality : 200 - quality * 2; - - uint8_t quantLuminance [8*8]; - uint8_t quantChrominance[8*8]; - for (auto i = 0; i < 8*8; i++) - { - int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100; - int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100; - - // clamp to 1..255 - quantLuminance [i] = clamp(luminance, 1, 255); - quantChrominance[i] = clamp(chrominance, 1, 255); - } - - // write quantization tables - bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field - // each table has 64 entries and is preceded by an ID byte - - bitWriter << 0x00 << quantLuminance; // first quantization table - if (isRGB) - bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images - - // //////////////////////////////////////// - // write image infos (SOF0 - start of frame) - bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field - - // 8 bits per channel - bitWriter << 0x08 - // image dimensions (big-endian) - << (height >> 8) << (height & 0xFF) - << (width >> 8) << (width & 0xFF); - - // sampling and quantization tables for each component - bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr) - for (auto id = 1; id <= numComponents; id++) - bitWriter << id // component ID (Y=1, Cb=2, Cr=3) - // bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical - << (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0 - << (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr - - // //////////////////////////////////////// - // Huffman tables - // DHT marker - define Huffman tables - bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208)); - // 2 bytes for the length field, store chrominance only if needed - // 1+16+12 for the DC luminance - // 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162) - // 1+16+12 for the DC chrominance - // 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above) - - // store luminance's DC+AC Huffman table definitions - bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline) - << DcLuminanceCodesPerBitsize - << DcLuminanceValues; - bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline) - << AcLuminanceCodesPerBitsize - << AcLuminanceValues; - - // compute actual Huffman code tables (see Jon's code for precalculated tables) - BitCode huffmanLuminanceDC[256]; - BitCode huffmanLuminanceAC[256]; - generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC); - generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC); - - // chrominance is only relevant for color images - BitCode huffmanChrominanceDC[256]; - BitCode huffmanChrominanceAC[256]; - if (isRGB) - { - // store luminance's DC+AC Huffman table definitions - bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline) - << DcChrominanceCodesPerBitsize - << DcChrominanceValues; - bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline) - << AcChrominanceCodesPerBitsize - << AcChrominanceValues; - - // compute actual Huffman code tables (see Jon's code for precalculated tables) - generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC); - generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC); - } - - // //////////////////////////////////////// - // start of scan (there is only a single scan for baseline JPEGs) - bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components, - // then 2 bytes for each component and 3 bytes for spectral selection - - // assign Huffman tables to each component - bitWriter << numComponents; - for (auto id = 1; id <= numComponents; id++) - // highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table - bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC - - // constant values for our baseline JPEGs (which have a single sequential scan) - static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0 - bitWriter << Spectral; - - // //////////////////////////////////////// - // adjust quantization tables with AAN scaling factors to simplify DCT - float scaledLuminance [8*8]; - float scaledChrominance[8*8]; - for (auto i = 0; i < 8*8; i++) - { - auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3 - auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7 - - // scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2) - static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f }; - auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8); - scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i]; - scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i]; - // if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f) - //static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp - //scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp - //scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]); - } - - // //////////////////////////////////////// - // precompute JPEG codewords for quantized DCT - BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit] - BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]] - uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined) - int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1 - for (int16_t value = 1; value < CodeWordLimit; value++) - { - // numBits = position of highest set bit (ignoring the sign) - // mask = (2^numBits) - 1 - if (value > mask) // one more bit ? - { - numBits++; - mask = (mask << 1) | 1; // append a set bit - } - codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value] - codewords[+value] = BitCode( value, numBits); - } - - // just convert image data from void* - auto pixels = (const uint8_t*)pixels_; - - // the next two variables are frequently used when checking for image borders - const auto maxWidth = width - 1; // "last row" - const auto maxHeight = height - 1; // "bottom line" - - // process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles - const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling - const auto mcuSize = 8 * sampling; - - // average color of the previous MCU - int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0; - // convert from RGB to YCbCr - float Y[8][8], Cb[8][8], Cr[8][8]; - - for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize) - for (auto mcuX = 0; mcuX < width; mcuX += mcuSize) - { - // YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too - // YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block) - for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420) - for (auto blockX = 0; blockX < mcuSize; blockX += 8) - { - // now we finally have an 8x8 block ... - for (auto deltaY = 0; deltaY < 8; deltaY++) - { - auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed - auto row = minimum(mcuY + blockY + deltaY, maxHeight); - for (auto deltaX = 0; deltaX < 8; deltaX++) - { - // find actual pixel position within the current image - auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows - if (column < maxWidth) - column++; - - // grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128 - if (!isRGB) - { - Y[deltaY][deltaX] = pixels[pixelPos] - 128.f; - continue; - } - - // RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel) - auto r = pixels[3 * pixelPos ]; - auto g = pixels[3 * pixelPos + 1]; - auto b = pixels[3 * pixelPos + 2]; - - Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128 - // YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass - if (!downsample) - { - Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion - Cr[deltaY][deltaX] = rgb2cr(r, g, b); - } - } - } - - // encode Y channel - lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords); - // Cb and Cr are encoded about 50 lines below - } - - // grayscale images don't need any Cb and Cr information - if (!isRGB) - continue; - - // //////////////////////////////////////// - // the following lines are only relevant for YCbCr420: - // average/downsample chrominance of four pixels while respecting the image borders - if (downsample) - for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency - { - auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area - auto column = mcuX; // column is updated inside next loop - auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3 - - // deltas (in bytes) to next row / column, must not exceed image borders - auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line - auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel - - for (short deltaX = 0; deltaX < 8; deltaX++) - { - // let's add all four samples (2x2 area) - auto right = pixelPos + columnStep; - auto down = pixelPos + rowStep; - auto downRight = pixelPos + columnStep + rowStep; - - // note: cast from 8 bits to >8 bits to avoid overflows when adding - auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ]; - auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1]; - auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2]; - - // convert to Cb and Cr - Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values - Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion - - // step forward to next 2x2 area - pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents) - column += 2; - - // reached right border ? - if (column >= maxWidth) - { - columnStep = 0; - pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel - } - } - } // end of YCbCr420 code for Cb and Cr - - // encode Cb and Cr - lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); - lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords); - } - - bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer - - // /////////////////////////// - // EOI marker - bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker() - return true; -} // writeJpeg() -} // namespace TooJpeg diff --git a/src/spice2x/external/toojpeg/toojpeg.h b/src/spice2x/external/toojpeg/toojpeg.h deleted file mode 100644 index aa16b0c..0000000 --- a/src/spice2x/external/toojpeg/toojpeg.h +++ /dev/null @@ -1,62 +0,0 @@ -// ////////////////////////////////////////////////////////// -// toojpeg.h -// written by Stephan Brumme, 2018-2019 -// see https://create.stephan-brumme.com/toojpeg/ -// - -// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part). -// Its interface has only one function: writeJpeg() - and that's it ! -// -// basic example: -// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel -// auto pixels = new unsigned char[1024*768*3]; -// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer -// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file -// => let's go ! -// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768); - -#pragma once - -namespace TooJpeg -{ - // write one byte (to disk, memory, ...) - typedef void (*WRITE_ONE_BYTE)(unsigned char); - // this callback is called for every byte generated by the encoder and behaves similar to fputc - // if you prefer stylish C++11 syntax then it can be a lambda, too: - // auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); }; - - // output - callback that stores a single byte (writes to disk, memory, ...) - // pixels - stored in RGB format or grayscale, stored from upper-left to lower-right - // width,height - image size - // isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel) - // quality - between 1 (worst) and 100 (best) - // downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale - // comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF - bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height, - bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr); -} // namespace TooJpeg - -// My main inspiration was Jon Olick's Minimalistic JPEG writer -// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ). -// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer", -// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js -// -// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from. -// And I managed to remove the need for any external includes ... -// yes, that's right: my library has no (!) includes at all, not even #include -// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish. -// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack. -// -// In contrast to Jon's code, compression can be significantly improved in many use cases: -// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels -// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss -// -// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo). -// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark -// -// Last but not least you can optionally add a JPEG comment. -// -// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient). -// I haven't tested the code on big-endian systems or anything that smells like an apple. -// -// USE AT YOUR OWN RISK. Because you are a brave soul :-) diff --git a/src/spice2x/games/gitadora/asio.cpp b/src/spice2x/games/gitadora/asio.cpp index ee06fb7..5b70566 100644 --- a/src/spice2x/games/gitadora/asio.cpp +++ b/src/spice2x/games/gitadora/asio.cpp @@ -1,171 +1,171 @@ -#include "asio.h" - -#include -#include - -#include "avs/game.h" -#include "gitadora.h" -#include "util/detour.h" -#include "util/logging.h" - -namespace games::gitadora { - - // Redirects the game's hard-coded "XONAR" ASIO driver lookup to the - // driver name in ASIO_DRIVER by intercepting registry calls to - // HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles - // so we can recognise them on subsequent reg* calls. - - static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast(0x4001); - static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast(0x4002); - static const char *FAKE_ASIO_DEVICE_NAME = "XONAR"; - - static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; - static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; - static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr; - static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr; - static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr; - - static HKEY real_asio_reg_handle = nullptr; - static HKEY real_asio_device_reg_handle = nullptr; - - static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, - PHKEY phkResult) - { - if (ASIO_DRIVER.has_value() && - lpSubKey != nullptr && - phkResult != nullptr && - hKey == PARENT_ASIO_REG_HANDLE && - _stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) { - - *phkResult = DEVICE_ASIO_REG_HANDLE; - - log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value()); - const auto result = RegOpenKeyExA_orig( - real_asio_reg_handle, - ASIO_DRIVER.value().c_str(), - ulOptions, - samDesired, - &real_asio_device_reg_handle); - - if (result != ERROR_SUCCESS) { - log_warning( - "gitadora::asio", - "failed to open registry subkey '{}', error=0x{:x}", - ASIO_DRIVER.value(), result); - log_warning( - "gitadora::asio", - "due to improper ASIO setting, audio init will fail"); - } - - return result; - } - - return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult); - } - - static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { - if (ASIO_DRIVER.has_value() && - lpSubKey != nullptr && - phkResult != nullptr && - hKey == HKEY_LOCAL_MACHINE && - _stricmp(lpSubKey, "software\\asio") == 0) - { - *phkResult = PARENT_ASIO_REG_HANDLE; - return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle); - } - - return RegOpenKeyA_orig(hKey, lpSubKey, phkResult); - } - - static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) { - if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) { - if (dwIndex == 0) { - // forward to real handle just to verify the key exists; we - // overwrite the name with our fake driver string regardless - auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName); - if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) { - log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME); - strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName); - lpName[cchName - 1] = '\0'; - } - return ret; - } else { - return ERROR_NO_MORE_ITEMS; - } - } - - return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName); - } - - static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, - LPBYTE lpData, LPDWORD lpcbData) - { - HKEY target = hKey; - - if (ASIO_DRIVER.has_value() && - lpValueName != nullptr && - lpData != nullptr && - lpcbData != nullptr && - hKey == DEVICE_ASIO_REG_HANDLE) { - if (_stricmp(lpValueName, "Description") == 0) { - // engine may verify the driver name after open; ensure it still - // sees something containing "XONAR" so the substring check passes - const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1; - if (*lpcbData < len) { - *lpcbData = static_cast(len); - return ERROR_MORE_DATA; - } - memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len); - *lpcbData = static_cast(len); - if (lpType != nullptr) { - *lpType = REG_SZ; - } - return ERROR_SUCCESS; - } - - // for everything else (CLSID etc.) defer to the real driver subkey - target = real_asio_device_reg_handle; - } - - return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData); - } - - static LONG WINAPI RegCloseKey_hook(HKEY hKey) { - if (hKey == PARENT_ASIO_REG_HANDLE) { - if (real_asio_reg_handle != nullptr) { - RegCloseKey_orig(real_asio_reg_handle); - real_asio_reg_handle = nullptr; - } - return ERROR_SUCCESS; - } - - if (hKey == DEVICE_ASIO_REG_HANDLE) { - if (real_asio_device_reg_handle != nullptr) { - RegCloseKey_orig(real_asio_device_reg_handle); - real_asio_device_reg_handle = nullptr; - } - return ERROR_SUCCESS; - } - - return RegCloseKey_orig(hKey); - } - - void asio_hook_init() { - if (!ASIO_DRIVER.has_value()) { - return; - } - - log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value()); - - RegCloseKey_orig = detour::iat_try( - "RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE); - RegEnumKeyA_orig = detour::iat_try( - "RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE); - RegOpenKeyA_orig = detour::iat_try( - "RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE); - RegOpenKeyExA_orig = detour::iat_try( - "RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE); - RegQueryValueExA_orig = detour::iat_try( - "RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE); - } -} +#include "asio.h" + +#include +#include + +#include "avs/game.h" +#include "gitadora.h" +#include "util/detour.h" +#include "util/logging.h" + +namespace games::gitadora { + + // Redirects the game's hard-coded "XONAR" ASIO driver lookup to the + // driver name in ASIO_DRIVER by intercepting registry calls to + // HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles + // so we can recognise them on subsequent reg* calls. + + static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast(0x4001); + static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast(0x4002); + static const char *FAKE_ASIO_DEVICE_NAME = "XONAR"; + + static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; + static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; + static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr; + static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr; + static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr; + + static HKEY real_asio_reg_handle = nullptr; + static HKEY real_asio_device_reg_handle = nullptr; + + static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, + PHKEY phkResult) + { + if (ASIO_DRIVER.has_value() && + lpSubKey != nullptr && + phkResult != nullptr && + hKey == PARENT_ASIO_REG_HANDLE && + _stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) { + + *phkResult = DEVICE_ASIO_REG_HANDLE; + + log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value()); + const auto result = RegOpenKeyExA_orig( + real_asio_reg_handle, + ASIO_DRIVER.value().c_str(), + ulOptions, + samDesired, + &real_asio_device_reg_handle); + + if (result != ERROR_SUCCESS) { + log_warning( + "gitadora::asio", + "failed to open registry subkey '{}', error=0x{:x}", + ASIO_DRIVER.value(), result); + log_warning( + "gitadora::asio", + "due to improper ASIO setting, audio init will fail"); + } + + return result; + } + + return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult); + } + + static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { + if (ASIO_DRIVER.has_value() && + lpSubKey != nullptr && + phkResult != nullptr && + hKey == HKEY_LOCAL_MACHINE && + _stricmp(lpSubKey, "software\\asio") == 0) + { + *phkResult = PARENT_ASIO_REG_HANDLE; + return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle); + } + + return RegOpenKeyA_orig(hKey, lpSubKey, phkResult); + } + + static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) { + if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) { + if (dwIndex == 0) { + // forward to real handle just to verify the key exists; we + // overwrite the name with our fake driver string regardless + auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName); + if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) { + log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME); + strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName); + lpName[cchName - 1] = '\0'; + } + return ret; + } else { + return ERROR_NO_MORE_ITEMS; + } + } + + return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName); + } + + static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, + LPBYTE lpData, LPDWORD lpcbData) + { + HKEY target = hKey; + + if (ASIO_DRIVER.has_value() && + lpValueName != nullptr && + lpData != nullptr && + lpcbData != nullptr && + hKey == DEVICE_ASIO_REG_HANDLE) { + if (_stricmp(lpValueName, "Description") == 0) { + // engine may verify the driver name after open; ensure it still + // sees something containing "XONAR" so the substring check passes + const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1; + if (*lpcbData < len) { + *lpcbData = static_cast(len); + return ERROR_MORE_DATA; + } + memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len); + *lpcbData = static_cast(len); + if (lpType != nullptr) { + *lpType = REG_SZ; + } + return ERROR_SUCCESS; + } + + // for everything else (CLSID etc.) defer to the real driver subkey + target = real_asio_device_reg_handle; + } + + return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData); + } + + static LONG WINAPI RegCloseKey_hook(HKEY hKey) { + if (hKey == PARENT_ASIO_REG_HANDLE) { + if (real_asio_reg_handle != nullptr) { + RegCloseKey_orig(real_asio_reg_handle); + real_asio_reg_handle = nullptr; + } + return ERROR_SUCCESS; + } + + if (hKey == DEVICE_ASIO_REG_HANDLE) { + if (real_asio_device_reg_handle != nullptr) { + RegCloseKey_orig(real_asio_device_reg_handle); + real_asio_device_reg_handle = nullptr; + } + return ERROR_SUCCESS; + } + + return RegCloseKey_orig(hKey); + } + + void asio_hook_init() { + if (!ASIO_DRIVER.has_value()) { + return; + } + + log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value()); + + RegCloseKey_orig = detour::iat_try( + "RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE); + RegEnumKeyA_orig = detour::iat_try( + "RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE); + RegOpenKeyA_orig = detour::iat_try( + "RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE); + RegOpenKeyExA_orig = detour::iat_try( + "RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE); + RegQueryValueExA_orig = detour::iat_try( + "RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE); + } +} diff --git a/src/spice2x/games/gitadora/asio.h b/src/spice2x/games/gitadora/asio.h index f4b5685..d43ce94 100644 --- a/src/spice2x/games/gitadora/asio.h +++ b/src/spice2x/games/gitadora/asio.h @@ -1,12 +1,12 @@ -#pragma once - -namespace games::gitadora { - - // installs IAT registry hooks in gfdm.dll that redirect the game's - // ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen - // driver name read from games::gitadora::ASIO_DRIVER. - // - // safe to call unconditionally; if ASIO_DRIVER is unset the hooks - // forward every call straight through to advapi32. - void asio_hook_init(); -} +#pragma once + +namespace games::gitadora { + + // installs IAT registry hooks in gfdm.dll that redirect the game's + // ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen + // driver name read from games::gitadora::ASIO_DRIVER. + // + // safe to call unconditionally; if ASIO_DRIVER is unset the hooks + // forward every call straight through to advapi32. + void asio_hook_init(); +} diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index e7f8b8d..998457e 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -41,6 +41,7 @@ namespace games::gitadora { bool ARENA_TWO_HEAD_EXCLUSIVE = false; std::optional ASIO_DRIVER = std::nullopt; bool ALLOW_REALTEK_AUDIO = false; + bool NATIVE_TOUCH = false; /* * Prevent GitaDora from creating folders on F drive @@ -809,17 +810,16 @@ namespace games::gitadora { hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true; } - // Single-window mode needs touch injection for its overlay. Two-head fullscreen - // mode uses the real SMALL output, so it only needs the display-topology shim. - if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { - // enable touch hook for subscreen overlay - const auto native_touch_ready = !wintouchemu::FORCE && - nativetouch::hook(avs::game::DLL_INSTANCE); - if (!native_touch_ready) { - wintouchemu::FORCE = true; - wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; - wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); - } + // touch injection drives mouse-as-touch and API touch for the subscreen, + // no matter whether it is drawn by the overlay (single-window mode) or by + // the dedicated SMALL window + NATIVE_TOUCH = !wintouchemu::FORCE && + nativetouch::hook(avs::game::DLL_INSTANCE); + if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) { + // the legacy fallback can only feed the subscreen overlay + wintouchemu::FORCE = true; + wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; + wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); } #if !SPICE_XP diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index 7851949..32aa8e5 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -23,6 +23,11 @@ namespace games::gitadora { extern bool ARENA_TWO_HEAD_EXCLUSIVE; extern std::optional ASIO_DRIVER; extern bool ALLOW_REALTEK_AUDIO; + extern bool NATIVE_TOUCH; + + // arena SMALL subscreen (touch panel) resolution + static constexpr int ARENA_SUBSCREEN_WIDTH = 800; + static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280; class GitaDoraGame : public games::Game { public: diff --git a/src/spice2x/games/iidx/iidx.cpp b/src/spice2x/games/iidx/iidx.cpp index 5f6ecec..dc1746d 100644 --- a/src/spice2x/games/iidx/iidx.cpp +++ b/src/spice2x/games/iidx/iidx.cpp @@ -279,6 +279,26 @@ namespace games::iidx { return nullptr; } + // best-effort read of a TDJ ROM file to determine if the game is running in TDJ mode + // + // these paths are not emulated - they hit whatever is actually mounted at that drive letter. + // an empty optical or removable drive raises the modal "insert a disk" error (the launcher + // clears SEM_FAILCRITICALERRORS process-wide) and a downed network drive stalls on redirector + // timeouts, so only probe what a TDJ cabinet would actually be laid out on. + // drive_path must be absolute and start with a drive letter. + static bool tdj_rom_matches(const char *drive_path, const char *expected) { + const wchar_t root[] = { (wchar_t) drive_path[0], L':', L'\\', L'\0' }; + const auto drive_type = GetDriveTypeW(root); + + if (drive_type != DRIVE_FIXED && drive_type != DRIVE_RAMDISK) { + log_misc("iidx", "not probing '{}' for TDJ, not a local disk (drive type {})", + drive_path, drive_type); + return false; + } + + return fileutils::text_read(drive_path) == expected; + } + #endif IIDXGame::IIDXGame() : Game("Beatmania IIDX") { @@ -339,8 +359,10 @@ namespace games::iidx { HAS_LIBAIO = true; // check TDJ mode - TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA"; - TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ"; + if (!TDJ_MODE) { + TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA") + || tdj_rom_matches("D:\\001rom.txt", "TDJ"); + } // force TDJ mode if (TDJ_MODE) { diff --git a/src/spice2x/games/iidx/mf_wrappers.cpp b/src/spice2x/games/iidx/mf_wrappers.cpp index 068573b..0097da5 100644 --- a/src/spice2x/games/iidx/mf_wrappers.cpp +++ b/src/spice2x/games/iidx/mf_wrappers.cpp @@ -1,132 +1,132 @@ -#include "mf_wrappers.h" -#include "util/libutils.h" -#include "util/logging.h" - -namespace games::iidx { - - static bool INITIALIZED = false; - - static HMODULE mf_dll = nullptr; - static HMODULE mfreadwrite_dll = nullptr; - static HMODULE mfplat_dll = nullptr; - - typedef HRESULT (__stdcall * MFCreateAttributes_t)( - _Out_ IMFAttributes** ppMFAttributes, - _In_ UINT32 cInitialSize - ); - - typedef HRESULT (__stdcall * MFCreateMediaType_t)( - _Out_ IMFMediaType** ppMFType - ); - - typedef HRESULT (__stdcall * MFEnumDeviceSources_t)( - _In_ IMFAttributes* pAttributes, - _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, - _Out_ UINT32* pcSourceActivate - ); - - typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)( - _In_ IMFMediaSource *pMediaSource, - _In_opt_ IMFAttributes *pAttributes, - _Out_ IMFSourceReader **ppSourceReader - ); - - typedef HRESULT (__stdcall * MFGetService_t)( - IUnknown* punkObject, - REFGUID guidService, - REFIID riid, - _Outptr_ LPVOID* ppvObject - ); - - static MFCreateAttributes_t MFCreateAttributes = nullptr; - static MFCreateMediaType_t MFCreateMediaType = nullptr; - static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr; - static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr; - static MFGetService_t MFGetService = nullptr; - - void init_mf_library() { - - // why was all of this needed? - // - // when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib - // this made Unity-based really unhappy, causing them to skip over the logic that loads mf library - // causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt) - // - // as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file - - if (INITIALIZED) { - return; - } - INITIALIZED = true; - - log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN"); - - mf_dll = libutils::load_library("mf.dll", true); - mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true); - mfplat_dll = libutils::load_library("mfplat.dll", true); - - MFCreateAttributes = (MFCreateAttributes_t) - libutils::get_proc(mfplat_dll, "MFCreateAttributes"); - if (!MFCreateAttributes) { - log_fatal("mf_wrappers", "MFCreateAttributes failed to hook"); - } - - MFCreateMediaType = (MFCreateMediaType_t) - libutils::get_proc(mfplat_dll, "MFCreateMediaType"); - if (!MFCreateMediaType) { - log_fatal("mf_wrappers", "MFCreateMediaType failed to hook"); - } - - MFEnumDeviceSources = (MFEnumDeviceSources_t) - libutils::get_proc(mf_dll, "MFEnumDeviceSources"); - if (!MFEnumDeviceSources) { - log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook"); - } - - MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t) - libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource"); - if (!MFCreateSourceReaderFromMediaSource) { - log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook"); - } - - MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService"); - if (!MFGetService) { - log_fatal("mf_wrappers", "MFGetService failed to hook"); - } - - log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE"); - } - - HRESULT WrappedMFCreateAttributes ( - _Out_ IMFAttributes** ppMFAttributes, - _In_ UINT32 cInitialSize) { - return MFCreateAttributes(ppMFAttributes, cInitialSize); - } - - HRESULT WrappedMFCreateMediaType ( - _Out_ IMFMediaType** ppMFType) { - return MFCreateMediaType(ppMFType); - } - - HRESULT WrappedMFEnumDeviceSources ( - _In_ IMFAttributes* pAttributes, - _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, - _Out_ UINT32* pcSourceActivate) { - return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate); - } - - HRESULT WrappedMFCreateSourceReaderFromMediaSource ( - _In_ IMFMediaSource *pMediaSource, - _In_opt_ IMFAttributes *pAttributes, - _Out_ IMFSourceReader **ppSourceReader) { - return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader); - } - - HRESULT WrappedMFGetService ( - IUnknown* punkObject, - REFGUID guidService, - REFIID riid, - _Outptr_ LPVOID* ppvObject) { - return MFGetService(punkObject, guidService, riid, ppvObject); - } +#include "mf_wrappers.h" +#include "util/libutils.h" +#include "util/logging.h" + +namespace games::iidx { + + static bool INITIALIZED = false; + + static HMODULE mf_dll = nullptr; + static HMODULE mfreadwrite_dll = nullptr; + static HMODULE mfplat_dll = nullptr; + + typedef HRESULT (__stdcall * MFCreateAttributes_t)( + _Out_ IMFAttributes** ppMFAttributes, + _In_ UINT32 cInitialSize + ); + + typedef HRESULT (__stdcall * MFCreateMediaType_t)( + _Out_ IMFMediaType** ppMFType + ); + + typedef HRESULT (__stdcall * MFEnumDeviceSources_t)( + _In_ IMFAttributes* pAttributes, + _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, + _Out_ UINT32* pcSourceActivate + ); + + typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)( + _In_ IMFMediaSource *pMediaSource, + _In_opt_ IMFAttributes *pAttributes, + _Out_ IMFSourceReader **ppSourceReader + ); + + typedef HRESULT (__stdcall * MFGetService_t)( + IUnknown* punkObject, + REFGUID guidService, + REFIID riid, + _Outptr_ LPVOID* ppvObject + ); + + static MFCreateAttributes_t MFCreateAttributes = nullptr; + static MFCreateMediaType_t MFCreateMediaType = nullptr; + static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr; + static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr; + static MFGetService_t MFGetService = nullptr; + + void init_mf_library() { + + // why was all of this needed? + // + // when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib + // this made Unity-based really unhappy, causing them to skip over the logic that loads mf library + // causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt) + // + // as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file + + if (INITIALIZED) { + return; + } + INITIALIZED = true; + + log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN"); + + mf_dll = libutils::load_library("mf.dll", true); + mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true); + mfplat_dll = libutils::load_library("mfplat.dll", true); + + MFCreateAttributes = (MFCreateAttributes_t) + libutils::get_proc(mfplat_dll, "MFCreateAttributes"); + if (!MFCreateAttributes) { + log_fatal("mf_wrappers", "MFCreateAttributes failed to hook"); + } + + MFCreateMediaType = (MFCreateMediaType_t) + libutils::get_proc(mfplat_dll, "MFCreateMediaType"); + if (!MFCreateMediaType) { + log_fatal("mf_wrappers", "MFCreateMediaType failed to hook"); + } + + MFEnumDeviceSources = (MFEnumDeviceSources_t) + libutils::get_proc(mf_dll, "MFEnumDeviceSources"); + if (!MFEnumDeviceSources) { + log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook"); + } + + MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t) + libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource"); + if (!MFCreateSourceReaderFromMediaSource) { + log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook"); + } + + MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService"); + if (!MFGetService) { + log_fatal("mf_wrappers", "MFGetService failed to hook"); + } + + log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE"); + } + + HRESULT WrappedMFCreateAttributes ( + _Out_ IMFAttributes** ppMFAttributes, + _In_ UINT32 cInitialSize) { + return MFCreateAttributes(ppMFAttributes, cInitialSize); + } + + HRESULT WrappedMFCreateMediaType ( + _Out_ IMFMediaType** ppMFType) { + return MFCreateMediaType(ppMFType); + } + + HRESULT WrappedMFEnumDeviceSources ( + _In_ IMFAttributes* pAttributes, + _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, + _Out_ UINT32* pcSourceActivate) { + return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate); + } + + HRESULT WrappedMFCreateSourceReaderFromMediaSource ( + _In_ IMFMediaSource *pMediaSource, + _In_opt_ IMFAttributes *pAttributes, + _Out_ IMFSourceReader **ppSourceReader) { + return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader); + } + + HRESULT WrappedMFGetService ( + IUnknown* punkObject, + REFGUID guidService, + REFIID riid, + _Outptr_ LPVOID* ppvObject) { + return MFGetService(punkObject, guidService, riid, ppvObject); + } } \ No newline at end of file diff --git a/src/spice2x/games/iidx/mf_wrappers.h b/src/spice2x/games/iidx/mf_wrappers.h index 1dba852..b574709 100644 --- a/src/spice2x/games/iidx/mf_wrappers.h +++ b/src/spice2x/games/iidx/mf_wrappers.h @@ -1,33 +1,33 @@ -#include -#include -#include -#include - -#pragma once - -namespace games::iidx { - void init_mf_library(); - - HRESULT WrappedMFCreateAttributes ( - _Out_ IMFAttributes** ppMFAttributes, - _In_ UINT32 cInitialSize); - - HRESULT WrappedMFCreateMediaType ( - _Out_ IMFMediaType** ppMFType); - - HRESULT WrappedMFEnumDeviceSources ( - _In_ IMFAttributes* pAttributes, - _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, - _Out_ UINT32* pcSourceActivate); - - HRESULT WrappedMFCreateSourceReaderFromMediaSource ( - _In_ IMFMediaSource *pMediaSource, - _In_opt_ IMFAttributes *pAttributes, - _Out_ IMFSourceReader **ppSourceReader); - - HRESULT WrappedMFGetService ( - IUnknown* punkObject, - REFGUID guidService, - REFIID riid, - _Outptr_ LPVOID* ppvObject); +#include +#include +#include +#include + +#pragma once + +namespace games::iidx { + void init_mf_library(); + + HRESULT WrappedMFCreateAttributes ( + _Out_ IMFAttributes** ppMFAttributes, + _In_ UINT32 cInitialSize); + + HRESULT WrappedMFCreateMediaType ( + _Out_ IMFMediaType** ppMFType); + + HRESULT WrappedMFEnumDeviceSources ( + _In_ IMFAttributes* pAttributes, + _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, + _Out_ UINT32* pcSourceActivate); + + HRESULT WrappedMFCreateSourceReaderFromMediaSource ( + _In_ IMFMediaSource *pMediaSource, + _In_opt_ IMFAttributes *pAttributes, + _Out_ IMFSourceReader **ppSourceReader); + + HRESULT WrappedMFGetService ( + IUnknown* punkObject, + REFGUID guidService, + REFIID riid, + _Outptr_ LPVOID* ppvObject); } \ No newline at end of file diff --git a/src/spice2x/games/nost/touch_mode.cpp b/src/spice2x/games/nost/touch_mode.cpp index dd918a3..907efd1 100644 --- a/src/spice2x/games/nost/touch_mode.cpp +++ b/src/spice2x/games/nost/touch_mode.cpp @@ -1,233 +1,233 @@ -#include "touch_mode.h" - -#include -#include -#include - -#include "touch/native/nativetouchhook.h" -#include "util/logging.h" - -namespace games::nost::touch_mode { - - // native contact positions feed piano input directly. native events reach the game - // in nav mode and are suppressed in piano mode. mode-button contacts are always - // suppressed and change that routing after release. - static constexpr LONG PIANO_LEFT_GAP = 11; - static constexpr LONG PIANO_RIGHT_GAP = 10; - static constexpr uint32_t PIANO_KEY_COUNT = 28; - - struct TouchGeometry { - HWND window = nullptr; - RECT mode_button {}; - LONG client_width = 0; - LONG client_height = 0; - - bool valid() const { - return window != nullptr && client_width > 0 && client_height > 0; - } - }; - - struct NativeContact { - POINT position {}; - - // position contains game-client coordinates - bool client_position_valid = false; - - // down began on the mode switch button; remains true through up - bool mode_button = false; - }; - - static std::atomic_bool accept_events { false }; - static std::atomic current_mode_state { Mode::Nav }; - static std::mutex state_mutex; - static TouchGeometry touch_geometry; - - // native contacts are kept by ID so each contact contributes exactly one position - static std::unordered_map active_contacts; - - // a hardware button release requests one change after all contacts are released - static bool mode_change_pending = false; - - static void reset_state_locked() { - current_mode_state.store(Mode::Nav, std::memory_order_release); - touch_geometry = {}; - active_contacts.clear(); - mode_change_pending = false; - } - - // hardware contacts arrive in screen coordinates - static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) { - if (!touch_geometry.valid()) { - return false; - } - - POINT position { event.x, event.y }; - if (!ScreenToClient(touch_geometry.window, &position)) { - return false; - } - return PtInRect(&touch_geometry.mode_button, position) != FALSE; - } - - static bool update_touch_state(const nativetouch::NativeTouchEvent &event) { - std::lock_guard lock(state_mutex); - - // first, process down / move events - if (event.down || event.move) { - auto contact = active_contacts.try_emplace(event.id).first; - - // keep track of IDs that began as a down on the mode switch button - if (event.down) { - contact->second.mode_button = native_touch_in_button(event); - } - - // check for valid position - POINT position { event.x, event.y }; - if (touch_geometry.window != nullptr && - ScreenToClient(touch_geometry.window, &position)) { - contact->second.position = position; - contact->second.client_position_valid = true; - } - } - - const auto contact = active_contacts.find(event.id); - const bool mode_button_contact = contact != active_contacts.end() && - contact->second.mode_button; - - // process up events - if (event.up) { - active_contacts.erase(event.id); - - // if a contact that began down event on the mode switch button has - // been released, a mode switch is now pending - if (mode_button_contact) { - mode_change_pending = true; - } - - // apply the change on the final hardware up. switching earlier would - // split another contact's down and up events across different modes - if (mode_change_pending && active_contacts.empty()) { - mode_change_pending = false; - const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav; - current_mode_state.store(next_mode, std::memory_order_release); - } - } - return mode_button_contact; - } - - // install the Nostalgia-specific native touch interception - void enable() { - if (accept_events.exchange(true, std::memory_order_acq_rel)) { - return; - } - - { - std::lock_guard lock(state_mutex); - reset_state_locked(); - } - - nativetouch::set_input_filter(filter_native_touch); - log_info("nost::touch", "enabled"); - } - - void disable() { - if (!accept_events.exchange(false, std::memory_order_acq_rel)) { - return; - } - - nativetouch::set_input_filter(nullptr); - - std::lock_guard lock(state_mutex); - reset_state_locked(); - } - - bool enabled() { - return accept_events.load(std::memory_order_acquire); - } - - Mode current_mode() { - return current_mode_state.load(std::memory_order_acquire); - } - - // publish the rendered overlay button rectangle in game-client pixels - void publish_button_bounds(HWND window, const RECT &client_bounds) { - TouchGeometry next {}; - RECT client_rect {}; - if (window != nullptr && GetClientRect(window, &client_rect) && - client_rect.right > 0 && client_rect.bottom > 0) { - next.window = window; - next.mode_button = client_bounds; - next.client_width = client_rect.right; - next.client_height = client_rect.bottom; - } - - std::lock_guard lock(state_mutex); - touch_geometry = next; - } - - // return the active 28-key piano bitfield for the PANB input update - uint32_t piano_key_state() { - if (!enabled() || current_mode() != Mode::Piano) { - return 0; - } - - std::lock_guard lock(state_mutex); - if (current_mode() != Mode::Piano || !touch_geometry.valid()) { - return 0; - } - - uint32_t state = 0; - for (const auto &contact : active_contacts) { - // invalid position or mode-button contact; ignore these contacts - if (!contact.second.client_position_valid || contact.second.mode_button) { - continue; - } - - const auto &position = contact.second.position; - - // outside the client area or on the mode button; ignore these contacts - if (position.x < 0 || position.x >= touch_geometry.client_width || - position.y < 0 || position.y >= touch_geometry.client_height || - PtInRect(&touch_geometry.mode_button, position)) { - continue; - } - - // divide the inset width evenly; touches in either side gap clamp to - // the nearest outer key so the physical screen edges remain playable - const auto piano_width = - touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP; - uint32_t key = 0; - if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) { - key = PIANO_KEY_COUNT - 1; - } else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) { - key = static_cast( - (position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width); - } - state |= UINT32_C(1) << key; - } - - return state; - } - - // update native contacts and report whether this event should be hidden from the game - bool filter_native_touch(const nativetouch::NativeTouchEvent &event) { - // synthetic events are outside this hardware-only feature - if (!enabled() || event.synthetic) { - // false leaves the event visible to the game - return false; - } - - // snapshot routing before an up event can commit a pending mode switch - const bool piano_mode_before_update = current_mode() == Mode::Piano; - - // update the contact lifetime and commit any pending switch when safe - const bool mode_button_contact = update_touch_state(event); - - // hide every event in a contact that began on the mode switch button - if (mode_button_contact) { - return true; - } - - // piano mode consumes hardware events; nav mode forwards them to the game - return piano_mode_before_update; - } -} +#include "touch_mode.h" + +#include +#include +#include + +#include "touch/native/nativetouchhook.h" +#include "util/logging.h" + +namespace games::nost::touch_mode { + + // native contact positions feed piano input directly. native events reach the game + // in nav mode and are suppressed in piano mode. mode-button contacts are always + // suppressed and change that routing after release. + static constexpr LONG PIANO_LEFT_GAP = 11; + static constexpr LONG PIANO_RIGHT_GAP = 10; + static constexpr uint32_t PIANO_KEY_COUNT = 28; + + struct TouchGeometry { + HWND window = nullptr; + RECT mode_button {}; + LONG client_width = 0; + LONG client_height = 0; + + bool valid() const { + return window != nullptr && client_width > 0 && client_height > 0; + } + }; + + struct NativeContact { + POINT position {}; + + // position contains game-client coordinates + bool client_position_valid = false; + + // down began on the mode switch button; remains true through up + bool mode_button = false; + }; + + static std::atomic_bool accept_events { false }; + static std::atomic current_mode_state { Mode::Nav }; + static std::mutex state_mutex; + static TouchGeometry touch_geometry; + + // native contacts are kept by ID so each contact contributes exactly one position + static std::unordered_map active_contacts; + + // a hardware button release requests one change after all contacts are released + static bool mode_change_pending = false; + + static void reset_state_locked() { + current_mode_state.store(Mode::Nav, std::memory_order_release); + touch_geometry = {}; + active_contacts.clear(); + mode_change_pending = false; + } + + // hardware contacts arrive in screen coordinates + static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) { + if (!touch_geometry.valid()) { + return false; + } + + POINT position { event.x, event.y }; + if (!ScreenToClient(touch_geometry.window, &position)) { + return false; + } + return PtInRect(&touch_geometry.mode_button, position) != FALSE; + } + + static bool update_touch_state(const nativetouch::NativeTouchEvent &event) { + std::lock_guard lock(state_mutex); + + // first, process down / move events + if (event.down || event.move) { + auto contact = active_contacts.try_emplace(event.id).first; + + // keep track of IDs that began as a down on the mode switch button + if (event.down) { + contact->second.mode_button = native_touch_in_button(event); + } + + // check for valid position + POINT position { event.x, event.y }; + if (touch_geometry.window != nullptr && + ScreenToClient(touch_geometry.window, &position)) { + contact->second.position = position; + contact->second.client_position_valid = true; + } + } + + const auto contact = active_contacts.find(event.id); + const bool mode_button_contact = contact != active_contacts.end() && + contact->second.mode_button; + + // process up events + if (event.up) { + active_contacts.erase(event.id); + + // if a contact that began down event on the mode switch button has + // been released, a mode switch is now pending + if (mode_button_contact) { + mode_change_pending = true; + } + + // apply the change on the final hardware up. switching earlier would + // split another contact's down and up events across different modes + if (mode_change_pending && active_contacts.empty()) { + mode_change_pending = false; + const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav; + current_mode_state.store(next_mode, std::memory_order_release); + } + } + return mode_button_contact; + } + + // install the Nostalgia-specific native touch interception + void enable() { + if (accept_events.exchange(true, std::memory_order_acq_rel)) { + return; + } + + { + std::lock_guard lock(state_mutex); + reset_state_locked(); + } + + nativetouch::set_input_filter(filter_native_touch); + log_info("nost::touch", "enabled"); + } + + void disable() { + if (!accept_events.exchange(false, std::memory_order_acq_rel)) { + return; + } + + nativetouch::set_input_filter(nullptr); + + std::lock_guard lock(state_mutex); + reset_state_locked(); + } + + bool enabled() { + return accept_events.load(std::memory_order_acquire); + } + + Mode current_mode() { + return current_mode_state.load(std::memory_order_acquire); + } + + // publish the rendered overlay button rectangle in game-client pixels + void publish_button_bounds(HWND window, const RECT &client_bounds) { + TouchGeometry next {}; + RECT client_rect {}; + if (window != nullptr && GetClientRect(window, &client_rect) && + client_rect.right > 0 && client_rect.bottom > 0) { + next.window = window; + next.mode_button = client_bounds; + next.client_width = client_rect.right; + next.client_height = client_rect.bottom; + } + + std::lock_guard lock(state_mutex); + touch_geometry = next; + } + + // return the active 28-key piano bitfield for the PANB input update + uint32_t piano_key_state() { + if (!enabled() || current_mode() != Mode::Piano) { + return 0; + } + + std::lock_guard lock(state_mutex); + if (current_mode() != Mode::Piano || !touch_geometry.valid()) { + return 0; + } + + uint32_t state = 0; + for (const auto &contact : active_contacts) { + // invalid position or mode-button contact; ignore these contacts + if (!contact.second.client_position_valid || contact.second.mode_button) { + continue; + } + + const auto &position = contact.second.position; + + // outside the client area or on the mode button; ignore these contacts + if (position.x < 0 || position.x >= touch_geometry.client_width || + position.y < 0 || position.y >= touch_geometry.client_height || + PtInRect(&touch_geometry.mode_button, position)) { + continue; + } + + // divide the inset width evenly; touches in either side gap clamp to + // the nearest outer key so the physical screen edges remain playable + const auto piano_width = + touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP; + uint32_t key = 0; + if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) { + key = PIANO_KEY_COUNT - 1; + } else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) { + key = static_cast( + (position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width); + } + state |= UINT32_C(1) << key; + } + + return state; + } + + // update native contacts and report whether this event should be hidden from the game + bool filter_native_touch(const nativetouch::NativeTouchEvent &event) { + // synthetic events are outside this hardware-only feature + if (!enabled() || event.synthetic) { + // false leaves the event visible to the game + return false; + } + + // snapshot routing before an up event can commit a pending mode switch + const bool piano_mode_before_update = current_mode() == Mode::Piano; + + // update the contact lifetime and commit any pending switch when safe + const bool mode_button_contact = update_touch_state(event); + + // hide every event in a contact that began on the mode switch button + if (mode_button_contact) { + return true; + } + + // piano mode consumes hardware events; nav mode forwards them to the game + return piano_mode_before_update; + } +} diff --git a/src/spice2x/games/nost/touch_mode.h b/src/spice2x/games/nost/touch_mode.h index 7ef19fb..c478e13 100644 --- a/src/spice2x/games/nost/touch_mode.h +++ b/src/spice2x/games/nost/touch_mode.h @@ -1,27 +1,27 @@ -#pragma once - -#include -#include - -#include "touch/native/nativetouchhook.h" - -namespace games::nost::touch_mode { - - // nav mode forwards contacts to the game; piano mode converts them into piano keys - enum class Mode { - Nav, - Piano, - }; - - void enable(); - void disable(); - bool enabled(); - - Mode current_mode(); - - void publish_button_bounds(HWND window, const RECT &client_bounds); - - uint32_t piano_key_state(); - - bool filter_native_touch(const nativetouch::NativeTouchEvent &event); -} +#pragma once + +#include +#include + +#include "touch/native/nativetouchhook.h" + +namespace games::nost::touch_mode { + + // nav mode forwards contacts to the game; piano mode converts them into piano keys + enum class Mode { + Nav, + Piano, + }; + + void enable(); + void disable(); + bool enabled(); + + Mode current_mode(); + + void publish_button_bounds(HWND window, const RECT &client_bounds); + + uint32_t piano_key_state(); + + bool filter_native_touch(const nativetouch::NativeTouchEvent &event); +} diff --git a/src/spice2x/games/rb/touch_debug.cpp b/src/spice2x/games/rb/touch_debug.cpp index 8419377..7a4b2f7 100644 --- a/src/spice2x/games/rb/touch_debug.cpp +++ b/src/spice2x/games/rb/touch_debug.cpp @@ -1,143 +1,143 @@ -#include "touch_debug.h" - -#include -#include -#include -#include - -#include "external/imgui/imgui.h" -#include "games/rb/rb.h" -#include "games/rb/touch_defs.h" - -namespace games::rb { - - struct TouchDebugState { - std::array packet {}; - bool is_landscape = false; - }; - - std::atomic_bool TOUCH_DEBUG_OVERLAY = false; - static std::atomic_bool TOUCH_ACTIVE = false; - static std::mutex TOUCH_DEBUG_STATE_M; - static TouchDebugState TOUCH_DEBUG_STATE; - - static float touch_scale_factor() { - return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT; - } - - static void clear_touch_debug_state() { - std::lock_guard lock(TOUCH_DEBUG_STATE_M); - TOUCH_DEBUG_STATE = {}; - } - - static TouchDebugState get_touch_debug_state() { - std::lock_guard lock(TOUCH_DEBUG_STATE_M); - return TOUCH_DEBUG_STATE; - } - - static bool packet_bit_active( - const std::array &packet, int bit) { - return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0; - } - - static int sensor_center(int sensor, int sensor_count, int extent) { - return ((sensor * 2 + 1) * extent) / (sensor_count * 2); - } - - static float sensor_span_position(int sensor, int sensor_count, int extent) { - return sensor * (extent - 1) / (float) (sensor_count - 1); - } - - bool touch_debug_overlay_enabled() { - return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire); - } - - void touch_draw_debug_overlay() { - if (!touch_debug_overlay_enabled()) { - return; - } - - const auto &io = ImGui::GetIO(); - int width = static_cast(io.DisplaySize.x); - int height = static_cast(io.DisplaySize.y); - if (width <= 0 || height <= 0) { - return; - } - - const float scale_factor = touch_scale_factor(); - const float left = width * (1.f - scale_factor) / 2.f; - const float top = height * (1.f - scale_factor) / 2.f; - const float right = width - left; - const float bottom = height - top; - - TouchDebugState state = get_touch_debug_state(); - ImDrawList *draw_list = ImGui::GetBackgroundDrawList(); - auto draw_line = [&](float x1, float y1, float x2, float y2) { - draw_list->AddLine( - ImVec2(x1, y1), ImVec2(x2, y2), - IM_COL32(0, 255, 64, 255), 2.f); - }; - - // show the valid input area when touch scaling restricts it - if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) { - draw_list->AddRect( - ImVec2(left, top), ImVec2(right, bottom), - IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f); - } - - // spread the usable X sensors 2..45 from edge to edge - for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) { - if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) { - continue; - } - - float position = sensor_span_position( - sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT, - state.is_landscape ? height : width); - if (state.is_landscape) { - float y = top + position * scale_factor; - draw_line(left, y, right, y); - } else { - float x = left + position * scale_factor; - draw_line(x, top, x, bottom); - } - } - - for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) { - if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) { - continue; - } - - int position = sensor_center( - sensor, Y_SENSOR_COUNT, - state.is_landscape ? width : height); - if (state.is_landscape) { - float x = right - position * scale_factor; - draw_line(x, top, x, bottom); - } else { - float y = top + position * scale_factor; - draw_line(left, y, right, y); - } - } - } - - void touch_debug_attach() { - clear_touch_debug_state(); - TOUCH_ACTIVE.store(true, std::memory_order_release); - } - - void touch_debug_detach() { - TOUCH_ACTIVE.store(false, std::memory_order_release); - clear_touch_debug_state(); - } - - void touch_debug_publish(const unsigned char *data, bool is_landscape) { - if (!TOUCH_DEBUG_OVERLAY) { - return; - } - - std::lock_guard lock(TOUCH_DEBUG_STATE_M); - memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE); - TOUCH_DEBUG_STATE.is_landscape = is_landscape; - } -} +#include "touch_debug.h" + +#include +#include +#include +#include + +#include "external/imgui/imgui.h" +#include "games/rb/rb.h" +#include "games/rb/touch_defs.h" + +namespace games::rb { + + struct TouchDebugState { + std::array packet {}; + bool is_landscape = false; + }; + + std::atomic_bool TOUCH_DEBUG_OVERLAY = false; + static std::atomic_bool TOUCH_ACTIVE = false; + static std::mutex TOUCH_DEBUG_STATE_M; + static TouchDebugState TOUCH_DEBUG_STATE; + + static float touch_scale_factor() { + return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT; + } + + static void clear_touch_debug_state() { + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + TOUCH_DEBUG_STATE = {}; + } + + static TouchDebugState get_touch_debug_state() { + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + return TOUCH_DEBUG_STATE; + } + + static bool packet_bit_active( + const std::array &packet, int bit) { + return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0; + } + + static int sensor_center(int sensor, int sensor_count, int extent) { + return ((sensor * 2 + 1) * extent) / (sensor_count * 2); + } + + static float sensor_span_position(int sensor, int sensor_count, int extent) { + return sensor * (extent - 1) / (float) (sensor_count - 1); + } + + bool touch_debug_overlay_enabled() { + return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire); + } + + void touch_draw_debug_overlay() { + if (!touch_debug_overlay_enabled()) { + return; + } + + const auto &io = ImGui::GetIO(); + int width = static_cast(io.DisplaySize.x); + int height = static_cast(io.DisplaySize.y); + if (width <= 0 || height <= 0) { + return; + } + + const float scale_factor = touch_scale_factor(); + const float left = width * (1.f - scale_factor) / 2.f; + const float top = height * (1.f - scale_factor) / 2.f; + const float right = width - left; + const float bottom = height - top; + + TouchDebugState state = get_touch_debug_state(); + ImDrawList *draw_list = ImGui::GetBackgroundDrawList(); + auto draw_line = [&](float x1, float y1, float x2, float y2) { + draw_list->AddLine( + ImVec2(x1, y1), ImVec2(x2, y2), + IM_COL32(0, 255, 64, 255), 2.f); + }; + + // show the valid input area when touch scaling restricts it + if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) { + draw_list->AddRect( + ImVec2(left, top), ImVec2(right, bottom), + IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f); + } + + // spread the usable X sensors 2..45 from edge to edge + for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) { + if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) { + continue; + } + + float position = sensor_span_position( + sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT, + state.is_landscape ? height : width); + if (state.is_landscape) { + float y = top + position * scale_factor; + draw_line(left, y, right, y); + } else { + float x = left + position * scale_factor; + draw_line(x, top, x, bottom); + } + } + + for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) { + if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) { + continue; + } + + int position = sensor_center( + sensor, Y_SENSOR_COUNT, + state.is_landscape ? width : height); + if (state.is_landscape) { + float x = right - position * scale_factor; + draw_line(x, top, x, bottom); + } else { + float y = top + position * scale_factor; + draw_line(left, y, right, y); + } + } + } + + void touch_debug_attach() { + clear_touch_debug_state(); + TOUCH_ACTIVE.store(true, std::memory_order_release); + } + + void touch_debug_detach() { + TOUCH_ACTIVE.store(false, std::memory_order_release); + clear_touch_debug_state(); + } + + void touch_debug_publish(const unsigned char *data, bool is_landscape) { + if (!TOUCH_DEBUG_OVERLAY) { + return; + } + + std::lock_guard lock(TOUCH_DEBUG_STATE_M); + memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE); + TOUCH_DEBUG_STATE.is_landscape = is_landscape; + } +} diff --git a/src/spice2x/games/rb/touch_debug.h b/src/spice2x/games/rb/touch_debug.h index 1abd14d..75516da 100644 --- a/src/spice2x/games/rb/touch_debug.h +++ b/src/spice2x/games/rb/touch_debug.h @@ -1,14 +1,14 @@ -#pragma once - -#include - -namespace games::rb { - extern std::atomic_bool TOUCH_DEBUG_OVERLAY; - - bool touch_debug_overlay_enabled(); - void touch_draw_debug_overlay(); - - void touch_debug_attach(); - void touch_debug_detach(); - void touch_debug_publish(const unsigned char *data, bool is_landscape); -} +#pragma once + +#include + +namespace games::rb { + extern std::atomic_bool TOUCH_DEBUG_OVERLAY; + + bool touch_debug_overlay_enabled(); + void touch_draw_debug_overlay(); + + void touch_debug_attach(); + void touch_debug_detach(); + void touch_debug_publish(const unsigned char *data, bool is_landscape); +} diff --git a/src/spice2x/games/rb/touch_defs.h b/src/spice2x/games/rb/touch_defs.h index f0c2474..b5de3e8 100644 --- a/src/spice2x/games/rb/touch_defs.h +++ b/src/spice2x/games/rb/touch_defs.h @@ -1,17 +1,17 @@ -#pragma once - -namespace games::rb { - inline constexpr int TOUCH_SCALE_DEFAULT = 1000; - inline constexpr int TOUCH_PACKET_SIZE = 20; - inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3; - - inline constexpr int X_SENSOR_COUNT = 48; - inline constexpr int X_SENSOR_FIRST_ACTIVE = 2; - inline constexpr int X_SENSOR_LAST_ACTIVE = 45; - inline constexpr int X_SENSOR_ACTIVE_COUNT = - X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1; - inline constexpr int X_SENSOR_FIRST_BIT = 88; - - inline constexpr int Y_SENSOR_COUNT = 76; - inline constexpr int Y_SENSOR_FIRST_BIT = 75; -} +#pragma once + +namespace games::rb { + inline constexpr int TOUCH_SCALE_DEFAULT = 1000; + inline constexpr int TOUCH_PACKET_SIZE = 20; + inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3; + + inline constexpr int X_SENSOR_COUNT = 48; + inline constexpr int X_SENSOR_FIRST_ACTIVE = 2; + inline constexpr int X_SENSOR_LAST_ACTIVE = 45; + inline constexpr int X_SENSOR_ACTIVE_COUNT = + X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1; + inline constexpr int X_SENSOR_FIRST_BIT = 88; + + inline constexpr int Y_SENSOR_COUNT = 76; + inline constexpr int Y_SENSOR_FIRST_BIT = 75; +} diff --git a/src/spice2x/games/sdvx/sdvx_live2d.cpp b/src/spice2x/games/sdvx/sdvx_live2d.cpp index 2d810f0..fdbfc94 100644 --- a/src/spice2x/games/sdvx/sdvx_live2d.cpp +++ b/src/spice2x/games/sdvx/sdvx_live2d.cpp @@ -1,72 +1,72 @@ -#include "sdvx_live2d.h" - -// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is -// compiled out of 32-bit builds. -#ifdef SPICE64 - -#include - -#include "hooks/graphics/graphics.h" -#include "launcher/logger.h" -#include "util/logging.h" - -namespace games::sdvx { - - // Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option). - // - // the game logs scene transitions as "I:Attach: in " / "I:Detach: in - // ". several scenes correspond to in-song gameplay (with the heavy - // Live2D rendering); we watch those log lines and keep the shared flag - // the d3d9 backend reads up to date. the hook never alters the log output - // (always returns false). - static bool live2d_scene_log_hook( - void *user, const std::string &data, logger::Style style, std::string &out) { - // any of these scenes counts as in-song gameplay (different play modes) - static const char *const gameplay_scenes[] = { - "in ALTERNATIVE_GAME_SCENE", - "in MEGAMIX_GAME_SCENE", - "in MEGAMIX_BATTLE", - "in BATTLE_GAME_SCENE", - "in AUTOMATION_GAME_SCENE", - "in ARENA_GAME_SCENE", - }; - bool in_gameplay_scene = false; - for (const auto *scene : gameplay_scenes) { - if (data.find(scene) != std::string::npos) { - in_gameplay_scene = true; - break; - } - } - if (!in_gameplay_scene) { - return false; - } - // note: log messages here must NOT contain any matched scene token, else - // this hook would re-enter itself when the message is pushed. - if (data.find("I:Attach: in ") != std::string::npos) { - if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) { - log_info("sdvx", "Live2D skip: entering gameplay"); - } - } else if (data.find("I:Detach: in ") != std::string::npos) { - if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) { - log_info("sdvx", "Live2D skip: leaving gameplay"); - } - } - return false; - } - - void live2d_scene_detection_init() { - static bool installed = false; - if (installed) { - return; - } - installed = true; - // the logger's hook list is a persistent static, so registering here is - // safe even though this runs before logger::start(). we intentionally do - // NOT log a confirmation now: at this point the log file isn't open yet - // and the message would be dropped. the entering/leaving-gameplay lines - // above provide runtime confirmation once the logger is running. - logger::hook_add(live2d_scene_log_hook, nullptr); - } -} - -#endif // SPICE64 +#include "sdvx_live2d.h" + +// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is +// compiled out of 32-bit builds. +#ifdef SPICE64 + +#include + +#include "hooks/graphics/graphics.h" +#include "launcher/logger.h" +#include "util/logging.h" + +namespace games::sdvx { + + // Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option). + // + // the game logs scene transitions as "I:Attach: in " / "I:Detach: in + // ". several scenes correspond to in-song gameplay (with the heavy + // Live2D rendering); we watch those log lines and keep the shared flag + // the d3d9 backend reads up to date. the hook never alters the log output + // (always returns false). + static bool live2d_scene_log_hook( + void *user, const std::string &data, logger::Style style, std::string &out) { + // any of these scenes counts as in-song gameplay (different play modes) + static const char *const gameplay_scenes[] = { + "in ALTERNATIVE_GAME_SCENE", + "in MEGAMIX_GAME_SCENE", + "in MEGAMIX_BATTLE", + "in BATTLE_GAME_SCENE", + "in AUTOMATION_GAME_SCENE", + "in ARENA_GAME_SCENE", + }; + bool in_gameplay_scene = false; + for (const auto *scene : gameplay_scenes) { + if (data.find(scene) != std::string::npos) { + in_gameplay_scene = true; + break; + } + } + if (!in_gameplay_scene) { + return false; + } + // note: log messages here must NOT contain any matched scene token, else + // this hook would re-enter itself when the message is pushed. + if (data.find("I:Attach: in ") != std::string::npos) { + if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) { + log_info("sdvx", "Live2D skip: entering gameplay"); + } + } else if (data.find("I:Detach: in ") != std::string::npos) { + if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) { + log_info("sdvx", "Live2D skip: leaving gameplay"); + } + } + return false; + } + + void live2d_scene_detection_init() { + static bool installed = false; + if (installed) { + return; + } + installed = true; + // the logger's hook list is a persistent static, so registering here is + // safe even though this runs before logger::start(). we intentionally do + // NOT log a confirmation now: at this point the log file isn't open yet + // and the message would be dropped. the entering/leaving-gameplay lines + // above provide runtime confirmation once the logger is running. + logger::hook_add(live2d_scene_log_hook, nullptr); + } +} + +#endif // SPICE64 diff --git a/src/spice2x/games/sdvx/sdvx_live2d.h b/src/spice2x/games/sdvx/sdvx_live2d.h index 5255806..e3bcaca 100644 --- a/src/spice2x/games/sdvx/sdvx_live2d.h +++ b/src/spice2x/games/sdvx/sdvx_live2d.h @@ -1,14 +1,14 @@ -#pragma once - -namespace games::sdvx { - -#ifdef SPICE64 - // installs the Live2D in-game scene-detection log hook used by the - // -sdvxnolive2d "ingame" option. does not require the SDVX game module to - // be attached, so it can be enabled purely from the launcher option. - // only the Live2D-capable SDVX versions are 64-bit, so this is compiled out - // of 32-bit builds. - void live2d_scene_detection_init(); -#endif - -} +#pragma once + +namespace games::sdvx { + +#ifdef SPICE64 + // installs the Live2D in-game scene-detection log hook used by the + // -sdvxnolive2d "ingame" option. does not require the SDVX game module to + // be attached, so it can be enabled purely from the launcher option. + // only the Live2D-capable SDVX versions are 64-bit, so this is compiled out + // of 32-bit builds. + void live2d_scene_detection_init(); +#endif + +} diff --git a/src/spice2x/hooks/audio/asio_driver_scan.cpp b/src/spice2x/hooks/audio/asio_driver_scan.cpp index 57e1a85..265c17e 100644 --- a/src/spice2x/hooks/audio/asio_driver_scan.cpp +++ b/src/spice2x/hooks/audio/asio_driver_scan.cpp @@ -1,84 +1,84 @@ -#include "asio_driver_scan.h" - -#include - -#include - -#include "util/utils.h" - -namespace hooks::audio { - - static constexpr char ASIO_REG_PATH[] = "software\\asio"; - static constexpr char ASIO_REG_DESC[] = "description"; - - // enumerate a single registry view, appending to entries while merging - // duplicates discovered in another view. Drivers are matched by name (not - // CLSID): the game's ASIO loader selects drivers by name, and some vendors - // register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR - // SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices. - static void scan_view( - REGSAM wow64_flag, - bool is_64bit, - std::vector &entries) { - - HKEY hkEnum = nullptr; - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0, - KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) { - return; - } - - char key_name[256]; - for (DWORD index = 0; - RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS; - index++) { - - // read description (display name), fall back to the key name. - // RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA - // because the latter is unavailable on Windows XP. - char desc[256] = { 0 }; - DWORD size = sizeof(desc); - std::string name = key_name; - HKEY hkDriver = nullptr; - if (RegOpenKeyExA(hkEnum, key_name, 0, - KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) { - DWORD type = 0; - if (RegQueryValueExA(hkDriver, - ASIO_REG_DESC, - nullptr, - &type, - reinterpret_cast(desc), - &size) == ERROR_SUCCESS - && type == REG_SZ && desc[0]) { - // ensure null termination - desc[sizeof(desc) - 1] = '\0'; - name = desc; - } - RegCloseKey(hkDriver); - } - - // merge with an existing entry from the other view (match by name) - const std::string name_lower = strtolower(name); - auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) { - return strtolower(e.name) == name_lower; - }); - if (it == entries.end()) { - entries.push_back({ name }); - it = entries.end() - 1; - } - it->found_32bit |= !is_64bit; - it->found_64bit |= is_64bit; - } - - RegCloseKey(hkEnum); - } - - std::vector scan_asio_drivers() { - std::vector entries; - - // 64-bit view first so it wins ordering when present in both - scan_view(KEY_WOW64_64KEY, true, entries); - scan_view(KEY_WOW64_32KEY, false, entries); - - return entries; - } -} +#include "asio_driver_scan.h" + +#include + +#include + +#include "util/utils.h" + +namespace hooks::audio { + + static constexpr char ASIO_REG_PATH[] = "software\\asio"; + static constexpr char ASIO_REG_DESC[] = "description"; + + // enumerate a single registry view, appending to entries while merging + // duplicates discovered in another view. Drivers are matched by name (not + // CLSID): the game's ASIO loader selects drivers by name, and some vendors + // register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR + // SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices. + static void scan_view( + REGSAM wow64_flag, + bool is_64bit, + std::vector &entries) { + + HKEY hkEnum = nullptr; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0, + KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) { + return; + } + + char key_name[256]; + for (DWORD index = 0; + RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS; + index++) { + + // read description (display name), fall back to the key name. + // RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA + // because the latter is unavailable on Windows XP. + char desc[256] = { 0 }; + DWORD size = sizeof(desc); + std::string name = key_name; + HKEY hkDriver = nullptr; + if (RegOpenKeyExA(hkEnum, key_name, 0, + KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) { + DWORD type = 0; + if (RegQueryValueExA(hkDriver, + ASIO_REG_DESC, + nullptr, + &type, + reinterpret_cast(desc), + &size) == ERROR_SUCCESS + && type == REG_SZ && desc[0]) { + // ensure null termination + desc[sizeof(desc) - 1] = '\0'; + name = desc; + } + RegCloseKey(hkDriver); + } + + // merge with an existing entry from the other view (match by name) + const std::string name_lower = strtolower(name); + auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) { + return strtolower(e.name) == name_lower; + }); + if (it == entries.end()) { + entries.push_back({ name }); + it = entries.end() - 1; + } + it->found_32bit |= !is_64bit; + it->found_64bit |= is_64bit; + } + + RegCloseKey(hkEnum); + } + + std::vector scan_asio_drivers() { + std::vector entries; + + // 64-bit view first so it wins ordering when present in both + scan_view(KEY_WOW64_64KEY, true, entries); + scan_view(KEY_WOW64_32KEY, false, entries); + + return entries; + } +} diff --git a/src/spice2x/hooks/audio/asio_driver_scan.h b/src/spice2x/hooks/audio/asio_driver_scan.h index 25a0c47..2f72123 100644 --- a/src/spice2x/hooks/audio/asio_driver_scan.h +++ b/src/spice2x/hooks/audio/asio_driver_scan.h @@ -1,15 +1,15 @@ -#pragma once - -#include -#include - -namespace hooks::audio { - - struct AsioDriverScanEntry { - std::string name; - bool found_32bit = false; - bool found_64bit = false; - }; - - std::vector scan_asio_drivers(); -} +#pragma once + +#include +#include + +namespace hooks::audio { + + struct AsioDriverScanEntry { + std::string name; + bool found_32bit = false; + bool found_64bit = false; + }; + + std::vector scan_asio_drivers(); +} diff --git a/src/spice2x/hooks/audio/asio_proxy.cpp b/src/spice2x/hooks/audio/asio_proxy.cpp index 8cfbaf3..e57cffb 100644 --- a/src/spice2x/hooks/audio/asio_proxy.cpp +++ b/src/spice2x/hooks/audio/asio_proxy.cpp @@ -1,1067 +1,1067 @@ -#include "asio_proxy.h" - -#include -#include -#include -#include -#include -#include - -#include "external/asio/asiolist.h" -#include "hooks/audio/audio.h" -#include "util/logging.h" -#include "util/utils.h" - -namespace { - - // readable name for an ASIO sample type (e.g. "ASIOSTInt32LSB"), falling back to the - // numeric value for unknown types - const char *asio_sample_type_name(AsioSampleType type) { - switch (type) { - case ASIOSTInt16MSB: return "ASIOSTInt16MSB"; - case ASIOSTInt24MSB: return "ASIOSTInt24MSB"; - case ASIOSTInt32MSB: return "ASIOSTInt32MSB"; - case ASIOSTFloat32MSB: return "ASIOSTFloat32MSB"; - case ASIOSTFloat64MSB: return "ASIOSTFloat64MSB"; - case ASIOSTInt32MSB16: return "ASIOSTInt32MSB16"; - case ASIOSTInt32MSB18: return "ASIOSTInt32MSB18"; - case ASIOSTInt32MSB20: return "ASIOSTInt32MSB20"; - case ASIOSTInt32MSB24: return "ASIOSTInt32MSB24"; - case ASIOSTInt16LSB: return "ASIOSTInt16LSB"; - case ASIOSTInt24LSB: return "ASIOSTInt24LSB"; - case ASIOSTInt32LSB: return "ASIOSTInt32LSB"; - case ASIOSTFloat32LSB: return "ASIOSTFloat32LSB"; - case ASIOSTFloat64LSB: return "ASIOSTFloat64LSB"; - case ASIOSTInt32LSB16: return "ASIOSTInt32LSB16"; - case ASIOSTInt32LSB18: return "ASIOSTInt32LSB18"; - case ASIOSTInt32LSB20: return "ASIOSTInt32LSB20"; - case ASIOSTInt32LSB24: return "ASIOSTInt32LSB24"; - default: return "unknown"; - } - } - - // bytes occupied by one sample of the given ASIO type, or 0 for formats we cannot size - // (used to compute the byte length of a planar channel buffer for raw copies) - int asio_sample_bytes(AsioSampleType type) { - switch (type) { - case ASIOSTInt16LSB: - case ASIOSTInt16MSB: - return 2; - case ASIOSTInt24LSB: - case ASIOSTInt24MSB: - return 3; - case ASIOSTInt32LSB: - case ASIOSTInt32MSB: - case ASIOSTInt32LSB16: - case ASIOSTInt32LSB18: - case ASIOSTInt32LSB20: - case ASIOSTInt32LSB24: - case ASIOSTInt32MSB16: - case ASIOSTInt32MSB18: - case ASIOSTInt32MSB20: - case ASIOSTInt32MSB24: - case ASIOSTFloat32LSB: - case ASIOSTFloat32MSB: - return 4; - case ASIOSTFloat64LSB: - case ASIOSTFloat64MSB: - return 8; - default: - return 0; - } - } - - // readable name for a stereo downmix selection, used in our logs - const char *stereo_downmix_name(WrappedAsio::StereoDownmix mode) { - switch (mode) { - case WrappedAsio::StereoDownmix::None: return "none"; - case WrappedAsio::StereoDownmix::Front: return "front"; - case WrappedAsio::StereoDownmix::Center: return "center"; - case WrappedAsio::StereoDownmix::Rear: return "rear"; - case WrappedAsio::StereoDownmix::Side: return "side"; - default: return "unknown"; - } - } - - // duration in milliseconds of a buffer of the given frame count at a sample rate, or a - // negative sentinel when the frame count or sample rate is unusable - double frames_to_ms(long frames, AsioSampleRate sample_rate) { - if (frames < 0 || sample_rate <= 0.0) { - return -1.0; - } - return (frames * 1000.0) / sample_rate; - } - - // scales one planar ASIO output buffer (frames samples of the given type) by gain in - // place, clamping integer formats so a boost saturates instead of wrapping. unsupported - // formats are left untouched. runs on the driver's realtime thread, so no allocation, - // locking or logging here - void apply_gain_planar(void *buffer, long frames, AsioSampleType type, float gain) { - if (buffer == nullptr || frames <= 0) { - return; - } - switch (type) { - case ASIOSTFloat32LSB: { - auto p = static_cast(buffer); - for (long i = 0; i < frames; i++) { - p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f); - } - break; - } - case ASIOSTFloat64LSB: { - auto p = static_cast(buffer); - for (long i = 0; i < frames; i++) { - p[i] = std::clamp(p[i] * static_cast(gain), -1.0, 1.0); - } - break; - } - case ASIOSTInt16LSB: { - auto p = static_cast(buffer); - for (long i = 0; i < frames; i++) { - p[i] = static_cast( - std::clamp(std::lround(p[i] * gain), -32768L, 32767L)); - } - break; - } - case ASIOSTInt24LSB: { - // packed 24-bit little-endian, 3 bytes per sample - auto bytes = static_cast(buffer); - for (long i = 0; i < frames; i++) { - uint8_t *s = bytes + i * 3; - int32_t v = s[0] | (s[1] << 8) | (s[2] << 16); - if (v & 0x800000) { - v |= ~0xFFFFFF; // sign extend - } - int64_t scaled = std::clamp( - std::llround(static_cast(v) * gain), -8388608, 8388607); - s[0] = scaled & 0xFF; - s[1] = (scaled >> 8) & 0xFF; - s[2] = (scaled >> 16) & 0xFF; - } - break; - } - case ASIOSTInt32LSB: { - auto p = static_cast(buffer); - for (long i = 0; i < frames; i++) { - p[i] = static_cast(std::clamp( - std::llround(static_cast(p[i]) * gain), INT32_MIN, INT32_MAX)); - } - break; - } - default: - // unsupported format (MSB, aligned 32-bit, DSD): leave untouched - break; - } - } - - // one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are - // single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object - // is destroyed and re-created within a process - which the host triggers by leaking and - // re-instantiating the driver during startup probing. so we build the real driver and - // its wrapper once, hold a reference so it survives the host's Release calls, and hand - // the same wrapper back for every later CoCreate. as a result the real driver is created - // and initialized exactly once - std::mutex g_wrappers_mutex; - std::vector> g_wrappers; - - WrappedAsio *find_wrapper(REFCLSID clsid) { - std::lock_guard lock(g_wrappers_mutex); - for (auto &entry : g_wrappers) { - if (IsEqualCLSID(entry.first, clsid)) { - return entry.second; - } - } - - return nullptr; - } - - void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) { - std::lock_guard lock(g_wrappers_mutex); - g_wrappers.emplace_back(clsid, wrapper); - } - - // ASIO drivers registered on this system (CLSID + registry name), scanned once - const std::vector> ®istered_asio_drivers() { - static const std::vector> drivers = [] { - std::vector> result; - AsioDriverList driver_list; - for (const auto &driver : driver_list.driver_list) { - result.emplace_back(driver.clsid, driver.name); - log_info( - "audio::wrappedasio", - "registered ASIO driver: name='{}', clsid={}", - driver.name, - guid2s(driver.clsid)); - } - - log_info("audio::wrappedasio", "discovered {} registered ASIO driver(s)", result.size()); - return result; - }(); - - return drivers; - } - - std::string registered_asio_name(REFCLSID clsid) { - for (const auto &driver : registered_asio_drivers()) { - if (IsEqualCLSID(driver.first, clsid)) { - return driver.second; - } - } - - return guid2s(clsid); - } -} - -namespace hooks::audio::asio { - - bool is_asio_creation(REFCLSID rclsid, REFIID riid) { - - // ASIO hosts request the driver using its own CLSID as the interface id - if (!IsEqualGUID(rclsid, riid)) { - return false; - } - - for (const auto &driver : registered_asio_drivers()) { - if (IsEqualCLSID(driver.first, rclsid)) { - return true; - } - } - - return false; - } -} - -#pragma region IUnknown -WrappedAsio::StereoDownmix WrappedAsio::STEREO_DOWNMIX = WrappedAsio::StereoDownmix::None; -std::atomic WrappedAsio::active_instance {nullptr}; - -WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name) { - if (_stricmp(name, "front") == 0) { - return StereoDownmix::Front; - } else if (_stricmp(name, "center") == 0) { - return StereoDownmix::Center; - } else if (_stricmp(name, "rear") == 0) { - return StereoDownmix::Rear; - } else if (_stricmp(name, "side") == 0) { - return StereoDownmix::Side; - } - - return StereoDownmix::None; -} - -WrappedAsio::~WrappedAsio() { - this->detach_post_process(); - - // never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until - // release_all_wrappers() drops the pin at shutdown - the only point this can fire, and - // only once the host has released its own references. tears down the real driver - this->pReal->Release(); - - log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid)); -} - -HRESULT STDMETHODCALLTYPE WrappedAsio::QueryInterface(REFIID riid, void **ppv) { - if (ppv == nullptr) { - return E_POINTER; - } - - // ASIO hosts query for the driver using its own CLSID as the IID - if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, this->clsid)) { - this->AddRef(); - *ppv = static_cast(this); - - return S_OK; - } - - // the host is asking for some other interface; forward to the real driver. a failure - // here is a common reason a host discards a driver and retries - const HRESULT ret = this->pReal->QueryInterface(riid, ppv); - if (SUCCEEDED(ret)) { - log_info("audio::wrappedasio", "QueryInterface({}) -> forwarded to real driver", guid2s(riid)); - } else { - log_info( - "audio::wrappedasio", - "QueryInterface({}) -> not supported by driver, hr={:#x}", - guid2s(riid), - static_cast(ret)); - } - - return ret; -} - -ULONG STDMETHODCALLTYPE WrappedAsio::AddRef() { - // decoupled from the real driver: we count host references on the wrapper and hold a - // single reference on pReal for our lifetime. this neutralizes a host bug (iidx32+) - // that takes a duplicate AddRef with no matching Release, which would leak the driver - return ++this->ref_count; -} - -ULONG STDMETHODCALLTYPE WrappedAsio::Release() { - const ULONG refs = --this->ref_count; - if (refs == 0) { - delete this; - } - - return refs; -} -#pragma endregion - -#pragma region IAsio -AsioBool __thiscall WrappedAsio::init(void *sys_handle) { - // the real driver is single-instance and kept alive; initialize it exactly once. the - // host re-calls init() on each CoCreate during probing, but re-initializing a live - // driver crashes some hardware drivers, so once we have a live driver we report success - if (this->initialized) { - log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name); - return AsioTrue; - } - - const AsioBool result = this->pReal->init(sys_handle); - if (result == AsioTrue) { - this->initialized = true; - log_info( - "audio::wrappedasio", - "init succeeded for '{}' (driver version {})", - this->driver_name, - this->pReal->get_driver_version()); - } else { - char message[128] = {}; - this->pReal->get_error_message(message); - log_warning("audio::wrappedasio", "init failed: {}", message); - } - - return result; -} - -void __thiscall WrappedAsio::get_driver_name(char *name) { - this->pReal->get_driver_name(name); -} - -long __thiscall WrappedAsio::get_driver_version() { - return this->pReal->get_driver_version(); -} - -void __thiscall WrappedAsio::get_error_message(char *string) { - this->pReal->get_error_message(string); -} - -AsioError __thiscall WrappedAsio::start() { - const AsioError result = this->pReal->start(); - if (result == ASE_OK) { - this->started = true; - log_info( - "audio::wrappedasio", - "start succeeded, ASIO stream is now running on '{}'", - this->driver_name); - } else { - log_warning("audio::wrappedasio", "start failed, err={}", static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::stop() { - const AsioError result = this->pReal->stop(); - if (result == ASE_OK) { - this->started = false; - log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name); - } else { - log_warning("audio::wrappedasio", "stop failed, err={}", static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::get_channels(long *num_input_channels, long *num_output_channels) { - const AsioError result = this->pReal->get_channels(num_input_channels, num_output_channels); - if (result != ASE_OK) { - log_warning("audio::wrappedasio", "get_channels failed, err={}", static_cast(result)); - return result; - } - - if (force_two_channels() - && num_output_channels != nullptr - && *num_output_channels < FORCED_OUTPUT_CHANNELS) - { - // the device has fewer outputs than the game hardcodes; report the count it - // expects so it proceeds to create_buffers, where we forward only the real - // front pair and discard the rest - log_info( - "audio::wrappedasio", - "reporting output channel count as {} (device has {}) for forced two-channel", - FORCED_OUTPUT_CHANNELS, - *num_output_channels); - *num_output_channels = FORCED_OUTPUT_CHANNELS; - } - - log_info( - "audio::wrappedasio", - "get_channels -> in={}, out={}", - num_input_channels ? *num_input_channels : -1, - num_output_channels ? *num_output_channels : -1); - - return result; -} - -AsioError __thiscall WrappedAsio::get_latencies(long *input_latency, long *output_latency) { - const AsioError result = this->pReal->get_latencies(input_latency, output_latency); - if (result == ASE_OK) { - // include millisecond equivalents alongside the frame counts for readability - AsioSampleRate sample_rate = 0.0; - this->pReal->get_sample_rate(&sample_rate); - const long in_frames = input_latency ? *input_latency : -1; - const long out_frames = output_latency ? *output_latency : -1; - log_info( - "audio::wrappedasio", - "get_latencies -> in={} frames ({:.2f} ms), out={} frames ({:.2f} ms)", - in_frames, - frames_to_ms(in_frames, sample_rate), - out_frames, - frames_to_ms(out_frames, sample_rate)); - } else { - log_warning("audio::wrappedasio", "get_latencies failed, err={}", static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::get_buffer_size( - long *min_size, - long *max_size, - long *preferred_size, - long *granularity) -{ - const AsioError result = this->pReal->get_buffer_size(min_size, max_size, preferred_size, granularity); - if (result != ASE_OK) { - log_warning("audio::wrappedasio", "get_buffer_size failed, err={}", static_cast(result)); - return result; - } - - // include millisecond equivalents alongside the frame counts for readability - AsioSampleRate sample_rate = 0.0; - this->pReal->get_sample_rate(&sample_rate); - const long min_frames = min_size ? *min_size : -1; - const long max_frames = max_size ? *max_size : -1; - const long preferred_frames = preferred_size ? *preferred_size : -1; - log_info( - "audio::wrappedasio", - "get_buffer_size -> min={} frames ({:.2f} ms), max={} frames ({:.2f} ms), " - "preferred={} frames ({:.2f} ms), granularity={}", - min_frames, - frames_to_ms(min_frames, sample_rate), - max_frames, - frames_to_ms(max_frames, sample_rate), - preferred_frames, - frames_to_ms(preferred_frames, sample_rate), - granularity ? *granularity : -1); - - return result; -} - -AsioError __thiscall WrappedAsio::can_sample_rate(AsioSampleRate sample_rate) { - const AsioError result = this->pReal->can_sample_rate(sample_rate); - if (result == ASE_OK) { - log_misc("audio::wrappedasio", "can_sample_rate({} Hz) -> supported", sample_rate); - } else { - log_misc( - "audio::wrappedasio", - "can_sample_rate({} Hz) -> not supported, err={}", - sample_rate, - static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::get_sample_rate(AsioSampleRate *sample_rate) { - const AsioError result = this->pReal->get_sample_rate(sample_rate); - if (result == ASE_OK) { - log_misc("audio::wrappedasio", "get_sample_rate -> {} Hz", sample_rate ? *sample_rate : 0.0); - } else { - log_warning("audio::wrappedasio", "get_sample_rate failed, err={}", static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::set_sample_rate(AsioSampleRate sample_rate) { - const AsioError result = this->pReal->set_sample_rate(sample_rate); - if (result == ASE_OK) { - log_info("audio::wrappedasio", "set_sample_rate({} Hz) succeeded", sample_rate); - } else { - log_warning( - "audio::wrappedasio", - "set_sample_rate({} Hz) failed, err={}", - sample_rate, - static_cast(result)); - } - - return result; -} - -AsioError __thiscall WrappedAsio::get_clock_sources(ASIOClockSource *clocks, long *num_sources) { - return this->pReal->get_clock_sources(clocks, num_sources); -} - -AsioError __thiscall WrappedAsio::set_clock_source(long reference) { - return this->pReal->set_clock_source(reference); -} - -AsioError __thiscall WrappedAsio::get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) { - return this->pReal->get_sample_position(s_pos, t_stamp); -} - -AsioError __thiscall WrappedAsio::get_channel_info(AsioChannelInfo *info) { - // forced two-channel: the game probes all output channels it thinks exist, but the - // device only has the real front pair. fabricate a plausible entry for the channels - // beyond the device without touching the real driver - they are discarded in - // create_buffers anyway - long real_in = 0, real_out = 0; - if (force_two_channels() - && info != nullptr - && info->is_input == AsioFalse - && this->pReal->get_channels(&real_in, &real_out) == ASE_OK - && info->channel >= real_out) - { - const long channel = info->channel; - - // report the real device's output sample format rather than a fixed type: when - // stereo downmix is active the game writes these dummy channels in this format and - // we raw-copy the selected pair onto the device's front channels, so the formats - // must match or the copy produces static. the guard above already proved the device - // has output channels, so query channel 0's format directly (avoiding a redundant - // get_channels) and fall back to Int32LSB if that query fails - AsioChannelInfo real_ci {}; - real_ci.channel = 0; - real_ci.is_input = AsioFalse; - AsioSampleType fake_type = ASIOSTInt32LSB; - if (this->pReal->get_channel_info(&real_ci) == ASE_OK) { - fake_type = real_ci.type; - } - - info->is_active = AsioTrue; - info->channel_group = 0; - info->type = fake_type; - snprintf(info->name, sizeof(info->name), "Fake ASIO OUT %ld", channel); - log_info( - "audio::wrappedasio", - "get_channel_info(channel={}, dir=output) -> fake channel, type={} ({})", - channel, - asio_sample_type_name(info->type), - static_cast(info->type)); - - return ASE_OK; - } - - const AsioError result = this->pReal->get_channel_info(info); - if (result == ASE_OK && info != nullptr) { - log_info( - "audio::wrappedasio", - "get_channel_info(channel={}, dir={}) -> active={}, group={}, type={} ({}), name='{}'", - info->channel, - info->is_input == AsioTrue ? "input" : "output", - info->is_active == AsioTrue, - info->channel_group, - asio_sample_type_name(info->type), - static_cast(info->type), - info->name); - } else if (result != ASE_OK) { - log_warning("audio::wrappedasio", "get_channel_info failed, err={}", static_cast(result)); - } - - return result; -} - -AsioCallbacks *WrappedAsio::install_proxy_callbacks(AsioCallbacks *game_callbacks) { - const float gain = hooks::audio::VOLUME_BOOST; - - // start from a clean slate; a previous buffer set may have left state behind - this->volume_channels.clear(); - this->volume_active = false; - this->downmix_active = false; - - const bool want_volume = (gain != 1.0f); - - // front is the device's own pair, so selecting it (or None) means no copy is needed - const bool want_downmix = (STEREO_DOWNMIX != StereoDownmix::None - && STEREO_DOWNMIX != StereoDownmix::Front); - - // no post-processing configured (or no callbacks to wrap): pass the game's callbacks - // straight through and do zero realtime work, exactly as before - if ((!want_volume && !want_downmix) || game_callbacks == nullptr) { - return game_callbacks; - } - - if (want_volume) { - this->volume_active = true; - this->volume_gain = gain; - } - - // the trampolines reach the game's buffer_switch through this copy, regardless of which - // effect is active - this->game_callbacks = *game_callbacks; - - // wrap only the buffer-switch callbacks, where the audio data lives and we rework it. - // the other two carry no data we touch, so forward the game's own pointers unchanged - - // the driver expects them non-null and the game already owns their context - this->proxy_callbacks = {}; - this->proxy_callbacks.buffer_switch = &WrappedAsio::proxy_buffer_switch; - this->proxy_callbacks.sample_rate_did_change = game_callbacks->sample_rate_did_change; - this->proxy_callbacks.asio_message = game_callbacks->asio_message; - this->proxy_callbacks.buffer_switch_time_info = - game_callbacks->buffer_switch_time_info ? &WrappedAsio::proxy_buffer_switch_time_info : nullptr; - - return &this->proxy_callbacks; -} - -AsioSampleType WrappedAsio::device_output_sample_type() { - long real_in = 0, real_out = 0; - if (this->pReal->get_channels(&real_in, &real_out) != ASE_OK || real_out <= 0) { - return ASIOSTLastEntry; - } - - AsioChannelInfo ci {}; - ci.channel = 0; - ci.is_input = AsioFalse; - if (this->pReal->get_channel_info(&ci) != ASE_OK) { - return ASIOSTLastEntry; - } - return ci.type; -} - -void WrappedAsio::record_volume_output_channel(const AsioBufferInfo &info) { - if (info.is_input != AsioFalse) { - return; - } - - // ask the real driver for this channel's sample format so the realtime path knows how - // to scale it; fall back to a sentinel that apply_gain_planar leaves untouched - AsioChannelInfo ci {}; - ci.channel = info.channel_num; - ci.is_input = AsioFalse; - AsioSampleType type = ASIOSTLastEntry; - if (this->pReal->get_channel_info(&ci) == ASE_OK) { - type = ci.type; - } - - VolumeOutputChannel ch; - ch.buffers[0] = info.buffers[0]; - ch.buffers[1] = info.buffers[1]; - ch.type = type; - this->volume_channels.push_back(ch); -} - -void WrappedAsio::record_downmix_channels( - AsioBufferInfo *buffer_infos, long num_channels, long buffer_size) -{ - // map the selected pair to source channel indices (0-indexed, standard 7.1 layout); - // None and Front need no copy - long src_left = 0, src_right = 0; - switch (STEREO_DOWNMIX) { - case StereoDownmix::Center: src_left = 2; src_right = 2; break; - case StereoDownmix::Rear: src_left = 4; src_right = 5; break; - case StereoDownmix::Side: src_left = 6; src_right = 7; break; - default: return; - } - - // find the double-buffer pair for a given output channel among those the game created, - // or nullptr if the device does not expose it - auto find_output = [&](long channel) -> void ** { - for (long i = 0; i < num_channels; i++) { - AsioBufferInfo &bi = buffer_infos[i]; - if (bi.is_input == AsioFalse && bi.channel_num == channel) { - return bi.buffers; - } - } - return nullptr; - }; - - // destinations are device channels 0/1; sources are the selected pair - void **dst0 = find_output(0); - void **dst1 = find_output(1); - void **src_l = find_output(src_left); - void **src_r = find_output(src_right); - if (dst0 == nullptr || dst1 == nullptr || src_l == nullptr || src_r == nullptr) { - log_warning( - "audio::wrappedasio", - "stereo downmix disabled: device is missing the front pair or source channels " - "{}/{} (game created {} channel(s))", - src_left, - src_right, - num_channels); - return; - } - - // all device output channels share one sample format; query it to size the copy - const AsioSampleType type = this->device_output_sample_type(); - const int sample_bytes = asio_sample_bytes(type); - if (sample_bytes <= 0) { - log_warning( - "audio::wrappedasio", - "stereo downmix disabled: unsupported sample format {} ({})", - asio_sample_type_name(type), - static_cast(type)); - return; - } - - this->downmix_copies[0] = {{dst0[0], dst0[1]}, {src_l[0], src_l[1]}}; - this->downmix_copies[1] = {{dst1[0], dst1[1]}, {src_r[0], src_r[1]}}; - this->downmix_bytes = static_cast(buffer_size) * sample_bytes; - this->downmix_active = true; - - log_info( - "audio::wrappedasio", - "stereo downmix active: pair={} (src {}/{} -> device 0/1), {} frames, {} byte(s)/sample", - stereo_downmix_name(STEREO_DOWNMIX), - src_left, - src_right, - buffer_size, - sample_bytes); -} - -void WrappedAsio::publish_post_process(long buffer_size) { - if (!this->volume_active && !this->downmix_active) { - return; - } - - // everything the realtime thread reads is now in place; make ourselves reachable - this->volume_buffer_size = buffer_size; - WrappedAsio::active_instance.store(this, std::memory_order_release); - - if (this->volume_active) { - log_info( - "audio::wrappedasio", - "volume boost active: gain={}, scaling {} output channel(s)", - this->volume_gain, - this->volume_channels.size()); - } -} - -void WrappedAsio::detach_post_process() { - // stop our realtime trampolines from reaching this wrapper, but only if we are the - // currently published instance - WrappedAsio *expected = this; - WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr); -} - -void WrappedAsio::quiesce_for_reuse() { - // a previous abandoned probing cycle may have left a running stream and live buffers on - // the real driver without calling stop/dispose; tear that down now (without destroying - // the driver) so the host's next create_buffers starts clean. stop() guarantees no - // further buffer_switch, and dispose_buffers detaches our trampolines - if (this->started) { - log_info( - "audio::wrappedasio", - "reuse: stopping leftover stream on '{}' before handing the driver back", - this->driver_name); - this->stop(); - } - if (this->buffers_created) { - log_info( - "audio::wrappedasio", - "reuse: disposing leftover buffers on '{}' before handing the driver back", - this->driver_name); - this->dispose_buffers(); - } -} - -void WrappedAsio::apply_output_volume(long double_buffer_index) { - if (double_buffer_index != 0 && double_buffer_index != 1) { - return; - } - const float gain = this->volume_gain; - const long frames = this->volume_buffer_size; - for (const VolumeOutputChannel &ch : this->volume_channels) { - apply_gain_planar(ch.buffers[double_buffer_index], frames, ch.type, gain); - } -} - -void WrappedAsio::apply_downmix(long double_buffer_index) { - if (!this->downmix_active) { - return; - } - if (double_buffer_index != 0 && double_buffer_index != 1) { - return; - } - - // raw planar copy of the selected source channels onto device channels 0/1; for the - // center selection both copies share one source. skip self-copies (front) - for (const DownmixCopy © : this->downmix_copies) { - void *dst = copy.dst[double_buffer_index]; - const void *src = copy.src[double_buffer_index]; - if (dst != nullptr && src != nullptr && dst != src) { - std::memcpy(dst, src, this->downmix_bytes); - } - } -} - -void __cdecl WrappedAsio::proxy_buffer_switch(long double_buffer_index, AsioBool direct_process) { - WrappedAsio *self = WrappedAsio::active_instance.load(std::memory_order_acquire); - if (self == nullptr) { - return; - } - - // let the game write its samples into the driver buffers first, then rework them before - // the driver plays this half on the next switch: downmix first (arrange channels 0/1), - // then scale the device outputs by the volume boost - if (self->game_callbacks.buffer_switch != nullptr) { - self->game_callbacks.buffer_switch(double_buffer_index, direct_process); - } - self->apply_downmix(double_buffer_index); - self->apply_output_volume(double_buffer_index); -} - -AsioTime * __cdecl WrappedAsio::proxy_buffer_switch_time_info( - AsioTime *params, long double_buffer_index, AsioBool direct_process) -{ - WrappedAsio *self = WrappedAsio::active_instance.load(std::memory_order_acquire); - if (self == nullptr) { - return params; - } - - AsioTime *ret = params; - if (self->game_callbacks.buffer_switch_time_info != nullptr) { - ret = self->game_callbacks.buffer_switch_time_info( - params, double_buffer_index, direct_process); - } else if (self->game_callbacks.buffer_switch != nullptr) { - self->game_callbacks.buffer_switch(double_buffer_index, direct_process); - } - self->apply_downmix(double_buffer_index); - self->apply_output_volume(double_buffer_index); - return ret; -} - -AsioError __thiscall WrappedAsio::create_buffers( - AsioBufferInfo *buffer_infos, - long num_channels, - long buffer_size, - AsioCallbacks *callbacks) -{ - // swap in our buffer-switch trampolines if any post-processing is configured, so the - // real driver calls us and we rework its output after the game fills it (no-op otherwise) - AsioCallbacks *effective = this->install_proxy_callbacks(callbacks); - - if (force_two_channels()) { - return this->create_buffers_front_pair(buffer_infos, num_channels, buffer_size, effective); - } - - const AsioError result = this->pReal->create_buffers(buffer_infos, num_channels, buffer_size, effective); - if (result == ASE_OK) { - log_info( - "audio::wrappedasio", - "create_buffers(channels={}, size={} frames) succeeded", - num_channels, - buffer_size); - - // capture the post-process state now the buffers exist, then publish ourselves to - // the realtime thread once everything is in place. downmix is not recorded here: any - // active stereo extraction forces the front-pair path above, so this path only ever - // runs the volume boost - if (this->volume_active) { - for (long i = 0; i < num_channels; i++) { - this->record_volume_output_channel(buffer_infos[i]); - } - } - this->publish_post_process(buffer_size); - this->buffers_created = true; - } else { - log_warning( - "audio::wrappedasio", - "create_buffers(channels={}, size={} frames) failed, err={}", - num_channels, - buffer_size, - static_cast(result)); - } - - return result; -} - -AsioError WrappedAsio::create_buffers_front_pair( - AsioBufferInfo *buffer_infos, - long num_channels, - long buffer_size, - AsioCallbacks *callbacks) -{ - // front-pair extraction (forced two-channel ASIO): the game asks for more output - // channels than the real device has (e.g. 8 vs 2). forward only the channels the - // device actually provides (channel 0/1 = front L/R) and hand the game throwaway - // buffers for the rest, so its front mix lands on the device and the surround - // channels are discarded. the game writes directly into the driver/dummy buffers - // from its own bufferSwitch; the realtime work we do is the optional volume boost - // (scaling the forwarded device channels) and, if a non-front stereo downmix is - // selected, copying that pair from its dummy buffers onto the device's front pair - long real_in = 0, real_out = 0; - const AsioError ch_result = this->pReal->get_channels(&real_in, &real_out); - if (ch_result != ASE_OK) { - log_warning( - "audio::wrappedasio", - "create_buffers: get_channels failed, err={}", - static_cast(ch_result)); - return ch_result; - } - - // partition the requested channels: those the device can serve are forwarded, the rest - // are discarded. record source indices for both so we can patch the game's array after - std::vector forwarded; - std::vector forwarded_src; - std::vector discarded_src; - forwarded.reserve(num_channels); - forwarded_src.reserve(num_channels); - discarded_src.reserve(num_channels); - for (long i = 0; i < num_channels; i++) { - const AsioBufferInfo &bi = buffer_infos[i]; - const long limit = (bi.is_input == AsioTrue) ? real_in : real_out; - if (bi.channel_num < limit) { - forwarded.push_back(bi); - forwarded_src.push_back(i); - } else { - discarded_src.push_back(i); - } - } - - const AsioError result = this->pReal->create_buffers( - forwarded.data(), static_cast(forwarded.size()), buffer_size, callbacks); - if (result != ASE_OK) { - log_warning( - "audio::wrappedasio", - "create_buffers(forwarded={} of {}, size={} frames) failed, err={}", - forwarded.size(), - num_channels, - buffer_size, - static_cast(result)); - return result; - } - - // copy the real driver buffer pointers back into the game's array - for (size_t k = 0; k < forwarded.size(); k++) { - AsioBufferInfo &dst = buffer_infos[forwarded_src[k]]; - dst.buffers[0] = forwarded[k].buffers[0]; - dst.buffers[1] = forwarded[k].buffers[1]; - - // only the forwarded channels reach the device, so those are the ones the volume - // boost scales (the discarded channels go to throwaway buffers below) - if (this->volume_active) { - this->record_volume_output_channel(dst); - } - } - - // hand throwaway double buffers to the discarded channels. sized generously at - // 8 bytes/sample (covers every ASIO sample type) so the game can never overrun them - // regardless of the negotiated format - this->dummy_buffers.clear(); - this->dummy_buffers.reserve(discarded_src.size() * 2); - const size_t dummy_bytes = static_cast(buffer_size) * 8; - for (const long i : discarded_src) { - for (void *&buffer : buffer_infos[i].buffers) { - auto buf = std::make_unique(dummy_bytes); - std::memset(buf.get(), 0, dummy_bytes); - buffer = buf.get(); - this->dummy_buffers.push_back(std::move(buf)); - } - } - - // record the downmix source/destination buffers now the array is fully patched: the - // selected pair (e.g. rear) lives in the dummy buffers above, the device front pair in - // the forwarded driver buffers, so the realtime copy lands the chosen pair on the - // device's 2.0 output. then publish ourselves to the realtime thread - this->record_downmix_channels(buffer_infos, num_channels, buffer_size); - this->publish_post_process(buffer_size); - this->buffers_created = true; - - log_info( - "audio::wrappedasio", - "create_buffers: front-pair extraction - forwarded {} channel(s) to device, " - "discarded {} (requested {}, size={} frames)", - forwarded.size(), - discarded_src.size(), - num_channels, - buffer_size); - - return ASE_OK; -} - -AsioError __thiscall WrappedAsio::dispose_buffers() { - // stop our realtime trampolines from touching buffers the driver is about to free - this->detach_post_process(); - - const AsioError result = this->pReal->dispose_buffers(); - this->dummy_buffers.clear(); - this->volume_channels.clear(); - this->volume_active = false; - this->downmix_active = false; - this->buffers_created = false; - return result; -} - -AsioError __thiscall WrappedAsio::control_panel() { - return this->pReal->control_panel(); -} - -AsioError __thiscall WrappedAsio::future(long selector, void *opt) { - return this->pReal->future(selector, opt); -} - -AsioError __thiscall WrappedAsio::output_ready() { - return this->pReal->output_ready(); -} -#pragma endregion - -namespace hooks::audio::asio { - - IUnknown *wrap(REFCLSID clsid, void *real) { - log_info("audio::wrappedasio", "wrapping ASIO driver interface, clsid={}", guid2s(clsid)); - - auto *wrapper = new WrappedAsio( - reinterpret_cast(real), clsid, registered_asio_name(clsid)); - - // pin the wrapper (and the real driver it owns) for the process lifetime; later - // CoCreate calls reuse this same instance via wrap_existing - wrapper->AddRef(); - store_wrapper(clsid, wrapper); - - return static_cast(wrapper); - } - - IUnknown *wrap_existing(REFCLSID clsid) { - WrappedAsio *wrapper = find_wrapper(clsid); - if (wrapper == nullptr) { - return nullptr; - } - - log_misc( - "audio::wrappedasio", - "reusing cached ASIO driver instance, clsid={}", - guid2s(clsid)); - - wrapper->quiesce_for_reuse(); - - // hand the host another reference to the one instance we keep alive - wrapper->AddRef(); - return static_cast(wrapper); - } - - void release_all_wrappers() { - // collect the pinned wrappers under the lock, then drop our references outside it so - // a final Release (which can run the driver's own teardown) never happens while the - // lock is held - std::vector> wrappers; - { - std::lock_guard lock(g_wrappers_mutex); - wrappers = g_wrappers; - g_wrappers.clear(); - } - - for (auto &entry : wrappers) { - // drop only the pin wrap() took. if the host has already released its own - // references (the normal case at shutdown) this destroys the wrapper and releases - // the real driver; if the host is still using it, the refcount stays above zero - // and we leave its live stream/buffers untouched - log_misc( - "audio::wrappedasio", - "releasing cached ASIO driver instance, clsid={}", - guid2s(entry.first)); - entry.second->Release(); - } - } -} - +#include "asio_proxy.h" + +#include +#include +#include +#include +#include +#include + +#include "external/asio/asiolist.h" +#include "hooks/audio/audio.h" +#include "util/logging.h" +#include "util/utils.h" + +namespace { + + // readable name for an ASIO sample type (e.g. "ASIOSTInt32LSB"), falling back to the + // numeric value for unknown types + const char *asio_sample_type_name(AsioSampleType type) { + switch (type) { + case ASIOSTInt16MSB: return "ASIOSTInt16MSB"; + case ASIOSTInt24MSB: return "ASIOSTInt24MSB"; + case ASIOSTInt32MSB: return "ASIOSTInt32MSB"; + case ASIOSTFloat32MSB: return "ASIOSTFloat32MSB"; + case ASIOSTFloat64MSB: return "ASIOSTFloat64MSB"; + case ASIOSTInt32MSB16: return "ASIOSTInt32MSB16"; + case ASIOSTInt32MSB18: return "ASIOSTInt32MSB18"; + case ASIOSTInt32MSB20: return "ASIOSTInt32MSB20"; + case ASIOSTInt32MSB24: return "ASIOSTInt32MSB24"; + case ASIOSTInt16LSB: return "ASIOSTInt16LSB"; + case ASIOSTInt24LSB: return "ASIOSTInt24LSB"; + case ASIOSTInt32LSB: return "ASIOSTInt32LSB"; + case ASIOSTFloat32LSB: return "ASIOSTFloat32LSB"; + case ASIOSTFloat64LSB: return "ASIOSTFloat64LSB"; + case ASIOSTInt32LSB16: return "ASIOSTInt32LSB16"; + case ASIOSTInt32LSB18: return "ASIOSTInt32LSB18"; + case ASIOSTInt32LSB20: return "ASIOSTInt32LSB20"; + case ASIOSTInt32LSB24: return "ASIOSTInt32LSB24"; + default: return "unknown"; + } + } + + // bytes occupied by one sample of the given ASIO type, or 0 for formats we cannot size + // (used to compute the byte length of a planar channel buffer for raw copies) + int asio_sample_bytes(AsioSampleType type) { + switch (type) { + case ASIOSTInt16LSB: + case ASIOSTInt16MSB: + return 2; + case ASIOSTInt24LSB: + case ASIOSTInt24MSB: + return 3; + case ASIOSTInt32LSB: + case ASIOSTInt32MSB: + case ASIOSTInt32LSB16: + case ASIOSTInt32LSB18: + case ASIOSTInt32LSB20: + case ASIOSTInt32LSB24: + case ASIOSTInt32MSB16: + case ASIOSTInt32MSB18: + case ASIOSTInt32MSB20: + case ASIOSTInt32MSB24: + case ASIOSTFloat32LSB: + case ASIOSTFloat32MSB: + return 4; + case ASIOSTFloat64LSB: + case ASIOSTFloat64MSB: + return 8; + default: + return 0; + } + } + + // readable name for a stereo downmix selection, used in our logs + const char *stereo_downmix_name(WrappedAsio::StereoDownmix mode) { + switch (mode) { + case WrappedAsio::StereoDownmix::None: return "none"; + case WrappedAsio::StereoDownmix::Front: return "front"; + case WrappedAsio::StereoDownmix::Center: return "center"; + case WrappedAsio::StereoDownmix::Rear: return "rear"; + case WrappedAsio::StereoDownmix::Side: return "side"; + default: return "unknown"; + } + } + + // duration in milliseconds of a buffer of the given frame count at a sample rate, or a + // negative sentinel when the frame count or sample rate is unusable + double frames_to_ms(long frames, AsioSampleRate sample_rate) { + if (frames < 0 || sample_rate <= 0.0) { + return -1.0; + } + return (frames * 1000.0) / sample_rate; + } + + // scales one planar ASIO output buffer (frames samples of the given type) by gain in + // place, clamping integer formats so a boost saturates instead of wrapping. unsupported + // formats are left untouched. runs on the driver's realtime thread, so no allocation, + // locking or logging here + void apply_gain_planar(void *buffer, long frames, AsioSampleType type, float gain) { + if (buffer == nullptr || frames <= 0) { + return; + } + switch (type) { + case ASIOSTFloat32LSB: { + auto p = static_cast(buffer); + for (long i = 0; i < frames; i++) { + p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f); + } + break; + } + case ASIOSTFloat64LSB: { + auto p = static_cast(buffer); + for (long i = 0; i < frames; i++) { + p[i] = std::clamp(p[i] * static_cast(gain), -1.0, 1.0); + } + break; + } + case ASIOSTInt16LSB: { + auto p = static_cast(buffer); + for (long i = 0; i < frames; i++) { + p[i] = static_cast( + std::clamp(std::lround(p[i] * gain), -32768L, 32767L)); + } + break; + } + case ASIOSTInt24LSB: { + // packed 24-bit little-endian, 3 bytes per sample + auto bytes = static_cast(buffer); + for (long i = 0; i < frames; i++) { + uint8_t *s = bytes + i * 3; + int32_t v = s[0] | (s[1] << 8) | (s[2] << 16); + if (v & 0x800000) { + v |= ~0xFFFFFF; // sign extend + } + int64_t scaled = std::clamp( + std::llround(static_cast(v) * gain), -8388608, 8388607); + s[0] = scaled & 0xFF; + s[1] = (scaled >> 8) & 0xFF; + s[2] = (scaled >> 16) & 0xFF; + } + break; + } + case ASIOSTInt32LSB: { + auto p = static_cast(buffer); + for (long i = 0; i < frames; i++) { + p[i] = static_cast(std::clamp( + std::llround(static_cast(p[i]) * gain), INT32_MIN, INT32_MAX)); + } + break; + } + default: + // unsupported format (MSB, aligned 32-bit, DSD): leave untouched + break; + } + } + + // one wrapper per CLSID, kept alive for the process lifetime. ASIO drivers are + // single-instance, and some hardware drivers (e.g. Neva Uno) crash if their COM object + // is destroyed and re-created within a process - which the host triggers by leaking and + // re-instantiating the driver during startup probing. so we build the real driver and + // its wrapper once, hold a reference so it survives the host's Release calls, and hand + // the same wrapper back for every later CoCreate. as a result the real driver is created + // and initialized exactly once + std::mutex g_wrappers_mutex; + std::vector> g_wrappers; + + WrappedAsio *find_wrapper(REFCLSID clsid) { + std::lock_guard lock(g_wrappers_mutex); + for (auto &entry : g_wrappers) { + if (IsEqualCLSID(entry.first, clsid)) { + return entry.second; + } + } + + return nullptr; + } + + void store_wrapper(REFCLSID clsid, WrappedAsio *wrapper) { + std::lock_guard lock(g_wrappers_mutex); + g_wrappers.emplace_back(clsid, wrapper); + } + + // ASIO drivers registered on this system (CLSID + registry name), scanned once + const std::vector> ®istered_asio_drivers() { + static const std::vector> drivers = [] { + std::vector> result; + AsioDriverList driver_list; + for (const auto &driver : driver_list.driver_list) { + result.emplace_back(driver.clsid, driver.name); + log_info( + "audio::wrappedasio", + "registered ASIO driver: name='{}', clsid={}", + driver.name, + guid2s(driver.clsid)); + } + + log_info("audio::wrappedasio", "discovered {} registered ASIO driver(s)", result.size()); + return result; + }(); + + return drivers; + } + + std::string registered_asio_name(REFCLSID clsid) { + for (const auto &driver : registered_asio_drivers()) { + if (IsEqualCLSID(driver.first, clsid)) { + return driver.second; + } + } + + return guid2s(clsid); + } +} + +namespace hooks::audio::asio { + + bool is_asio_creation(REFCLSID rclsid, REFIID riid) { + + // ASIO hosts request the driver using its own CLSID as the interface id + if (!IsEqualGUID(rclsid, riid)) { + return false; + } + + for (const auto &driver : registered_asio_drivers()) { + if (IsEqualCLSID(driver.first, rclsid)) { + return true; + } + } + + return false; + } +} + +#pragma region IUnknown +WrappedAsio::StereoDownmix WrappedAsio::STEREO_DOWNMIX = WrappedAsio::StereoDownmix::None; +std::atomic WrappedAsio::active_instance {nullptr}; + +WrappedAsio::StereoDownmix WrappedAsio::name_to_stereo_downmix(const char *name) { + if (_stricmp(name, "front") == 0) { + return StereoDownmix::Front; + } else if (_stricmp(name, "center") == 0) { + return StereoDownmix::Center; + } else if (_stricmp(name, "rear") == 0) { + return StereoDownmix::Rear; + } else if (_stricmp(name, "side") == 0) { + return StereoDownmix::Side; + } + + return StereoDownmix::None; +} + +WrappedAsio::~WrappedAsio() { + this->detach_post_process(); + + // never runs mid-run: wrap() pins the wrapper so the refcount stays above zero until + // release_all_wrappers() drops the pin at shutdown - the only point this can fire, and + // only once the host has released its own references. tears down the real driver + this->pReal->Release(); + + log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid)); +} + +HRESULT STDMETHODCALLTYPE WrappedAsio::QueryInterface(REFIID riid, void **ppv) { + if (ppv == nullptr) { + return E_POINTER; + } + + // ASIO hosts query for the driver using its own CLSID as the IID + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, this->clsid)) { + this->AddRef(); + *ppv = static_cast(this); + + return S_OK; + } + + // the host is asking for some other interface; forward to the real driver. a failure + // here is a common reason a host discards a driver and retries + const HRESULT ret = this->pReal->QueryInterface(riid, ppv); + if (SUCCEEDED(ret)) { + log_info("audio::wrappedasio", "QueryInterface({}) -> forwarded to real driver", guid2s(riid)); + } else { + log_info( + "audio::wrappedasio", + "QueryInterface({}) -> not supported by driver, hr={:#x}", + guid2s(riid), + static_cast(ret)); + } + + return ret; +} + +ULONG STDMETHODCALLTYPE WrappedAsio::AddRef() { + // decoupled from the real driver: we count host references on the wrapper and hold a + // single reference on pReal for our lifetime. this neutralizes a host bug (iidx32+) + // that takes a duplicate AddRef with no matching Release, which would leak the driver + return ++this->ref_count; +} + +ULONG STDMETHODCALLTYPE WrappedAsio::Release() { + const ULONG refs = --this->ref_count; + if (refs == 0) { + delete this; + } + + return refs; +} +#pragma endregion + +#pragma region IAsio +AsioBool __thiscall WrappedAsio::init(void *sys_handle) { + // the real driver is single-instance and kept alive; initialize it exactly once. the + // host re-calls init() on each CoCreate during probing, but re-initializing a live + // driver crashes some hardware drivers, so once we have a live driver we report success + if (this->initialized) { + log_misc("audio::wrappedasio", "init skipped, '{}' already initialized", this->driver_name); + return AsioTrue; + } + + const AsioBool result = this->pReal->init(sys_handle); + if (result == AsioTrue) { + this->initialized = true; + log_info( + "audio::wrappedasio", + "init succeeded for '{}' (driver version {})", + this->driver_name, + this->pReal->get_driver_version()); + } else { + char message[128] = {}; + this->pReal->get_error_message(message); + log_warning("audio::wrappedasio", "init failed: {}", message); + } + + return result; +} + +void __thiscall WrappedAsio::get_driver_name(char *name) { + this->pReal->get_driver_name(name); +} + +long __thiscall WrappedAsio::get_driver_version() { + return this->pReal->get_driver_version(); +} + +void __thiscall WrappedAsio::get_error_message(char *string) { + this->pReal->get_error_message(string); +} + +AsioError __thiscall WrappedAsio::start() { + const AsioError result = this->pReal->start(); + if (result == ASE_OK) { + this->started = true; + log_info( + "audio::wrappedasio", + "start succeeded, ASIO stream is now running on '{}'", + this->driver_name); + } else { + log_warning("audio::wrappedasio", "start failed, err={}", static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::stop() { + const AsioError result = this->pReal->stop(); + if (result == ASE_OK) { + this->started = false; + log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name); + } else { + log_warning("audio::wrappedasio", "stop failed, err={}", static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::get_channels(long *num_input_channels, long *num_output_channels) { + const AsioError result = this->pReal->get_channels(num_input_channels, num_output_channels); + if (result != ASE_OK) { + log_warning("audio::wrappedasio", "get_channels failed, err={}", static_cast(result)); + return result; + } + + if (force_two_channels() + && num_output_channels != nullptr + && *num_output_channels < FORCED_OUTPUT_CHANNELS) + { + // the device has fewer outputs than the game hardcodes; report the count it + // expects so it proceeds to create_buffers, where we forward only the real + // front pair and discard the rest + log_info( + "audio::wrappedasio", + "reporting output channel count as {} (device has {}) for forced two-channel", + FORCED_OUTPUT_CHANNELS, + *num_output_channels); + *num_output_channels = FORCED_OUTPUT_CHANNELS; + } + + log_info( + "audio::wrappedasio", + "get_channels -> in={}, out={}", + num_input_channels ? *num_input_channels : -1, + num_output_channels ? *num_output_channels : -1); + + return result; +} + +AsioError __thiscall WrappedAsio::get_latencies(long *input_latency, long *output_latency) { + const AsioError result = this->pReal->get_latencies(input_latency, output_latency); + if (result == ASE_OK) { + // include millisecond equivalents alongside the frame counts for readability + AsioSampleRate sample_rate = 0.0; + this->pReal->get_sample_rate(&sample_rate); + const long in_frames = input_latency ? *input_latency : -1; + const long out_frames = output_latency ? *output_latency : -1; + log_info( + "audio::wrappedasio", + "get_latencies -> in={} frames ({:.2f} ms), out={} frames ({:.2f} ms)", + in_frames, + frames_to_ms(in_frames, sample_rate), + out_frames, + frames_to_ms(out_frames, sample_rate)); + } else { + log_warning("audio::wrappedasio", "get_latencies failed, err={}", static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::get_buffer_size( + long *min_size, + long *max_size, + long *preferred_size, + long *granularity) +{ + const AsioError result = this->pReal->get_buffer_size(min_size, max_size, preferred_size, granularity); + if (result != ASE_OK) { + log_warning("audio::wrappedasio", "get_buffer_size failed, err={}", static_cast(result)); + return result; + } + + // include millisecond equivalents alongside the frame counts for readability + AsioSampleRate sample_rate = 0.0; + this->pReal->get_sample_rate(&sample_rate); + const long min_frames = min_size ? *min_size : -1; + const long max_frames = max_size ? *max_size : -1; + const long preferred_frames = preferred_size ? *preferred_size : -1; + log_info( + "audio::wrappedasio", + "get_buffer_size -> min={} frames ({:.2f} ms), max={} frames ({:.2f} ms), " + "preferred={} frames ({:.2f} ms), granularity={}", + min_frames, + frames_to_ms(min_frames, sample_rate), + max_frames, + frames_to_ms(max_frames, sample_rate), + preferred_frames, + frames_to_ms(preferred_frames, sample_rate), + granularity ? *granularity : -1); + + return result; +} + +AsioError __thiscall WrappedAsio::can_sample_rate(AsioSampleRate sample_rate) { + const AsioError result = this->pReal->can_sample_rate(sample_rate); + if (result == ASE_OK) { + log_misc("audio::wrappedasio", "can_sample_rate({} Hz) -> supported", sample_rate); + } else { + log_misc( + "audio::wrappedasio", + "can_sample_rate({} Hz) -> not supported, err={}", + sample_rate, + static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::get_sample_rate(AsioSampleRate *sample_rate) { + const AsioError result = this->pReal->get_sample_rate(sample_rate); + if (result == ASE_OK) { + log_misc("audio::wrappedasio", "get_sample_rate -> {} Hz", sample_rate ? *sample_rate : 0.0); + } else { + log_warning("audio::wrappedasio", "get_sample_rate failed, err={}", static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::set_sample_rate(AsioSampleRate sample_rate) { + const AsioError result = this->pReal->set_sample_rate(sample_rate); + if (result == ASE_OK) { + log_info("audio::wrappedasio", "set_sample_rate({} Hz) succeeded", sample_rate); + } else { + log_warning( + "audio::wrappedasio", + "set_sample_rate({} Hz) failed, err={}", + sample_rate, + static_cast(result)); + } + + return result; +} + +AsioError __thiscall WrappedAsio::get_clock_sources(ASIOClockSource *clocks, long *num_sources) { + return this->pReal->get_clock_sources(clocks, num_sources); +} + +AsioError __thiscall WrappedAsio::set_clock_source(long reference) { + return this->pReal->set_clock_source(reference); +} + +AsioError __thiscall WrappedAsio::get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) { + return this->pReal->get_sample_position(s_pos, t_stamp); +} + +AsioError __thiscall WrappedAsio::get_channel_info(AsioChannelInfo *info) { + // forced two-channel: the game probes all output channels it thinks exist, but the + // device only has the real front pair. fabricate a plausible entry for the channels + // beyond the device without touching the real driver - they are discarded in + // create_buffers anyway + long real_in = 0, real_out = 0; + if (force_two_channels() + && info != nullptr + && info->is_input == AsioFalse + && this->pReal->get_channels(&real_in, &real_out) == ASE_OK + && info->channel >= real_out) + { + const long channel = info->channel; + + // report the real device's output sample format rather than a fixed type: when + // stereo downmix is active the game writes these dummy channels in this format and + // we raw-copy the selected pair onto the device's front channels, so the formats + // must match or the copy produces static. the guard above already proved the device + // has output channels, so query channel 0's format directly (avoiding a redundant + // get_channels) and fall back to Int32LSB if that query fails + AsioChannelInfo real_ci {}; + real_ci.channel = 0; + real_ci.is_input = AsioFalse; + AsioSampleType fake_type = ASIOSTInt32LSB; + if (this->pReal->get_channel_info(&real_ci) == ASE_OK) { + fake_type = real_ci.type; + } + + info->is_active = AsioTrue; + info->channel_group = 0; + info->type = fake_type; + snprintf(info->name, sizeof(info->name), "Fake ASIO OUT %ld", channel); + log_info( + "audio::wrappedasio", + "get_channel_info(channel={}, dir=output) -> fake channel, type={} ({})", + channel, + asio_sample_type_name(info->type), + static_cast(info->type)); + + return ASE_OK; + } + + const AsioError result = this->pReal->get_channel_info(info); + if (result == ASE_OK && info != nullptr) { + log_info( + "audio::wrappedasio", + "get_channel_info(channel={}, dir={}) -> active={}, group={}, type={} ({}), name='{}'", + info->channel, + info->is_input == AsioTrue ? "input" : "output", + info->is_active == AsioTrue, + info->channel_group, + asio_sample_type_name(info->type), + static_cast(info->type), + info->name); + } else if (result != ASE_OK) { + log_warning("audio::wrappedasio", "get_channel_info failed, err={}", static_cast(result)); + } + + return result; +} + +AsioCallbacks *WrappedAsio::install_proxy_callbacks(AsioCallbacks *game_callbacks) { + const float gain = hooks::audio::VOLUME_BOOST; + + // start from a clean slate; a previous buffer set may have left state behind + this->volume_channels.clear(); + this->volume_active = false; + this->downmix_active = false; + + const bool want_volume = (gain != 1.0f); + + // front is the device's own pair, so selecting it (or None) means no copy is needed + const bool want_downmix = (STEREO_DOWNMIX != StereoDownmix::None + && STEREO_DOWNMIX != StereoDownmix::Front); + + // no post-processing configured (or no callbacks to wrap): pass the game's callbacks + // straight through and do zero realtime work, exactly as before + if ((!want_volume && !want_downmix) || game_callbacks == nullptr) { + return game_callbacks; + } + + if (want_volume) { + this->volume_active = true; + this->volume_gain = gain; + } + + // the trampolines reach the game's buffer_switch through this copy, regardless of which + // effect is active + this->game_callbacks = *game_callbacks; + + // wrap only the buffer-switch callbacks, where the audio data lives and we rework it. + // the other two carry no data we touch, so forward the game's own pointers unchanged - + // the driver expects them non-null and the game already owns their context + this->proxy_callbacks = {}; + this->proxy_callbacks.buffer_switch = &WrappedAsio::proxy_buffer_switch; + this->proxy_callbacks.sample_rate_did_change = game_callbacks->sample_rate_did_change; + this->proxy_callbacks.asio_message = game_callbacks->asio_message; + this->proxy_callbacks.buffer_switch_time_info = + game_callbacks->buffer_switch_time_info ? &WrappedAsio::proxy_buffer_switch_time_info : nullptr; + + return &this->proxy_callbacks; +} + +AsioSampleType WrappedAsio::device_output_sample_type() { + long real_in = 0, real_out = 0; + if (this->pReal->get_channels(&real_in, &real_out) != ASE_OK || real_out <= 0) { + return ASIOSTLastEntry; + } + + AsioChannelInfo ci {}; + ci.channel = 0; + ci.is_input = AsioFalse; + if (this->pReal->get_channel_info(&ci) != ASE_OK) { + return ASIOSTLastEntry; + } + return ci.type; +} + +void WrappedAsio::record_volume_output_channel(const AsioBufferInfo &info) { + if (info.is_input != AsioFalse) { + return; + } + + // ask the real driver for this channel's sample format so the realtime path knows how + // to scale it; fall back to a sentinel that apply_gain_planar leaves untouched + AsioChannelInfo ci {}; + ci.channel = info.channel_num; + ci.is_input = AsioFalse; + AsioSampleType type = ASIOSTLastEntry; + if (this->pReal->get_channel_info(&ci) == ASE_OK) { + type = ci.type; + } + + VolumeOutputChannel ch; + ch.buffers[0] = info.buffers[0]; + ch.buffers[1] = info.buffers[1]; + ch.type = type; + this->volume_channels.push_back(ch); +} + +void WrappedAsio::record_downmix_channels( + AsioBufferInfo *buffer_infos, long num_channels, long buffer_size) +{ + // map the selected pair to source channel indices (0-indexed, standard 7.1 layout); + // None and Front need no copy + long src_left = 0, src_right = 0; + switch (STEREO_DOWNMIX) { + case StereoDownmix::Center: src_left = 2; src_right = 2; break; + case StereoDownmix::Rear: src_left = 4; src_right = 5; break; + case StereoDownmix::Side: src_left = 6; src_right = 7; break; + default: return; + } + + // find the double-buffer pair for a given output channel among those the game created, + // or nullptr if the device does not expose it + auto find_output = [&](long channel) -> void ** { + for (long i = 0; i < num_channels; i++) { + AsioBufferInfo &bi = buffer_infos[i]; + if (bi.is_input == AsioFalse && bi.channel_num == channel) { + return bi.buffers; + } + } + return nullptr; + }; + + // destinations are device channels 0/1; sources are the selected pair + void **dst0 = find_output(0); + void **dst1 = find_output(1); + void **src_l = find_output(src_left); + void **src_r = find_output(src_right); + if (dst0 == nullptr || dst1 == nullptr || src_l == nullptr || src_r == nullptr) { + log_warning( + "audio::wrappedasio", + "stereo downmix disabled: device is missing the front pair or source channels " + "{}/{} (game created {} channel(s))", + src_left, + src_right, + num_channels); + return; + } + + // all device output channels share one sample format; query it to size the copy + const AsioSampleType type = this->device_output_sample_type(); + const int sample_bytes = asio_sample_bytes(type); + if (sample_bytes <= 0) { + log_warning( + "audio::wrappedasio", + "stereo downmix disabled: unsupported sample format {} ({})", + asio_sample_type_name(type), + static_cast(type)); + return; + } + + this->downmix_copies[0] = {{dst0[0], dst0[1]}, {src_l[0], src_l[1]}}; + this->downmix_copies[1] = {{dst1[0], dst1[1]}, {src_r[0], src_r[1]}}; + this->downmix_bytes = static_cast(buffer_size) * sample_bytes; + this->downmix_active = true; + + log_info( + "audio::wrappedasio", + "stereo downmix active: pair={} (src {}/{} -> device 0/1), {} frames, {} byte(s)/sample", + stereo_downmix_name(STEREO_DOWNMIX), + src_left, + src_right, + buffer_size, + sample_bytes); +} + +void WrappedAsio::publish_post_process(long buffer_size) { + if (!this->volume_active && !this->downmix_active) { + return; + } + + // everything the realtime thread reads is now in place; make ourselves reachable + this->volume_buffer_size = buffer_size; + WrappedAsio::active_instance.store(this, std::memory_order_release); + + if (this->volume_active) { + log_info( + "audio::wrappedasio", + "volume boost active: gain={}, scaling {} output channel(s)", + this->volume_gain, + this->volume_channels.size()); + } +} + +void WrappedAsio::detach_post_process() { + // stop our realtime trampolines from reaching this wrapper, but only if we are the + // currently published instance + WrappedAsio *expected = this; + WrappedAsio::active_instance.compare_exchange_strong(expected, nullptr); +} + +void WrappedAsio::quiesce_for_reuse() { + // a previous abandoned probing cycle may have left a running stream and live buffers on + // the real driver without calling stop/dispose; tear that down now (without destroying + // the driver) so the host's next create_buffers starts clean. stop() guarantees no + // further buffer_switch, and dispose_buffers detaches our trampolines + if (this->started) { + log_info( + "audio::wrappedasio", + "reuse: stopping leftover stream on '{}' before handing the driver back", + this->driver_name); + this->stop(); + } + if (this->buffers_created) { + log_info( + "audio::wrappedasio", + "reuse: disposing leftover buffers on '{}' before handing the driver back", + this->driver_name); + this->dispose_buffers(); + } +} + +void WrappedAsio::apply_output_volume(long double_buffer_index) { + if (double_buffer_index != 0 && double_buffer_index != 1) { + return; + } + const float gain = this->volume_gain; + const long frames = this->volume_buffer_size; + for (const VolumeOutputChannel &ch : this->volume_channels) { + apply_gain_planar(ch.buffers[double_buffer_index], frames, ch.type, gain); + } +} + +void WrappedAsio::apply_downmix(long double_buffer_index) { + if (!this->downmix_active) { + return; + } + if (double_buffer_index != 0 && double_buffer_index != 1) { + return; + } + + // raw planar copy of the selected source channels onto device channels 0/1; for the + // center selection both copies share one source. skip self-copies (front) + for (const DownmixCopy © : this->downmix_copies) { + void *dst = copy.dst[double_buffer_index]; + const void *src = copy.src[double_buffer_index]; + if (dst != nullptr && src != nullptr && dst != src) { + std::memcpy(dst, src, this->downmix_bytes); + } + } +} + +void __cdecl WrappedAsio::proxy_buffer_switch(long double_buffer_index, AsioBool direct_process) { + WrappedAsio *self = WrappedAsio::active_instance.load(std::memory_order_acquire); + if (self == nullptr) { + return; + } + + // let the game write its samples into the driver buffers first, then rework them before + // the driver plays this half on the next switch: downmix first (arrange channels 0/1), + // then scale the device outputs by the volume boost + if (self->game_callbacks.buffer_switch != nullptr) { + self->game_callbacks.buffer_switch(double_buffer_index, direct_process); + } + self->apply_downmix(double_buffer_index); + self->apply_output_volume(double_buffer_index); +} + +AsioTime * __cdecl WrappedAsio::proxy_buffer_switch_time_info( + AsioTime *params, long double_buffer_index, AsioBool direct_process) +{ + WrappedAsio *self = WrappedAsio::active_instance.load(std::memory_order_acquire); + if (self == nullptr) { + return params; + } + + AsioTime *ret = params; + if (self->game_callbacks.buffer_switch_time_info != nullptr) { + ret = self->game_callbacks.buffer_switch_time_info( + params, double_buffer_index, direct_process); + } else if (self->game_callbacks.buffer_switch != nullptr) { + self->game_callbacks.buffer_switch(double_buffer_index, direct_process); + } + self->apply_downmix(double_buffer_index); + self->apply_output_volume(double_buffer_index); + return ret; +} + +AsioError __thiscall WrappedAsio::create_buffers( + AsioBufferInfo *buffer_infos, + long num_channels, + long buffer_size, + AsioCallbacks *callbacks) +{ + // swap in our buffer-switch trampolines if any post-processing is configured, so the + // real driver calls us and we rework its output after the game fills it (no-op otherwise) + AsioCallbacks *effective = this->install_proxy_callbacks(callbacks); + + if (force_two_channels()) { + return this->create_buffers_front_pair(buffer_infos, num_channels, buffer_size, effective); + } + + const AsioError result = this->pReal->create_buffers(buffer_infos, num_channels, buffer_size, effective); + if (result == ASE_OK) { + log_info( + "audio::wrappedasio", + "create_buffers(channels={}, size={} frames) succeeded", + num_channels, + buffer_size); + + // capture the post-process state now the buffers exist, then publish ourselves to + // the realtime thread once everything is in place. downmix is not recorded here: any + // active stereo extraction forces the front-pair path above, so this path only ever + // runs the volume boost + if (this->volume_active) { + for (long i = 0; i < num_channels; i++) { + this->record_volume_output_channel(buffer_infos[i]); + } + } + this->publish_post_process(buffer_size); + this->buffers_created = true; + } else { + log_warning( + "audio::wrappedasio", + "create_buffers(channels={}, size={} frames) failed, err={}", + num_channels, + buffer_size, + static_cast(result)); + } + + return result; +} + +AsioError WrappedAsio::create_buffers_front_pair( + AsioBufferInfo *buffer_infos, + long num_channels, + long buffer_size, + AsioCallbacks *callbacks) +{ + // front-pair extraction (forced two-channel ASIO): the game asks for more output + // channels than the real device has (e.g. 8 vs 2). forward only the channels the + // device actually provides (channel 0/1 = front L/R) and hand the game throwaway + // buffers for the rest, so its front mix lands on the device and the surround + // channels are discarded. the game writes directly into the driver/dummy buffers + // from its own bufferSwitch; the realtime work we do is the optional volume boost + // (scaling the forwarded device channels) and, if a non-front stereo downmix is + // selected, copying that pair from its dummy buffers onto the device's front pair + long real_in = 0, real_out = 0; + const AsioError ch_result = this->pReal->get_channels(&real_in, &real_out); + if (ch_result != ASE_OK) { + log_warning( + "audio::wrappedasio", + "create_buffers: get_channels failed, err={}", + static_cast(ch_result)); + return ch_result; + } + + // partition the requested channels: those the device can serve are forwarded, the rest + // are discarded. record source indices for both so we can patch the game's array after + std::vector forwarded; + std::vector forwarded_src; + std::vector discarded_src; + forwarded.reserve(num_channels); + forwarded_src.reserve(num_channels); + discarded_src.reserve(num_channels); + for (long i = 0; i < num_channels; i++) { + const AsioBufferInfo &bi = buffer_infos[i]; + const long limit = (bi.is_input == AsioTrue) ? real_in : real_out; + if (bi.channel_num < limit) { + forwarded.push_back(bi); + forwarded_src.push_back(i); + } else { + discarded_src.push_back(i); + } + } + + const AsioError result = this->pReal->create_buffers( + forwarded.data(), static_cast(forwarded.size()), buffer_size, callbacks); + if (result != ASE_OK) { + log_warning( + "audio::wrappedasio", + "create_buffers(forwarded={} of {}, size={} frames) failed, err={}", + forwarded.size(), + num_channels, + buffer_size, + static_cast(result)); + return result; + } + + // copy the real driver buffer pointers back into the game's array + for (size_t k = 0; k < forwarded.size(); k++) { + AsioBufferInfo &dst = buffer_infos[forwarded_src[k]]; + dst.buffers[0] = forwarded[k].buffers[0]; + dst.buffers[1] = forwarded[k].buffers[1]; + + // only the forwarded channels reach the device, so those are the ones the volume + // boost scales (the discarded channels go to throwaway buffers below) + if (this->volume_active) { + this->record_volume_output_channel(dst); + } + } + + // hand throwaway double buffers to the discarded channels. sized generously at + // 8 bytes/sample (covers every ASIO sample type) so the game can never overrun them + // regardless of the negotiated format + this->dummy_buffers.clear(); + this->dummy_buffers.reserve(discarded_src.size() * 2); + const size_t dummy_bytes = static_cast(buffer_size) * 8; + for (const long i : discarded_src) { + for (void *&buffer : buffer_infos[i].buffers) { + auto buf = std::make_unique(dummy_bytes); + std::memset(buf.get(), 0, dummy_bytes); + buffer = buf.get(); + this->dummy_buffers.push_back(std::move(buf)); + } + } + + // record the downmix source/destination buffers now the array is fully patched: the + // selected pair (e.g. rear) lives in the dummy buffers above, the device front pair in + // the forwarded driver buffers, so the realtime copy lands the chosen pair on the + // device's 2.0 output. then publish ourselves to the realtime thread + this->record_downmix_channels(buffer_infos, num_channels, buffer_size); + this->publish_post_process(buffer_size); + this->buffers_created = true; + + log_info( + "audio::wrappedasio", + "create_buffers: front-pair extraction - forwarded {} channel(s) to device, " + "discarded {} (requested {}, size={} frames)", + forwarded.size(), + discarded_src.size(), + num_channels, + buffer_size); + + return ASE_OK; +} + +AsioError __thiscall WrappedAsio::dispose_buffers() { + // stop our realtime trampolines from touching buffers the driver is about to free + this->detach_post_process(); + + const AsioError result = this->pReal->dispose_buffers(); + this->dummy_buffers.clear(); + this->volume_channels.clear(); + this->volume_active = false; + this->downmix_active = false; + this->buffers_created = false; + return result; +} + +AsioError __thiscall WrappedAsio::control_panel() { + return this->pReal->control_panel(); +} + +AsioError __thiscall WrappedAsio::future(long selector, void *opt) { + return this->pReal->future(selector, opt); +} + +AsioError __thiscall WrappedAsio::output_ready() { + return this->pReal->output_ready(); +} +#pragma endregion + +namespace hooks::audio::asio { + + IUnknown *wrap(REFCLSID clsid, void *real) { + log_info("audio::wrappedasio", "wrapping ASIO driver interface, clsid={}", guid2s(clsid)); + + auto *wrapper = new WrappedAsio( + reinterpret_cast(real), clsid, registered_asio_name(clsid)); + + // pin the wrapper (and the real driver it owns) for the process lifetime; later + // CoCreate calls reuse this same instance via wrap_existing + wrapper->AddRef(); + store_wrapper(clsid, wrapper); + + return static_cast(wrapper); + } + + IUnknown *wrap_existing(REFCLSID clsid) { + WrappedAsio *wrapper = find_wrapper(clsid); + if (wrapper == nullptr) { + return nullptr; + } + + log_misc( + "audio::wrappedasio", + "reusing cached ASIO driver instance, clsid={}", + guid2s(clsid)); + + wrapper->quiesce_for_reuse(); + + // hand the host another reference to the one instance we keep alive + wrapper->AddRef(); + return static_cast(wrapper); + } + + void release_all_wrappers() { + // collect the pinned wrappers under the lock, then drop our references outside it so + // a final Release (which can run the driver's own teardown) never happens while the + // lock is held + std::vector> wrappers; + { + std::lock_guard lock(g_wrappers_mutex); + wrappers = g_wrappers; + g_wrappers.clear(); + } + + for (auto &entry : wrappers) { + // drop only the pin wrap() took. if the host has already released its own + // references (the normal case at shutdown) this destroys the wrapper and releases + // the real driver; if the host is still using it, the refcount stays above zero + // and we leave its live stream/buffers untouched + log_misc( + "audio::wrappedasio", + "releasing cached ASIO driver instance, clsid={}", + guid2s(entry.first)); + entry.second->Release(); + } + } +} + diff --git a/src/spice2x/hooks/audio/asio_proxy.h b/src/spice2x/hooks/audio/asio_proxy.h index 2b2c229..9dd7eea 100644 --- a/src/spice2x/hooks/audio/asio_proxy.h +++ b/src/spice2x/hooks/audio/asio_proxy.h @@ -1,240 +1,240 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "external/asio/asio.h" -#include "external/asio/iasiodrv.h" - -namespace hooks::audio::asio { - - // returns true if a CoCreateInstance call is instantiating a registered ASIO driver. - // ASIO hosts pass the driver CLSID as both class id and interface id; we also validate - // it against the system's registered ASIO drivers to avoid false positives - bool is_asio_creation(REFCLSID rclsid, REFIID riid); - - // wrap a real ASIO driver instance, taking ownership of the supplied reference, and - // return a proxy that forwards every call to it. also records it as the cached - // instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing) - IUnknown *wrap(REFCLSID clsid, void *real); - - // if a cached wrapper already exists for this CLSID, return it (with an added - // reference); otherwise nullptr to signal the caller to create the real driver and - // wrap it. lets the host reuse one driver instance instead of re-instantiating it - IUnknown *wrap_existing(REFCLSID clsid); - - // drop the process-lifetime references taken by wrap() so cached drivers can be released - // at shutdown. only relinquishes our pin, so a real driver is torn down once the host - // has released its own references too. call from a controlled shutdown point, never from - // a static destructor (the driver DLL may already be unloaded) - void release_all_wrappers(); -} - -// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic -struct WrappedAsio final : IAsio { - WrappedAsio(IAsio *real, REFCLSID clsid, std::string name) - : pReal(real), clsid(clsid), driver_name(std::move(name)) { - } - - WrappedAsio(const WrappedAsio &) = delete; - WrappedAsio &operator=(const WrappedAsio &) = delete; - - virtual ~WrappedAsio(); - - // selects which source channel pair of a multichannel ASIO output reaches the device's - // 2.0 front pair. when not None, the proxy presents the game's expected multichannel - // layout to the host so it proceeds to create_buffers, then opens only a two-channel - // stream on the real device and routes the selected pair onto it (see create_buffers). - // Front is the plain "force two channel" case (forward the device's own front pair); - // the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed). - // set once at boot, before any wrapper exists, so it needs no synchronization - enum class StereoDownmix { - None, // feature disabled - full multichannel passthrough - Front, // channels 0/1 - the device front pair is forwarded as-is (no copy) - Center, // channel 2 duplicated to both 0 and 1 - Rear, // channels 4/5 -> 0/1 - Side, // channels 6/7 -> 0/1 - }; - static StereoDownmix STEREO_DOWNMIX; - - // true when a stereo extraction is configured, i.e. the real device should open a 2.0 - // stream and only the selected pair should reach it. the former standalone - // FORCE_TWO_CHANNELS flag is now just the Front case of this - static bool force_two_channels() { - return STEREO_DOWNMIX != StereoDownmix::None; - } - - // some games hardcode a multichannel ASIO output and bail before create_buffers if - // get_channels reports fewer, so we report at least this many output channels when a - // stereo extraction is active - static constexpr long FORCED_OUTPUT_CHANNELS = 8; - - // maps an option string ("front", "center", "rear", "side") to a StereoDownmix value, - // returning None for anything unrecognized - static StereoDownmix name_to_stereo_downmix(const char *name); - -#pragma region IUnknown - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override; - ULONG STDMETHODCALLTYPE AddRef() override; - ULONG STDMETHODCALLTYPE Release() override; -#pragma endregion - -#pragma region IAsio - AsioBool __thiscall init(void *sys_handle) override; - void __thiscall get_driver_name(char *name) override; - long __thiscall get_driver_version() override; - void __thiscall get_error_message(char *string) override; - AsioError __thiscall start() override; - AsioError __thiscall stop() override; - AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override; - AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override; - AsioError __thiscall get_buffer_size( - long *min_size, - long *max_size, - long *preferred_size, - long *granularity) override; - AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override; - AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override; - AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override; - AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override; - AsioError __thiscall set_clock_source(long reference) override; - AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override; - AsioError __thiscall get_channel_info(AsioChannelInfo *info) override; - AsioError __thiscall create_buffers( - AsioBufferInfo *buffer_infos, - long num_channels, - long buffer_size, - AsioCallbacks *callbacks) override; - AsioError __thiscall dispose_buffers() override; - AsioError __thiscall control_panel() override; - AsioError __thiscall future(long selector, void *opt) override; - AsioError __thiscall output_ready() override; -#pragma endregion - - // quiesces any leftover stream/buffer state before the cached wrapper is handed back - // for reuse, without destroying the real driver (see wrap_existing) - void quiesce_for_reuse(); - -private: - // create_buffers implementation used when a stereo extraction is active: forwards only - // the channels the real device has and hands the game throwaway buffers for the rest - AsioError create_buffers_front_pair( - AsioBufferInfo *buffer_infos, - long num_channels, - long buffer_size, - AsioCallbacks *callbacks); - - // if any post-processing effect (volume boost or stereo downmix) is active, saves the - // game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to - // hand the real driver instead, so we can rework its output buffers after the game - // fills them. otherwise returns the game's callbacks unchanged. called at create_buffers - // time, before the stream starts - AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks); - - // records a device output channel whose buffers we scale by the volume boost. queries - // the real driver for the channel's sample format. called at create_buffers time - void record_volume_output_channel(const AsioBufferInfo &info); - - // the real device's output sample format, queried from its first output channel. all - // output channels of a device share one format, so this characterizes them all. returns - // ASIOSTLastEntry if the device has no output channels or the query fails - AsioSampleType device_output_sample_type(); - - // locates the destination pair (device channels 0/1) and the configured source channels - // in the game's buffer set so the realtime path can copy the selected pair onto 0/1. - // a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time - void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size); - - // publishes the captured post-process state to the realtime thread once the buffers - // exist, making our trampolines start reworking output. called at the end of either - // create_buffers path - void publish_post_process(long buffer_size); - - // detaches this instance from the realtime trampolines so they stop touching its - // buffers. called from dispose_buffers and the destructor - void detach_post_process(); - - // multiplies every recorded output channel's buffer for the given double-buffer index - // by the volume boost. runs on the driver's realtime thread from our buffer switch - void apply_output_volume(long double_buffer_index); - - // copies the configured source channel pair onto device channels 0/1 for the given - // double-buffer index. runs on the driver's realtime thread from our buffer switch - void apply_downmix(long double_buffer_index); - - // realtime-thread trampolines for the buffer-switch callbacks, handed to the real - // driver in place of the game's; ASIO callbacks carry no user data, so they reach the - // active wrapper through active_instance, call the game's original, then rework output. - // the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the - // game's own pointers, so they need no trampoline - static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process); - static AsioTime * __cdecl proxy_buffer_switch_time_info( - AsioTime *params, long double_buffer_index, AsioBool direct_process); - - // the single wrapper whose proxy callbacks are installed (ASIO is single-instance with - // one running stream); read by the static trampolines to reach the right wrapper - static std::atomic active_instance; - - IAsio *const pReal; - const CLSID clsid; - - // registry name of the driver (not get_driver_name), used in our logs as a single - // unambiguous name; constant for our lifetime - std::string driver_name; - - // the real driver is initialized exactly once; repeat init() calls are a no-op success - bool initialized = false; - - // whether the real driver currently has a buffer set / running stream. used to quiesce - // leftover state when the cached wrapper is reused (see quiesce_for_reuse) - bool buffers_created = false; - bool started = false; - - // our own reference count; we hold one reference on pReal and release it when this - // drops to zero - std::atomic ref_count {1}; - - // throwaway double buffers handed to the channels we discard when a stereo extraction - // is active (see create_buffers). owned for the lifetime of the buffer set and freed - // in dispose_buffers; only read by the game from its own bufferSwitch, never by us - std::vector> dummy_buffers; - - // one device output channel scaled by the volume boost in our buffer switch - struct VolumeOutputChannel { - void *buffers[2]; - AsioSampleType type; - }; - - // the game's original callbacks (captured when we install our proxy set) and the proxy - // set we hand the real driver; the realtime trampolines reach the game's buffer_switch - // through game_callbacks regardless of which effect is active - AsioCallbacks game_callbacks {}; - AsioCallbacks proxy_callbacks {}; - - // volume boost state, captured at create_buffers time and published to the realtime - // thread via active_instance once fully built; untouched while the stream runs. - // volume_active gates whether the realtime path scales any buffers - bool volume_active = false; - float volume_gain = 1.0f; - long volume_buffer_size = 0; - std::vector volume_channels; - - // one device channel (0 or 1) fed by a source channel during stereo downmix; both - // buffer pointers are indexed by the ASIO double-buffer index, the same as the channels - struct DownmixCopy { - void *dst[2]; - void *src[2]; - }; - - // stereo downmix state, captured at create_buffers time and published alongside the - // volume state; untouched while the stream runs. downmix_active gates whether the - // realtime path copies the selected source pair onto device channels 0/1. copies[0] - // feeds device channel 0, copies[1] feeds device channel 1 - bool downmix_active = false; - DownmixCopy downmix_copies[2] {}; - size_t downmix_bytes = 0; -}; +#pragma once + +#include +#include +#include +#include + +#include + +#include "external/asio/asio.h" +#include "external/asio/iasiodrv.h" + +namespace hooks::audio::asio { + + // returns true if a CoCreateInstance call is instantiating a registered ASIO driver. + // ASIO hosts pass the driver CLSID as both class id and interface id; we also validate + // it against the system's registered ASIO drivers to avoid false positives + bool is_asio_creation(REFCLSID rclsid, REFIID riid); + + // wrap a real ASIO driver instance, taking ownership of the supplied reference, and + // return a proxy that forwards every call to it. also records it as the cached + // instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing) + IUnknown *wrap(REFCLSID clsid, void *real); + + // if a cached wrapper already exists for this CLSID, return it (with an added + // reference); otherwise nullptr to signal the caller to create the real driver and + // wrap it. lets the host reuse one driver instance instead of re-instantiating it + IUnknown *wrap_existing(REFCLSID clsid); + + // drop the process-lifetime references taken by wrap() so cached drivers can be released + // at shutdown. only relinquishes our pin, so a real driver is torn down once the host + // has released its own references too. call from a controlled shutdown point, never from + // a static destructor (the driver DLL may already be unloaded) + void release_all_wrappers(); +} + +// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic +struct WrappedAsio final : IAsio { + WrappedAsio(IAsio *real, REFCLSID clsid, std::string name) + : pReal(real), clsid(clsid), driver_name(std::move(name)) { + } + + WrappedAsio(const WrappedAsio &) = delete; + WrappedAsio &operator=(const WrappedAsio &) = delete; + + virtual ~WrappedAsio(); + + // selects which source channel pair of a multichannel ASIO output reaches the device's + // 2.0 front pair. when not None, the proxy presents the game's expected multichannel + // layout to the host so it proceeds to create_buffers, then opens only a two-channel + // stream on the real device and routes the selected pair onto it (see create_buffers). + // Front is the plain "force two channel" case (forward the device's own front pair); + // the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed). + // set once at boot, before any wrapper exists, so it needs no synchronization + enum class StereoDownmix { + None, // feature disabled - full multichannel passthrough + Front, // channels 0/1 - the device front pair is forwarded as-is (no copy) + Center, // channel 2 duplicated to both 0 and 1 + Rear, // channels 4/5 -> 0/1 + Side, // channels 6/7 -> 0/1 + }; + static StereoDownmix STEREO_DOWNMIX; + + // true when a stereo extraction is configured, i.e. the real device should open a 2.0 + // stream and only the selected pair should reach it. the former standalone + // FORCE_TWO_CHANNELS flag is now just the Front case of this + static bool force_two_channels() { + return STEREO_DOWNMIX != StereoDownmix::None; + } + + // some games hardcode a multichannel ASIO output and bail before create_buffers if + // get_channels reports fewer, so we report at least this many output channels when a + // stereo extraction is active + static constexpr long FORCED_OUTPUT_CHANNELS = 8; + + // maps an option string ("front", "center", "rear", "side") to a StereoDownmix value, + // returning None for anything unrecognized + static StereoDownmix name_to_stereo_downmix(const char *name); + +#pragma region IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; +#pragma endregion + +#pragma region IAsio + AsioBool __thiscall init(void *sys_handle) override; + void __thiscall get_driver_name(char *name) override; + long __thiscall get_driver_version() override; + void __thiscall get_error_message(char *string) override; + AsioError __thiscall start() override; + AsioError __thiscall stop() override; + AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override; + AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override; + AsioError __thiscall get_buffer_size( + long *min_size, + long *max_size, + long *preferred_size, + long *granularity) override; + AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override; + AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override; + AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override; + AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override; + AsioError __thiscall set_clock_source(long reference) override; + AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override; + AsioError __thiscall get_channel_info(AsioChannelInfo *info) override; + AsioError __thiscall create_buffers( + AsioBufferInfo *buffer_infos, + long num_channels, + long buffer_size, + AsioCallbacks *callbacks) override; + AsioError __thiscall dispose_buffers() override; + AsioError __thiscall control_panel() override; + AsioError __thiscall future(long selector, void *opt) override; + AsioError __thiscall output_ready() override; +#pragma endregion + + // quiesces any leftover stream/buffer state before the cached wrapper is handed back + // for reuse, without destroying the real driver (see wrap_existing) + void quiesce_for_reuse(); + +private: + // create_buffers implementation used when a stereo extraction is active: forwards only + // the channels the real device has and hands the game throwaway buffers for the rest + AsioError create_buffers_front_pair( + AsioBufferInfo *buffer_infos, + long num_channels, + long buffer_size, + AsioCallbacks *callbacks); + + // if any post-processing effect (volume boost or stereo downmix) is active, saves the + // game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to + // hand the real driver instead, so we can rework its output buffers after the game + // fills them. otherwise returns the game's callbacks unchanged. called at create_buffers + // time, before the stream starts + AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks); + + // records a device output channel whose buffers we scale by the volume boost. queries + // the real driver for the channel's sample format. called at create_buffers time + void record_volume_output_channel(const AsioBufferInfo &info); + + // the real device's output sample format, queried from its first output channel. all + // output channels of a device share one format, so this characterizes them all. returns + // ASIOSTLastEntry if the device has no output channels or the query fails + AsioSampleType device_output_sample_type(); + + // locates the destination pair (device channels 0/1) and the configured source channels + // in the game's buffer set so the realtime path can copy the selected pair onto 0/1. + // a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time + void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size); + + // publishes the captured post-process state to the realtime thread once the buffers + // exist, making our trampolines start reworking output. called at the end of either + // create_buffers path + void publish_post_process(long buffer_size); + + // detaches this instance from the realtime trampolines so they stop touching its + // buffers. called from dispose_buffers and the destructor + void detach_post_process(); + + // multiplies every recorded output channel's buffer for the given double-buffer index + // by the volume boost. runs on the driver's realtime thread from our buffer switch + void apply_output_volume(long double_buffer_index); + + // copies the configured source channel pair onto device channels 0/1 for the given + // double-buffer index. runs on the driver's realtime thread from our buffer switch + void apply_downmix(long double_buffer_index); + + // realtime-thread trampolines for the buffer-switch callbacks, handed to the real + // driver in place of the game's; ASIO callbacks carry no user data, so they reach the + // active wrapper through active_instance, call the game's original, then rework output. + // the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the + // game's own pointers, so they need no trampoline + static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process); + static AsioTime * __cdecl proxy_buffer_switch_time_info( + AsioTime *params, long double_buffer_index, AsioBool direct_process); + + // the single wrapper whose proxy callbacks are installed (ASIO is single-instance with + // one running stream); read by the static trampolines to reach the right wrapper + static std::atomic active_instance; + + IAsio *const pReal; + const CLSID clsid; + + // registry name of the driver (not get_driver_name), used in our logs as a single + // unambiguous name; constant for our lifetime + std::string driver_name; + + // the real driver is initialized exactly once; repeat init() calls are a no-op success + bool initialized = false; + + // whether the real driver currently has a buffer set / running stream. used to quiesce + // leftover state when the cached wrapper is reused (see quiesce_for_reuse) + bool buffers_created = false; + bool started = false; + + // our own reference count; we hold one reference on pReal and release it when this + // drops to zero + std::atomic ref_count {1}; + + // throwaway double buffers handed to the channels we discard when a stereo extraction + // is active (see create_buffers). owned for the lifetime of the buffer set and freed + // in dispose_buffers; only read by the game from its own bufferSwitch, never by us + std::vector> dummy_buffers; + + // one device output channel scaled by the volume boost in our buffer switch + struct VolumeOutputChannel { + void *buffers[2]; + AsioSampleType type; + }; + + // the game's original callbacks (captured when we install our proxy set) and the proxy + // set we hand the real driver; the realtime trampolines reach the game's buffer_switch + // through game_callbacks regardless of which effect is active + AsioCallbacks game_callbacks {}; + AsioCallbacks proxy_callbacks {}; + + // volume boost state, captured at create_buffers time and published to the realtime + // thread via active_instance once fully built; untouched while the stream runs. + // volume_active gates whether the realtime path scales any buffers + bool volume_active = false; + float volume_gain = 1.0f; + long volume_buffer_size = 0; + std::vector volume_channels; + + // one device channel (0 or 1) fed by a source channel during stereo downmix; both + // buffer pointers are indexed by the ASIO double-buffer index, the same as the channels + struct DownmixCopy { + void *dst[2]; + void *src[2]; + }; + + // stereo downmix state, captured at create_buffers time and published alongside the + // volume state; untouched while the stream runs. downmix_active gates whether the + // realtime path copies the selected source pair onto device channels 0/1. copies[0] + // feeds device channel 0, copies[1] feeds device channel 1 + bool downmix_active = false; + DownmixCopy downmix_copies[2] {}; + size_t downmix_bytes = 0; +}; diff --git a/src/spice2x/hooks/audio/backends/mmdevice/audio_endpoint_volume.h b/src/spice2x/hooks/audio/backends/mmdevice/audio_endpoint_volume.h index 205cf33..13e28ed 100644 --- a/src/spice2x/hooks/audio/backends/mmdevice/audio_endpoint_volume.h +++ b/src/spice2x/hooks/audio/backends/mmdevice/audio_endpoint_volume.h @@ -1,43 +1,43 @@ -#pragma once - -#include -#include - -struct WrappedIAudioEndpointVolume : IAudioEndpointVolume { - explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {} - - WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete; - WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete; - - virtual ~WrappedIAudioEndpointVolume() = default; - -#pragma region IUnknown - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; - ULONG STDMETHODCALLTYPE AddRef() override; - ULONG STDMETHODCALLTYPE Release() override; -#pragma endregion - -#pragma region IAudioEndpointVolume - HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; - HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; - HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override; - HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override; - HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override; - HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override; - HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override; - HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override; - HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override; - HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override; - HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override; - HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override; -#pragma endregion - -private: - IAudioEndpointVolume *const pReal; +#pragma once + +#include +#include + +struct WrappedIAudioEndpointVolume : IAudioEndpointVolume { + explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {} + + WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete; + WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete; + + virtual ~WrappedIAudioEndpointVolume() = default; + +#pragma region IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; +#pragma endregion + +#pragma region IAudioEndpointVolume + HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; + HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; + HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override; + HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override; + HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override; + HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override; + HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override; + HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override; + HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override; + HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override; + HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override; + HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override; +#pragma endregion + +private: + IAudioEndpointVolume *const pReal; }; \ No newline at end of file diff --git a/src/spice2x/hooks/audio/backends/mmdevice/null_device.cpp b/src/spice2x/hooks/audio/backends/mmdevice/null_device.cpp index 424e612..0e5a2bd 100644 --- a/src/spice2x/hooks/audio/backends/mmdevice/null_device.cpp +++ b/src/spice2x/hooks/audio/backends/mmdevice/null_device.cpp @@ -1,185 +1,185 @@ -#include "null_device.h" - -#include -#include - -#include - -#include "hooks/audio/audio.h" -#include "hooks/audio/audio_private.h" -#include "hooks/audio/backends/wasapi/dummy_audio_client.h" -#include "util/logging.h" -#include "util/utils.h" - -#include "null_discard_backend.h" - -// friendly name reported by the synthetic device. must contain "Realtek" so the -// gitadora arena device search matches it. -static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio"; - -// arbitrary identifier reported by the synthetic device. -static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}"; - -// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h -static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = { - { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } }, - 14 -}; - -bool null_render_device_enabled() { - return hooks::audio::INJECT_FAKE_REALTEK_AUDIO; -} - -// duplicate a wide string into CoTaskMem so the caller can free it with -// CoTaskMemFree / PropVariantClear as the COM API contract requires. -static LPWSTR co_task_wcsdup(const wchar_t *src) { - const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t); - auto *dst = static_cast(CoTaskMemAlloc(bytes)); - if (dst != nullptr) { - memcpy(dst, src, bytes); - } - return dst; -} - -namespace { - - // minimal IPropertyStore that only answers PKEY_Device_FriendlyName. - struct NullPropertyStore : IPropertyStore { - std::atomic ref_cnt = 1; - - virtual ~NullPropertyStore() = default; - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override { - if (ppvObj == nullptr) { - return E_POINTER; - } - if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) { - this->AddRef(); - *ppvObj = this; - return S_OK; - } - *ppvObj = nullptr; - return E_NOINTERFACE; - } - ULONG STDMETHODCALLTYPE AddRef() override { - return ++this->ref_cnt; - } - ULONG STDMETHODCALLTYPE Release() override { - const ULONG refs = --this->ref_cnt; - if (refs == 0) { - delete this; - } - return refs; - } - HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override { - if (cProps == nullptr) { - return E_POINTER; - } - *cProps = 1; - return S_OK; - } - HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override { - if (pkey == nullptr) { - return E_POINTER; - } - if (iProp != 0) { - return E_INVALIDARG; - } - *pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL; - return S_OK; - } - HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override { - if (pv == nullptr) { - return E_POINTER; - } - PropVariantInit(pv); - if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid - && key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) { - pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME); - if (pv->pwszVal == nullptr) { - return E_OUTOFMEMORY; - } - pv->vt = VT_LPWSTR; - } - // unknown keys are returned as VT_EMPTY / S_OK - return S_OK; - } - HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override { - return STG_E_ACCESSDENIED; - } - HRESULT STDMETHODCALLTYPE Commit() override { - return S_OK; - } - }; -} - -#pragma region IUnknown -HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) { - if (ppvObj == nullptr) { - return E_POINTER; - } - if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) { - this->AddRef(); - *ppvObj = this; - return S_OK; - } - *ppvObj = nullptr; - return E_NOINTERFACE; -} -ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() { - return ++this->ref_cnt; -} -ULONG STDMETHODCALLTYPE NullMMDevice::Release() { - const ULONG refs = --this->ref_cnt; - if (refs == 0) { - delete this; - } - return refs; -} -#pragma endregion - -#pragma region IMMDevice -HRESULT STDMETHODCALLTYPE NullMMDevice::Activate( - REFIID iid, - DWORD, - PROPVARIANT *, - void **ppInterface) -{ - if (ppInterface == nullptr) { - return E_POINTER; - } - *ppInterface = nullptr; - - log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid)); - - if (iid == IID_IAudioClient) { - auto *client = static_cast(new DummyIAudioClient(new NullDiscardBackend())); - *ppInterface = client; - - return S_OK; - } - - return E_NOINTERFACE; -} -HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) { - if (ppProperties == nullptr) { - return E_POINTER; - } - *ppProperties = new NullPropertyStore(); - return S_OK; -} -HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) { - if (ppstrId == nullptr) { - return E_POINTER; - } - *ppstrId = co_task_wcsdup(NULL_DEVICE_ID); - return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY; -} -HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) { - if (pdwState == nullptr) { - return E_POINTER; - } - *pdwState = DEVICE_STATE_ACTIVE; - return S_OK; -} -#pragma endregion +#include "null_device.h" + +#include +#include + +#include + +#include "hooks/audio/audio.h" +#include "hooks/audio/audio_private.h" +#include "hooks/audio/backends/wasapi/dummy_audio_client.h" +#include "util/logging.h" +#include "util/utils.h" + +#include "null_discard_backend.h" + +// friendly name reported by the synthetic device. must contain "Realtek" so the +// gitadora arena device search matches it. +static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio"; + +// arbitrary identifier reported by the synthetic device. +static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}"; + +// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h +static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = { + { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } }, + 14 +}; + +bool null_render_device_enabled() { + return hooks::audio::INJECT_FAKE_REALTEK_AUDIO; +} + +// duplicate a wide string into CoTaskMem so the caller can free it with +// CoTaskMemFree / PropVariantClear as the COM API contract requires. +static LPWSTR co_task_wcsdup(const wchar_t *src) { + const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t); + auto *dst = static_cast(CoTaskMemAlloc(bytes)); + if (dst != nullptr) { + memcpy(dst, src, bytes); + } + return dst; +} + +namespace { + + // minimal IPropertyStore that only answers PKEY_Device_FriendlyName. + struct NullPropertyStore : IPropertyStore { + std::atomic ref_cnt = 1; + + virtual ~NullPropertyStore() = default; + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override { + if (ppvObj == nullptr) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) { + this->AddRef(); + *ppvObj = this; + return S_OK; + } + *ppvObj = nullptr; + return E_NOINTERFACE; + } + ULONG STDMETHODCALLTYPE AddRef() override { + return ++this->ref_cnt; + } + ULONG STDMETHODCALLTYPE Release() override { + const ULONG refs = --this->ref_cnt; + if (refs == 0) { + delete this; + } + return refs; + } + HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override { + if (cProps == nullptr) { + return E_POINTER; + } + *cProps = 1; + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override { + if (pkey == nullptr) { + return E_POINTER; + } + if (iProp != 0) { + return E_INVALIDARG; + } + *pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL; + return S_OK; + } + HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override { + if (pv == nullptr) { + return E_POINTER; + } + PropVariantInit(pv); + if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid + && key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) { + pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME); + if (pv->pwszVal == nullptr) { + return E_OUTOFMEMORY; + } + pv->vt = VT_LPWSTR; + } + // unknown keys are returned as VT_EMPTY / S_OK + return S_OK; + } + HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override { + return STG_E_ACCESSDENIED; + } + HRESULT STDMETHODCALLTYPE Commit() override { + return S_OK; + } + }; +} + +#pragma region IUnknown +HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) { + if (ppvObj == nullptr) { + return E_POINTER; + } + if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) { + this->AddRef(); + *ppvObj = this; + return S_OK; + } + *ppvObj = nullptr; + return E_NOINTERFACE; +} +ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() { + return ++this->ref_cnt; +} +ULONG STDMETHODCALLTYPE NullMMDevice::Release() { + const ULONG refs = --this->ref_cnt; + if (refs == 0) { + delete this; + } + return refs; +} +#pragma endregion + +#pragma region IMMDevice +HRESULT STDMETHODCALLTYPE NullMMDevice::Activate( + REFIID iid, + DWORD, + PROPVARIANT *, + void **ppInterface) +{ + if (ppInterface == nullptr) { + return E_POINTER; + } + *ppInterface = nullptr; + + log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid)); + + if (iid == IID_IAudioClient) { + auto *client = static_cast(new DummyIAudioClient(new NullDiscardBackend())); + *ppInterface = client; + + return S_OK; + } + + return E_NOINTERFACE; +} +HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) { + if (ppProperties == nullptr) { + return E_POINTER; + } + *ppProperties = new NullPropertyStore(); + return S_OK; +} +HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) { + if (ppstrId == nullptr) { + return E_POINTER; + } + *ppstrId = co_task_wcsdup(NULL_DEVICE_ID); + return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY; +} +HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) { + if (pdwState == nullptr) { + return E_POINTER; + } + *pdwState = DEVICE_STATE_ACTIVE; + return S_OK; +} +#pragma endregion diff --git a/src/spice2x/hooks/audio/backends/mmdevice/null_device.h b/src/spice2x/hooks/audio/backends/mmdevice/null_device.h index ea64f6f..a479869 100644 --- a/src/spice2x/hooks/audio/backends/mmdevice/null_device.h +++ b/src/spice2x/hooks/audio/backends/mmdevice/null_device.h @@ -1,39 +1,39 @@ -#pragma once - -#include - -#include - -// returns true when a synthetic render endpoint should be injected into device -// enumeration. games like gitadora arena search the render endpoint list for a -// device whose friendly name contains "Realtek" and crash with a null pointer -// dereference when no match exists. presenting a fake match that routes to the -// null audio backend lets the search succeed while discarding the audio. -bool null_render_device_enabled(); - -// fake IMMDevice that reports a "Realtek" friendly name and activates straight -// into the null audio backend, never touching real hardware. -struct NullMMDevice : IMMDevice { - NullMMDevice() = default; - - NullMMDevice(const NullMMDevice &) = delete; - NullMMDevice &operator=(const NullMMDevice &) = delete; - - virtual ~NullMMDevice() = default; - -#pragma region IUnknown - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; - ULONG STDMETHODCALLTYPE AddRef() override; - ULONG STDMETHODCALLTYPE Release() override; -#pragma endregion - -#pragma region IMMDevice - HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override; - HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override; - HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override; - HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override; -#pragma endregion - -private: - std::atomic ref_cnt = 1; -}; +#pragma once + +#include + +#include + +// returns true when a synthetic render endpoint should be injected into device +// enumeration. games like gitadora arena search the render endpoint list for a +// device whose friendly name contains "Realtek" and crash with a null pointer +// dereference when no match exists. presenting a fake match that routes to the +// null audio backend lets the search succeed while discarding the audio. +bool null_render_device_enabled(); + +// fake IMMDevice that reports a "Realtek" friendly name and activates straight +// into the null audio backend, never touching real hardware. +struct NullMMDevice : IMMDevice { + NullMMDevice() = default; + + NullMMDevice(const NullMMDevice &) = delete; + NullMMDevice &operator=(const NullMMDevice &) = delete; + + virtual ~NullMMDevice() = default; + +#pragma region IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; +#pragma endregion + +#pragma region IMMDevice + HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override; + HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override; + HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override; + HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override; +#pragma endregion + +private: + std::atomic ref_cnt = 1; +}; diff --git a/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.cpp b/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.cpp index 4c1c949..20d0a1b 100644 --- a/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.cpp +++ b/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.cpp @@ -1,139 +1,139 @@ -#include "null_discard_backend.h" - -#include -#include -#include - -#include "hooks/audio/util.h" -#include "util/logging.h" - -NullDiscardBackend::~NullDiscardBackend() { - this->running = false; - if (this->pacing_thread.joinable()) { - this->pacing_thread.join(); - } -} - -const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept { - return this->format_; -} - -HRESULT NullDiscardBackend::on_initialize( - AUDCLNT_SHAREMODE *, - DWORD *, - REFERENCE_TIME *hnsBufferDuration, - REFERENCE_TIME *, - const WAVEFORMATEX *pFormat, - LPCGUID) -{ - copy_wave_format(&this->format_, pFormat); - - // honor the game's requested buffer duration, falling back to 10 ms - constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units - this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0) - ? *hnsBufferDuration - : DEFAULT_REFTIME; - - this->buffer_frames = std::max(1, static_cast( - static_cast(this->format_.Format.nSamplesPerSec) - * this->period_reftime / 10000000.0 + 0.5)); - - log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit", - this->format_.Format.nChannels, - this->format_.Format.nSamplesPerSec, - this->format_.Format.wBitsPerSample); - - return S_OK; -} - -HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) { - *buffer_frames = this->buffer_frames; - return S_OK; -} - -HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) { - *latency = this->period_reftime; - return S_OK; -} - -HRESULT NullDiscardBackend::on_get_current_padding(std::optional &padding_frames) { - // discarded immediately, so the buffer always reads as fully drained - padding_frames = 0; - return S_OK; -} - -HRESULT NullDiscardBackend::on_is_format_supported( - AUDCLNT_SHAREMODE *, - const WAVEFORMATEX *, - WAVEFORMATEX **ppClosestMatch) -{ - if (ppClosestMatch) { - *ppClosestMatch = nullptr; - } - return S_OK; -} - -HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) { - return E_NOTIMPL; -} - -HRESULT NullDiscardBackend::on_get_device_period( - REFERENCE_TIME *default_device_period, - REFERENCE_TIME *minimum_device_period) -{ - if (default_device_period) { - *default_device_period = this->period_reftime; - } - if (minimum_device_period) { - *minimum_device_period = this->period_reftime; - } - return S_OK; -} - -HRESULT NullDiscardBackend::on_start() { - if (!this->running.exchange(true)) { - this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this); - } - return S_OK; -} - -HRESULT NullDiscardBackend::on_stop() { - return S_OK; -} - -HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) { - // keep the game's event so pace_loop() can wake it; there is no real device behind it - this->relay_handle = *event_handle; - return S_OK; -} - -HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) { - const size_t buffer_size = - static_cast(this->format_.Format.nBlockAlign) * num_frames_requested; - if (this->scratch.size() < buffer_size) { - this->scratch.resize(buffer_size); - } - *ppData = this->scratch.data(); - return S_OK; -} - -HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) { - // discard the audio entirely - return S_OK; -} - -void NullDiscardBackend::pace_loop() { - using namespace std::chrono; - - // audio is discarded, so timing precision and drift do not matter; just wake the - // game once per buffer period to keep its render thread from blocking on the event. - const auto period = duration_cast( - duration(this->period_reftime / 10000000.0)); - - while (this->running.load()) { - if (this->relay_handle) { - SetEvent(this->relay_handle); - } - std::this_thread::sleep_for(period); - } -} +#include "null_discard_backend.h" + +#include +#include +#include + +#include "hooks/audio/util.h" +#include "util/logging.h" + +NullDiscardBackend::~NullDiscardBackend() { + this->running = false; + if (this->pacing_thread.joinable()) { + this->pacing_thread.join(); + } +} + +const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept { + return this->format_; +} + +HRESULT NullDiscardBackend::on_initialize( + AUDCLNT_SHAREMODE *, + DWORD *, + REFERENCE_TIME *hnsBufferDuration, + REFERENCE_TIME *, + const WAVEFORMATEX *pFormat, + LPCGUID) +{ + copy_wave_format(&this->format_, pFormat); + + // honor the game's requested buffer duration, falling back to 10 ms + constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units + this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0) + ? *hnsBufferDuration + : DEFAULT_REFTIME; + + this->buffer_frames = std::max(1, static_cast( + static_cast(this->format_.Format.nSamplesPerSec) + * this->period_reftime / 10000000.0 + 0.5)); + + log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit", + this->format_.Format.nChannels, + this->format_.Format.nSamplesPerSec, + this->format_.Format.wBitsPerSample); + + return S_OK; +} + +HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) { + *buffer_frames = this->buffer_frames; + return S_OK; +} + +HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) { + *latency = this->period_reftime; + return S_OK; +} + +HRESULT NullDiscardBackend::on_get_current_padding(std::optional &padding_frames) { + // discarded immediately, so the buffer always reads as fully drained + padding_frames = 0; + return S_OK; +} + +HRESULT NullDiscardBackend::on_is_format_supported( + AUDCLNT_SHAREMODE *, + const WAVEFORMATEX *, + WAVEFORMATEX **ppClosestMatch) +{ + if (ppClosestMatch) { + *ppClosestMatch = nullptr; + } + return S_OK; +} + +HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) { + return E_NOTIMPL; +} + +HRESULT NullDiscardBackend::on_get_device_period( + REFERENCE_TIME *default_device_period, + REFERENCE_TIME *minimum_device_period) +{ + if (default_device_period) { + *default_device_period = this->period_reftime; + } + if (minimum_device_period) { + *minimum_device_period = this->period_reftime; + } + return S_OK; +} + +HRESULT NullDiscardBackend::on_start() { + if (!this->running.exchange(true)) { + this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this); + } + return S_OK; +} + +HRESULT NullDiscardBackend::on_stop() { + return S_OK; +} + +HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) { + // keep the game's event so pace_loop() can wake it; there is no real device behind it + this->relay_handle = *event_handle; + return S_OK; +} + +HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) { + const size_t buffer_size = + static_cast(this->format_.Format.nBlockAlign) * num_frames_requested; + if (this->scratch.size() < buffer_size) { + this->scratch.resize(buffer_size); + } + *ppData = this->scratch.data(); + return S_OK; +} + +HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) { + // discard the audio entirely + return S_OK; +} + +void NullDiscardBackend::pace_loop() { + using namespace std::chrono; + + // audio is discarded, so timing precision and drift do not matter; just wake the + // game once per buffer period to keep its render thread from blocking on the event. + const auto period = duration_cast( + duration(this->period_reftime / 10000000.0)); + + while (this->running.load()) { + if (this->relay_handle) { + SetEvent(this->relay_handle); + } + std::this_thread::sleep_for(period); + } +} diff --git a/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.h b/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.h index ceef773..7072de6 100644 --- a/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.h +++ b/src/spice2x/hooks/audio/backends/mmdevice/null_discard_backend.h @@ -1,54 +1,54 @@ -#pragma once - -#include -#include -#include -#include - -#include - -#include "hooks/audio/implementations/backend.h" - -// discards all audio while pacing the game's event handle once per buffer period, so the game -// keeps running normally with nothing output to any real device. routed through the shared -// DummyIAudioClient, the same plumbing the asio backend uses. -struct NullDiscardBackend final : AudioBackend { - ~NullDiscardBackend() final; - - const WAVEFORMATEXTENSIBLE &format() const noexcept override; - - HRESULT on_initialize( - AUDCLNT_SHAREMODE *, - DWORD *, - REFERENCE_TIME *hnsBufferDuration, - REFERENCE_TIME *, - const WAVEFORMATEX *pFormat, - LPCGUID) override; - HRESULT on_get_buffer_size(uint32_t *buffer_frames) override; - HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override; - HRESULT on_get_current_padding(std::optional &padding_frames) override; - HRESULT on_is_format_supported( - AUDCLNT_SHAREMODE *, - const WAVEFORMATEX *, - WAVEFORMATEX **ppClosestMatch) override; - HRESULT on_get_mix_format(WAVEFORMATEX **) override; - HRESULT on_get_device_period( - REFERENCE_TIME *default_device_period, - REFERENCE_TIME *minimum_device_period) override; - HRESULT on_start() override; - HRESULT on_stop() override; - HRESULT on_set_event_handle(HANDLE *event_handle) override; - HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override; - HRESULT on_release_buffer(uint32_t, DWORD) override; - -private: - void pace_loop(); - - WAVEFORMATEXTENSIBLE format_ {}; - uint32_t buffer_frames = 0; - REFERENCE_TIME period_reftime = 0; - HANDLE relay_handle = nullptr; - std::vector scratch; - std::thread pacing_thread; - std::atomic running = false; -}; +#pragma once + +#include +#include +#include +#include + +#include + +#include "hooks/audio/implementations/backend.h" + +// discards all audio while pacing the game's event handle once per buffer period, so the game +// keeps running normally with nothing output to any real device. routed through the shared +// DummyIAudioClient, the same plumbing the asio backend uses. +struct NullDiscardBackend final : AudioBackend { + ~NullDiscardBackend() final; + + const WAVEFORMATEXTENSIBLE &format() const noexcept override; + + HRESULT on_initialize( + AUDCLNT_SHAREMODE *, + DWORD *, + REFERENCE_TIME *hnsBufferDuration, + REFERENCE_TIME *, + const WAVEFORMATEX *pFormat, + LPCGUID) override; + HRESULT on_get_buffer_size(uint32_t *buffer_frames) override; + HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override; + HRESULT on_get_current_padding(std::optional &padding_frames) override; + HRESULT on_is_format_supported( + AUDCLNT_SHAREMODE *, + const WAVEFORMATEX *, + WAVEFORMATEX **ppClosestMatch) override; + HRESULT on_get_mix_format(WAVEFORMATEX **) override; + HRESULT on_get_device_period( + REFERENCE_TIME *default_device_period, + REFERENCE_TIME *minimum_device_period) override; + HRESULT on_start() override; + HRESULT on_stop() override; + HRESULT on_set_event_handle(HANDLE *event_handle) override; + HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override; + HRESULT on_release_buffer(uint32_t, DWORD) override; + +private: + void pace_loop(); + + WAVEFORMATEXTENSIBLE format_ {}; + uint32_t buffer_frames = 0; + REFERENCE_TIME period_reftime = 0; + HANDLE relay_handle = nullptr; + std::vector scratch; + std::thread pacing_thread; + std::atomic running = false; +}; diff --git a/src/spice2x/hooks/audio/backends/wasapi/downmix.cpp b/src/spice2x/hooks/audio/backends/wasapi/downmix.cpp index 16a3da4..b960832 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/downmix.cpp +++ b/src/spice2x/hooks/audio/backends/wasapi/downmix.cpp @@ -1,264 +1,264 @@ -#include "downmix.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include "util/logging.h" - -#include "util.h" - -namespace hooks::audio { - - namespace { - - constexpr float ATT_3DB = 0.70710678f; - - // speakers routed to the left/right output; anything else (center) feeds both sides - constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT - | SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT; - constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT - | SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT; - - // the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats - DWORD read_channel_mask(const WAVEFORMATEX *fmt) { - if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE - && fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { - return reinterpret_cast(fmt)->dwChannelMask; - } - return 0; - } - - // call visit(channel_index, speaker_bit) for each present speaker, in channel order - template - void for_each_speaker(DWORD mask, int channels, F &&visit) { - int channel = 0; - for (int bit = 0; bit < 18 && channel < channels; bit++) { - const DWORD speaker = 1u << bit; - if (mask & speaker) { - visit(channel++, speaker); - } - } - } - } - - void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, - DownmixAlgorithm algorithm) { - this->enabled = true; - this->algorithm = algorithm; - this->bytes_per_sample = game_format->wBitsPerSample / 8; - this->game_frame_size = game_format->nChannels * this->bytes_per_sample; - - this->is_float = is_ieee_float(game_format); - - // supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence - const bool supported = this->is_float - ? this->bytes_per_sample == 4 - : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); - if (!supported) { - log_fatal( - "audio::downmix", - "unsupported sample format ({}-bit {}), downmix will output silence", - game_format->wBitsPerSample, this->is_float ? "float" : "int"); - } - - this->left_mix.clear(); - this->right_mix.clear(); - this->build_layout_mix(game_format); - - make_stereo_format(game_format, stereo_out); - } - - void Downmix::make_stereo_format(const WAVEFORMATEX *game_format, - WAVEFORMATEXTENSIBLE *stereo_out) { - const int bytes_per_sample = game_format->wBitsPerSample / 8; - - memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE)); - stereo_out->Format.nChannels = 2; - stereo_out->Format.nBlockAlign = 2 * bytes_per_sample; - stereo_out->Format.nAvgBytesPerSec = - game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign; - stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; - } - - HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, - REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, - const WAVEFORMATEX *device_format, LPCGUID session_guid) { - - // the smaller stereo buffer can end up unaligned for the device when the game sized the - // duration for its larger multi-channel format; the helper recovers from that. - return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags, - buffer_duration, periodicity, device_format, session_guid); - } - - void Downmix::add_channel(int channel, DWORD speaker, float gain) { - if (speaker & LEFT_SPEAKERS) { - this->left_mix.push_back({ channel, gain }); - } else if (speaker & RIGHT_SPEAKERS) { - this->right_mix.push_back({ channel, gain }); - } else { // center: feed both sides - this->left_mix.push_back({ channel, gain }); - this->right_mix.push_back({ channel, gain }); - } - } - - // AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped - void Downmix::build_ac4_mix(DWORD mask, int channels) { - for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { - if (speaker == SPEAKER_LOW_FREQUENCY) { - return; - } - const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); - this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB); - }); - } - - // keep only the channels in `keep` (front/rear/side), each at unity gain - void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) { - for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { - if (speaker & keep) { - this->add_channel(ch, speaker, 1.0f); - } - }); - } - - // keep every channel (LFE dropped), then average each side so its gains sum to unity - void Downmix::build_normalize_mix(DWORD mask, int channels) { - for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { - if (speaker != SPEAKER_LOW_FREQUENCY) { - this->add_channel(ch, speaker, 1.0f); - } - }); - - for (auto *mix : { &this->left_mix, &this->right_mix }) { - if (!mix->empty()) { - const float gain = 1.0f / mix->size(); - for (auto &c : *mix) { - c.gain = gain; - } - } - } - } - - // fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right) - void Downmix::build_pairs_mix(int channels, float gain) { - for (int ch = 0; ch < channels; ch++) { - (((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain }); - } - } - - void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) { - const int channels = game_format->nChannels; - const DWORD mask = read_channel_mask(game_format); - - // without a mask the layout is unknown: extract/normalize have nothing to act on, so all - // algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB) - if (mask == 0) { - this->build_pairs_mix(channels, - this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f); - return; - } - - switch (this->algorithm) { - case DownmixAlgorithm::FrontOnly: - this->build_extract_mix(mask, channels, - SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); - break; - case DownmixAlgorithm::RearOnly: - this->build_extract_mix(mask, channels, - SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER); - break; - case DownmixAlgorithm::SideOnly: - this->build_extract_mix(mask, channels, - SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT); - break; - case DownmixAlgorithm::Normalize: - this->build_normalize_mix(mask, channels); - break; - case DownmixAlgorithm::AC4: - this->build_ac4_mix(mask, channels); - break; - } - } - - void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const { - const int bps = this->bytes_per_sample; - const int src_stride = this->game_frame_size; - const int dst_stride = 2 * bps; - - if (dst == nullptr || src == nullptr || bps <= 0) { - return; - } - - // sum each speaker's source channels into the matching stereo output - for (UINT32 i = 0; i < frames; i++) { - const BYTE *in = src + (size_t) i * src_stride; - BYTE *out = dst + (size_t) i * dst_stride; - - float left = 0.0f; - float right = 0.0f; - for (const auto &c : this->left_mix) { - left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; - } - for (const auto &c : this->right_mix) { - right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; - } - write_sample(out, bps, this->is_float, left); - write_sample(out + bps, bps, this->is_float, right); - } - } - - HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) { - const size_t needed = (size_t) frames * this->game_frame_size; - if (this->scratch.size() < needed) { - this->scratch.resize(needed); - } - - HRESULT ret = real->GetBuffer(frames, &this->device_buffer); - if (FAILED(ret)) { - this->device_buffer = nullptr; - return ret; - } - - *ppData = this->scratch.data(); - - return S_OK; - } - - HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) { - const size_t needed = (size_t) frames * this->game_frame_size; - if (this->scratch.size() < needed) { - this->scratch.resize(needed); - } - - *ppData = this->scratch.data(); - - return S_OK; - } - - void Downmix::downmix_into(BYTE *dst, UINT32 frames) const { - this->process(dst, this->scratch.data(), frames); - } - - void Downmix::write_device_buffer(UINT32 frames, DWORD flags) { - const int bps = this->bytes_per_sample; - const int dst_stride = 2 * bps; - - if (this->device_buffer == nullptr || frames == 0 || bps <= 0) { - return; - } - - // mute the first few buffers to avoid a pop on stream start - if (this->buffers_to_mute > 0) { - memset(this->device_buffer, 0, (size_t) frames * dst_stride); - this->buffers_to_mute--; - } else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) { - this->process(this->device_buffer, this->scratch.data(), frames); - } - } -} +#include "downmix.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "util/logging.h" + +#include "util.h" + +namespace hooks::audio { + + namespace { + + constexpr float ATT_3DB = 0.70710678f; + + // speakers routed to the left/right output; anything else (center) feeds both sides + constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT + | SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT; + constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT + | SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT; + + // the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats + DWORD read_channel_mask(const WAVEFORMATEX *fmt) { + if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE + && fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { + return reinterpret_cast(fmt)->dwChannelMask; + } + return 0; + } + + // call visit(channel_index, speaker_bit) for each present speaker, in channel order + template + void for_each_speaker(DWORD mask, int channels, F &&visit) { + int channel = 0; + for (int bit = 0; bit < 18 && channel < channels; bit++) { + const DWORD speaker = 1u << bit; + if (mask & speaker) { + visit(channel++, speaker); + } + } + } + } + + void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, + DownmixAlgorithm algorithm) { + this->enabled = true; + this->algorithm = algorithm; + this->bytes_per_sample = game_format->wBitsPerSample / 8; + this->game_frame_size = game_format->nChannels * this->bytes_per_sample; + + this->is_float = is_ieee_float(game_format); + + // supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence + const bool supported = this->is_float + ? this->bytes_per_sample == 4 + : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); + if (!supported) { + log_fatal( + "audio::downmix", + "unsupported sample format ({}-bit {}), downmix will output silence", + game_format->wBitsPerSample, this->is_float ? "float" : "int"); + } + + this->left_mix.clear(); + this->right_mix.clear(); + this->build_layout_mix(game_format); + + make_stereo_format(game_format, stereo_out); + } + + void Downmix::make_stereo_format(const WAVEFORMATEX *game_format, + WAVEFORMATEXTENSIBLE *stereo_out) { + const int bytes_per_sample = game_format->wBitsPerSample / 8; + + memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE)); + stereo_out->Format.nChannels = 2; + stereo_out->Format.nBlockAlign = 2 * bytes_per_sample; + stereo_out->Format.nAvgBytesPerSec = + game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign; + stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; + } + + HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, + REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, + const WAVEFORMATEX *device_format, LPCGUID session_guid) { + + // the smaller stereo buffer can end up unaligned for the device when the game sized the + // duration for its larger multi-channel format; the helper recovers from that. + return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags, + buffer_duration, periodicity, device_format, session_guid); + } + + void Downmix::add_channel(int channel, DWORD speaker, float gain) { + if (speaker & LEFT_SPEAKERS) { + this->left_mix.push_back({ channel, gain }); + } else if (speaker & RIGHT_SPEAKERS) { + this->right_mix.push_back({ channel, gain }); + } else { // center: feed both sides + this->left_mix.push_back({ channel, gain }); + this->right_mix.push_back({ channel, gain }); + } + } + + // AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped + void Downmix::build_ac4_mix(DWORD mask, int channels) { + for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { + if (speaker == SPEAKER_LOW_FREQUENCY) { + return; + } + const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); + this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB); + }); + } + + // keep only the channels in `keep` (front/rear/side), each at unity gain + void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) { + for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { + if (speaker & keep) { + this->add_channel(ch, speaker, 1.0f); + } + }); + } + + // keep every channel (LFE dropped), then average each side so its gains sum to unity + void Downmix::build_normalize_mix(DWORD mask, int channels) { + for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { + if (speaker != SPEAKER_LOW_FREQUENCY) { + this->add_channel(ch, speaker, 1.0f); + } + }); + + for (auto *mix : { &this->left_mix, &this->right_mix }) { + if (!mix->empty()) { + const float gain = 1.0f / mix->size(); + for (auto &c : *mix) { + c.gain = gain; + } + } + } + } + + // fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right) + void Downmix::build_pairs_mix(int channels, float gain) { + for (int ch = 0; ch < channels; ch++) { + (((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain }); + } + } + + void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) { + const int channels = game_format->nChannels; + const DWORD mask = read_channel_mask(game_format); + + // without a mask the layout is unknown: extract/normalize have nothing to act on, so all + // algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB) + if (mask == 0) { + this->build_pairs_mix(channels, + this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f); + return; + } + + switch (this->algorithm) { + case DownmixAlgorithm::FrontOnly: + this->build_extract_mix(mask, channels, + SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); + break; + case DownmixAlgorithm::RearOnly: + this->build_extract_mix(mask, channels, + SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER); + break; + case DownmixAlgorithm::SideOnly: + this->build_extract_mix(mask, channels, + SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT); + break; + case DownmixAlgorithm::Normalize: + this->build_normalize_mix(mask, channels); + break; + case DownmixAlgorithm::AC4: + this->build_ac4_mix(mask, channels); + break; + } + } + + void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const { + const int bps = this->bytes_per_sample; + const int src_stride = this->game_frame_size; + const int dst_stride = 2 * bps; + + if (dst == nullptr || src == nullptr || bps <= 0) { + return; + } + + // sum each speaker's source channels into the matching stereo output + for (UINT32 i = 0; i < frames; i++) { + const BYTE *in = src + (size_t) i * src_stride; + BYTE *out = dst + (size_t) i * dst_stride; + + float left = 0.0f; + float right = 0.0f; + for (const auto &c : this->left_mix) { + left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; + } + for (const auto &c : this->right_mix) { + right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; + } + write_sample(out, bps, this->is_float, left); + write_sample(out + bps, bps, this->is_float, right); + } + } + + HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) { + const size_t needed = (size_t) frames * this->game_frame_size; + if (this->scratch.size() < needed) { + this->scratch.resize(needed); + } + + HRESULT ret = real->GetBuffer(frames, &this->device_buffer); + if (FAILED(ret)) { + this->device_buffer = nullptr; + return ret; + } + + *ppData = this->scratch.data(); + + return S_OK; + } + + HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) { + const size_t needed = (size_t) frames * this->game_frame_size; + if (this->scratch.size() < needed) { + this->scratch.resize(needed); + } + + *ppData = this->scratch.data(); + + return S_OK; + } + + void Downmix::downmix_into(BYTE *dst, UINT32 frames) const { + this->process(dst, this->scratch.data(), frames); + } + + void Downmix::write_device_buffer(UINT32 frames, DWORD flags) { + const int bps = this->bytes_per_sample; + const int dst_stride = 2 * bps; + + if (this->device_buffer == nullptr || frames == 0 || bps <= 0) { + return; + } + + // mute the first few buffers to avoid a pop on stream start + if (this->buffers_to_mute > 0) { + memset(this->device_buffer, 0, (size_t) frames * dst_stride); + this->buffers_to_mute--; + } else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) { + this->process(this->device_buffer, this->scratch.data(), frames); + } + } +} diff --git a/src/spice2x/hooks/audio/backends/wasapi/downmix.h b/src/spice2x/hooks/audio/backends/wasapi/downmix.h index db32fc1..bee91f2 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/downmix.h +++ b/src/spice2x/hooks/audio/backends/wasapi/downmix.h @@ -1,150 +1,150 @@ -#pragma once - -#include -#include - -#include -#include -#include - -#include "hooks/audio/audio.h" - -struct IAudioClient; -struct IAudioRenderClient; - -namespace hooks::audio { - - // Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the - // game keeps writing its native multi-channel audio into a scratch buffer; on release that - // buffer is mixed down into the two front channels. - // - // The mix is derived from the source format's speaker mask according to the selected - // DownmixAlgorithm: - // FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side - // AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right - // pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped - // Normalize - every channel folded in (center to both sides) with each output side averaged - // so its channels are equally loud, LFE dropped - struct Downmix { - - // a source channel routed into one output speaker at the given gain - struct Contribution { - int channel; - float gain; - }; - - // map an option value (front/rear/side/ac4/normalize) to its algorithm. - static std::optional name_to_algorithm(const char *value) { - if (_stricmp(value, "front") == 0) { - return DownmixAlgorithm::FrontOnly; - } else if (_stricmp(value, "rear") == 0) { - return DownmixAlgorithm::RearOnly; - } else if (_stricmp(value, "side") == 0) { - return DownmixAlgorithm::SideOnly; - } else if (_stricmp(value, "ac4") == 0) { - return DownmixAlgorithm::AC4; - } else if (_stricmp(value, "normalize") == 0) { - return DownmixAlgorithm::Normalize; - } - - return std::nullopt; - } - - // human-readable name of an algorithm, for logging. - static const char *algorithm_name(DownmixAlgorithm algorithm) { - switch (algorithm) { - case DownmixAlgorithm::FrontOnly: return "front"; - case DownmixAlgorithm::RearOnly: return "rear"; - case DownmixAlgorithm::SideOnly: return "side"; - case DownmixAlgorithm::AC4: return "ac4"; - case DownmixAlgorithm::Normalize: return "normalize"; - default: return "unknown"; - } - } - - // whether the downmix is active for the current stream - bool enabled = false; - - // algorithm used to fold the multi-channel audio into stereo - DownmixAlgorithm algorithm = DownmixAlgorithm::AC4; - - // size in bytes of one frame of the game's multi-channel format - int game_frame_size = 0; - - // size in bytes of a single sample (per channel) - int bytes_per_sample = 0; - - // whether samples are IEEE floating point rather than integer PCM - bool is_float = false; - - // enable the downmix for the given game format and fill stereo_out with the equivalent - // stereo format to open the real device with. - void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, - DownmixAlgorithm algorithm); - - // build the stereo format equivalent to game_format (same sample rate and bit depth). - static void make_stereo_format(const WAVEFORMATEX *game_format, - WAVEFORMATEXTENSIBLE *stereo_out); - - // initialize the real device with the stereo format. downmixing reduces the channel count, - // shrinking the buffer's byte size, so the duration the game sized for its multi-channel - // format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED - // this performs the standard WASAPI realignment and retries. - HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, - REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, - const WAVEFORMATEX *device_format, LPCGUID session_guid); - - // mix `frames` frames of multi-channel `src` down into stereo `dst`. - void process(BYTE *dst, const BYTE *src, UINT32 frames) const; - - // grab the real stereo device buffer and hand the game the scratch buffer to write into. - HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData); - - // size the scratch and hand it to the game without acquiring a device buffer. used when a - // later stage (the resampler) owns the device interaction. - HRESULT get_scratch(UINT32 frames, BYTE **ppData); - - // downmix the scratch the game wrote into the caller's stereo buffer, without touching the - // device. used to feed the resampler when the two stages are chained. - void downmix_into(BYTE *dst, UINT32 frames) const; - - // mix the scratch buffer into the stereo device buffer held since get_buffer. the caller - // owns releasing the device buffer afterwards (see current_buffer / buffer_released). - void write_device_buffer(UINT32 frames, DWORD flags); - - // the real device buffer currently held, or null. - BYTE *current_buffer() const { return this->device_buffer; } - - // forget the held device buffer once the caller has released it. - void buffer_released() { this->device_buffer = nullptr; } - - private: - - // build the mix from the source speaker layout for the selected algorithm - void build_layout_mix(const WAVEFORMATEX *game_format); - - // per-algorithm builders, each filling left_mix / right_mix from the speaker mask - void build_ac4_mix(DWORD mask, int channels); - void build_extract_mix(DWORD mask, int channels, DWORD keep); - void build_normalize_mix(DWORD mask, int channels); - - // fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain` - void build_pairs_mix(int channels, float gain); - - // append one source channel to the output side(s) matching its speaker, at `gain` - void add_channel(int channel, DWORD speaker, float gain); - - // source channels summed into each output speaker - std::vector left_mix; - std::vector right_mix; - - // buffer the game writes its multi-channel audio into between get/release - std::vector scratch; - - // the real stereo device buffer currently held, or null - BYTE *device_buffer = nullptr; - - // leading buffers to silence to avoid a pop on stream start - int buffers_to_mute = 16; - }; -} +#pragma once + +#include +#include + +#include +#include +#include + +#include "hooks/audio/audio.h" + +struct IAudioClient; +struct IAudioRenderClient; + +namespace hooks::audio { + + // Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the + // game keeps writing its native multi-channel audio into a scratch buffer; on release that + // buffer is mixed down into the two front channels. + // + // The mix is derived from the source format's speaker mask according to the selected + // DownmixAlgorithm: + // FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side + // AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right + // pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped + // Normalize - every channel folded in (center to both sides) with each output side averaged + // so its channels are equally loud, LFE dropped + struct Downmix { + + // a source channel routed into one output speaker at the given gain + struct Contribution { + int channel; + float gain; + }; + + // map an option value (front/rear/side/ac4/normalize) to its algorithm. + static std::optional name_to_algorithm(const char *value) { + if (_stricmp(value, "front") == 0) { + return DownmixAlgorithm::FrontOnly; + } else if (_stricmp(value, "rear") == 0) { + return DownmixAlgorithm::RearOnly; + } else if (_stricmp(value, "side") == 0) { + return DownmixAlgorithm::SideOnly; + } else if (_stricmp(value, "ac4") == 0) { + return DownmixAlgorithm::AC4; + } else if (_stricmp(value, "normalize") == 0) { + return DownmixAlgorithm::Normalize; + } + + return std::nullopt; + } + + // human-readable name of an algorithm, for logging. + static const char *algorithm_name(DownmixAlgorithm algorithm) { + switch (algorithm) { + case DownmixAlgorithm::FrontOnly: return "front"; + case DownmixAlgorithm::RearOnly: return "rear"; + case DownmixAlgorithm::SideOnly: return "side"; + case DownmixAlgorithm::AC4: return "ac4"; + case DownmixAlgorithm::Normalize: return "normalize"; + default: return "unknown"; + } + } + + // whether the downmix is active for the current stream + bool enabled = false; + + // algorithm used to fold the multi-channel audio into stereo + DownmixAlgorithm algorithm = DownmixAlgorithm::AC4; + + // size in bytes of one frame of the game's multi-channel format + int game_frame_size = 0; + + // size in bytes of a single sample (per channel) + int bytes_per_sample = 0; + + // whether samples are IEEE floating point rather than integer PCM + bool is_float = false; + + // enable the downmix for the given game format and fill stereo_out with the equivalent + // stereo format to open the real device with. + void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, + DownmixAlgorithm algorithm); + + // build the stereo format equivalent to game_format (same sample rate and bit depth). + static void make_stereo_format(const WAVEFORMATEX *game_format, + WAVEFORMATEXTENSIBLE *stereo_out); + + // initialize the real device with the stereo format. downmixing reduces the channel count, + // shrinking the buffer's byte size, so the duration the game sized for its multi-channel + // format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED + // this performs the standard WASAPI realignment and retries. + HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, + REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, + const WAVEFORMATEX *device_format, LPCGUID session_guid); + + // mix `frames` frames of multi-channel `src` down into stereo `dst`. + void process(BYTE *dst, const BYTE *src, UINT32 frames) const; + + // grab the real stereo device buffer and hand the game the scratch buffer to write into. + HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData); + + // size the scratch and hand it to the game without acquiring a device buffer. used when a + // later stage (the resampler) owns the device interaction. + HRESULT get_scratch(UINT32 frames, BYTE **ppData); + + // downmix the scratch the game wrote into the caller's stereo buffer, without touching the + // device. used to feed the resampler when the two stages are chained. + void downmix_into(BYTE *dst, UINT32 frames) const; + + // mix the scratch buffer into the stereo device buffer held since get_buffer. the caller + // owns releasing the device buffer afterwards (see current_buffer / buffer_released). + void write_device_buffer(UINT32 frames, DWORD flags); + + // the real device buffer currently held, or null. + BYTE *current_buffer() const { return this->device_buffer; } + + // forget the held device buffer once the caller has released it. + void buffer_released() { this->device_buffer = nullptr; } + + private: + + // build the mix from the source speaker layout for the selected algorithm + void build_layout_mix(const WAVEFORMATEX *game_format); + + // per-algorithm builders, each filling left_mix / right_mix from the speaker mask + void build_ac4_mix(DWORD mask, int channels); + void build_extract_mix(DWORD mask, int channels, DWORD keep); + void build_normalize_mix(DWORD mask, int channels); + + // fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain` + void build_pairs_mix(int channels, float gain); + + // append one source channel to the output side(s) matching its speaker, at `gain` + void add_channel(int channel, DWORD speaker, float gain); + + // source channels summed into each output speaker + std::vector left_mix; + std::vector right_mix; + + // buffer the game writes its multi-channel audio into between get/release + std::vector scratch; + + // the real stereo device buffer currently held, or null + BYTE *device_buffer = nullptr; + + // leading buffers to silence to avoid a pop on stream start + int buffers_to_mute = 16; + }; +} diff --git a/src/spice2x/hooks/audio/backends/wasapi/resample.cpp b/src/spice2x/hooks/audio/backends/wasapi/resample.cpp index c651069..cf8f20c 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/resample.cpp +++ b/src/spice2x/hooks/audio/backends/wasapi/resample.cpp @@ -1,437 +1,437 @@ -#include "resample.h" - -#include -#include -#include -#include -#include - -#include - -#include "util/logging.h" - -#include "util.h" - -namespace hooks::audio { - - namespace { - - constexpr double PI = 3.14159265358979323846; - - // normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in - inline double sinc(double x) { - if (x == 0.0) { - return 1.0; - } - const double px = PI * x; - return std::sin(px) / px; - } - - // Blackman window across the kernel radius; zero at +/- radius - inline double blackman(double x, double radius) { - const double n = (x + radius) / (2.0 * radius); - if (n <= 0.0 || n >= 1.0) { - return 0.0; - } - return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n); - } - } - - std::optional Resampler::resolve(const WAVEFORMATEX *game_format) { - if (game_format == nullptr || !RESAMPLE_RATE.has_value()) { - return std::nullopt; - } - if (game_format->nSamplesPerSec == 0 - || game_format->nSamplesPerSec == RESAMPLE_RATE.value()) { - return std::nullopt; - } - return RESAMPLE_RATE; - } - - void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, - uint32_t target_rate) { - this->enabled = true; - this->channels = game_format->nChannels; - this->bytes_per_sample = game_format->wBitsPerSample / 8; - this->game_frame_size = this->channels * this->bytes_per_sample; - - this->is_float = is_ieee_float(game_format); - - const bool supported = this->is_float - ? this->bytes_per_sample == 4 - : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); - if (!supported) { - log_fatal( - "audio::resample", - "unsupported sample format ({}-bit {}) for -resample", - game_format->wBitsPerSample, this->is_float ? "float" : "int"); - } - - this->src_rate = game_format->nSamplesPerSec; - this->dst_rate = target_rate; - - // anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating - this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate); - this->half_taps = 16; - - // precompute the windowed-sinc kernel now that cutoff is known - this->build_kernel(); - - // prime the queue with half a window of silence so the first outputs have left history - this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f); - this->in_pos = this->half_taps; - - this->make_device_format(game_format, device_out, target_rate); - } - - void Resampler::make_device_format(const WAVEFORMATEX *game_format, - WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) { - const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize; - - memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE)); - memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE))); - - device_out->Format.nSamplesPerSec = target_rate; - device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign; - } - - HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, - DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, - const WAVEFORMATEX *device_format, LPCGUID session_guid) { - - // the resampler bypasses the OS mixer and talks to the device directly, so it only makes - // sense (and only works) for exclusive streams. shared streams are already resampled by - // the Windows audio engine, so refuse loudly rather than silently doing nothing. - if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) { - log_fatal("audio::resample", - "-resample requires WASAPI exclusive mode, but this stream is shared " - "(Windows already resamples shared streams)"); - } - - // record the pacing model. event-driven streams fill the whole device buffer each period - // (produce_exact); timer-driven streams poll padding and write variable partial chunks, so - // they drain the pending output to the device's free space each call (flush_timer). - this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0; - - return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags, - buffer_duration, periodicity, device_format, session_guid); - } - - UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const { - if (this->dst_rate == 0) { - return device_frames; - } - // round down so the game never believes it has more room than the device can hold - return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate); - } - - UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const { - if (this->dst_rate == 0) { - return device_padding; - } - // round up so the reported free space stays conservative - return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate); - } - - HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) { - const size_t needed = (size_t) frames * this->game_frame_size; - if (this->scratch.size() < needed) { - this->scratch.resize(needed); - } - - *ppData = this->scratch.data(); - - return S_OK; - } - - void Resampler::enqueue_input(UINT32 frames, bool silent) { - const int bps = this->bytes_per_sample; - const int ch = this->channels; - const size_t base = this->in_queue.size(); - - this->in_queue.resize(base + (size_t) frames * ch); - - if (silent || bps <= 0 || ch <= 0) { - std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f); - return; - } - - const BYTE *src = this->scratch.data(); - for (UINT32 f = 0; f < frames; f++) { - for (int c = 0; c < ch; c++) { - const size_t s = (size_t) f * ch + c; - this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float); - } - } - } - - void Resampler::build_kernel() { - const int taps = 2 * this->half_taps; - const int phases = this->kernel_phases; - const double cut = this->cutoff; - const double radius = (double) this->half_taps; - - // one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely - this->kernel_table.resize((size_t) (phases + 1) * taps); - - for (int p = 0; p <= phases; p++) { - const double frac = (double) p / (double) phases; - for (int k = 0; k < taps; k++) { - // tap k maps to input offset t = k - (half_taps - 1), matching emit_frame - const double x = frac - (double) (k - (this->half_taps - 1)); - this->kernel_table[(size_t) p * taps + k] = - (float) (cut * sinc(cut * x) * blackman(x, radius)); - } - } - } - - void Resampler::emit_frame() { - const int ch = this->channels; - const int radius = this->half_taps; - const int taps = 2 * radius; - const long avail = (long) (this->in_queue.size() / ch); - const long center = (long) std::floor(this->in_pos); - - // pick the two kernel rows bracketing this fractional position and the blend between them - const double frac = this->in_pos - (double) center; - const double fp = frac * (double) this->kernel_phases; - const int p0 = (int) fp; - const float blend = (float) (fp - (double) p0); - const float *row0 = &this->kernel_table[(size_t) p0 * taps]; - const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps]; - - // base input index for tap 0 (t = -(radius - 1)) - const long base = center - (radius - 1); - - for (int c = 0; c < ch; c++) { - double acc = 0.0; - for (int k = 0; k < taps; k++) { - const long idx = base + k; - if (idx < 0 || idx >= avail) { - continue; - } - const float w = row0[k] + blend * (row1[k] - row0[k]); - acc += (double) this->in_queue[(size_t) idx * ch + c] * w; - } - this->out_float.push_back((float) acc); - } - } - - void Resampler::drop_consumed() { - const int ch = this->channels; - const long drop = (long) std::floor(this->in_pos) - this->half_taps; - if (drop > 0) { - const size_t drop_samples = (size_t) drop * ch; - if (drop_samples <= this->in_queue.size()) { - this->in_queue.erase(this->in_queue.begin(), - this->in_queue.begin() + drop_samples); - this->in_pos -= drop; - } - } - } - - UINT32 Resampler::produce_exact(UINT32 out_frames) { - const int ch = this->channels; - this->out_float.clear(); - if (ch <= 0 || out_frames == 0) { - return 0; - } - this->out_float.reserve((size_t) out_frames * ch); - - // resample ratio. drive it from the buffer size actually advertised to the game rather - // than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game - // frames, so the game only ever delivers that many input frames per device period. - // consuming at the nominal ratio would eat slightly more input than arrives on any device - // where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly - // draining the queue until it underruns to permanent silence. using the advertised integer - // ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3% - // and inaudible, and it collapses to the exact ratio when the division is integer (160 -> - // 147 stays 147/160 = 44100/48000). - const double step = (double) this->frames_device_to_game(this->device_buffer_frames) - / (double) this->device_buffer_frames; - - // input frames the block will touch: from in_pos through the right edge of the sinc kernel - // at the final output sample. if the queue is short of this, the kernel tail reads past the - // end and distorts every buffer, so buffer one extra block of input before the first output - // (emitting silence without consuming) to build a cushion the kernel can always reach into. - const long avail = (long) (this->in_queue.size() / ch); - const long need = (long) std::ceil(this->in_pos + step * (double) out_frames) - + this->half_taps; - - if (this->priming) { - if (avail < need + (long) out_frames) { - this->out_float.assign((size_t) out_frames * ch, 0.0f); - return out_frames; - } - this->priming = false; - } - - for (UINT32 o = 0; o < out_frames; o++) { - this->emit_frame(); - this->in_pos += step; - } - - this->drop_consumed(); - return out_frames; - } - - UINT32 Resampler::produce_variable() { - const int ch = this->channels; - if (ch <= 0) { - return 0; - } - - // input frames consumed per output frame. timer-driven streams write variable partial - // chunks, so produce however many output frames the currently queued input can fully - // support and leave the rest for the next call; this keeps input and output balanced at - // the exact src/dst ratio over time without depending on the device buffer size. - const double step = (double) this->src_rate / (double) this->dst_rate; - const long avail = (long) (this->in_queue.size() / ch); - - // emit only while the sinc kernel's right edge stays within the queued input. the kernel - // reaches from in_pos out to half_taps frames ahead, so stop once that would read past the - // end; the remaining input becomes the next block's lookahead. - UINT32 produced = 0; - while ((long) std::ceil(this->in_pos) + this->half_taps < avail) { - this->emit_frame(); - this->in_pos += step; - produced++; - } - - this->drop_consumed(); - return produced; - } - - void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const { - const int bps = this->bytes_per_sample; - const int ch = this->channels; - const size_t count = (size_t) frames * ch; - - for (size_t i = 0; i < count; i++) { - write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain); - } - } - - HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, - DWORD flags, float boost) { - if (!this->enabled) { - return S_OK; - } - - // cache the device buffer size once - if (this->device_buffer_frames == 0) { - client->GetBufferSize(&this->device_buffer_frames); - } - if (this->device_buffer_frames == 0) { - return S_OK; - } - - const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; - this->enqueue_input(frames, silent); - - // confirm once that conversion actually started producing output - static std::once_flag active_printed; - std::call_once(active_printed, [this]() { - log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})", - this->src_rate, this->dst_rate, this->channels, - this->event_driven ? "event-driven" : "timer-driven"); - }); - - // the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer - // path, so log it once for parity with that path's "volume boost active" line. - if (boost != 1.0f) { - static std::once_flag boost_printed; - std::call_once(boost_printed, [boost]() { - log_info("audio::resample", "volume boost active (resample): gain={}", boost); - }); - } - - return this->event_driven - ? this->flush_event(real, boost) - : this->flush_timer(real, client, boost); - } - - HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) { - - // event-driven exclusive streams must hand the device a full buffer every period and may - // not push partial counts. resample the whole input block into exactly the device buffer - // size. - const UINT32 produced = this->produce_exact(this->device_buffer_frames); - if (produced == 0) { - return S_OK; - } - - BYTE *dev = nullptr; - HRESULT ret = real->GetBuffer(produced, &dev); - if (FAILED(ret) || dev == nullptr) { - return ret; - } - - // mute the first few buffers to avoid a pop on stream start - float gain = boost; - if (this->buffers_to_mute > 0) { - gain = 0.0f; - this->buffers_to_mute--; - } - - this->write_output(dev, produced, gain); - - return real->ReleaseBuffer(produced, 0); - } - - HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) { - - // convert everything currently queued into the pending output FIFO (out_float). timer- - // driven games write variable partial chunks, so produce only what the queued input can - // fully support and keep the remainder for the next call. - this->produce_variable(); - - const int ch = this->channels; - if (ch <= 0) { - return S_OK; - } - - const UINT32 pending = (UINT32) (this->out_float.size() / ch); - if (pending == 0) { - return S_OK; - } - - // push as many frames as the device currently has free, keeping the rest queued for the - // next call. timer-driven games poll padding and write whenever there is room, so matching - // the device's free space here avoids overflowing the ring while staying device-paced. - UINT32 padding = 0; - if (FAILED(client->GetCurrentPadding(&padding))) { - return S_OK; - } - const UINT32 device_free = this->device_buffer_frames > padding - ? this->device_buffer_frames - padding - : 0; - if (device_free == 0) { - return S_OK; - } - - const UINT32 to_write = std::min(pending, device_free); - - BYTE *dev = nullptr; - HRESULT ret = real->GetBuffer(to_write, &dev); - if (FAILED(ret) || dev == nullptr) { - return ret; - } - - // mute the first few buffers to avoid a pop on stream start - float gain = boost; - if (this->buffers_to_mute > 0) { - gain = 0.0f; - this->buffers_to_mute--; - } - - this->write_output(dev, to_write, gain); - ret = real->ReleaseBuffer(to_write, 0); - - // drop the frames just written from the front of the pending FIFO - this->out_float.erase(this->out_float.begin(), - this->out_float.begin() + (size_t) to_write * ch); - - return ret; - } -} +#include "resample.h" + +#include +#include +#include +#include +#include + +#include + +#include "util/logging.h" + +#include "util.h" + +namespace hooks::audio { + + namespace { + + constexpr double PI = 3.14159265358979323846; + + // normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in + inline double sinc(double x) { + if (x == 0.0) { + return 1.0; + } + const double px = PI * x; + return std::sin(px) / px; + } + + // Blackman window across the kernel radius; zero at +/- radius + inline double blackman(double x, double radius) { + const double n = (x + radius) / (2.0 * radius); + if (n <= 0.0 || n >= 1.0) { + return 0.0; + } + return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n); + } + } + + std::optional Resampler::resolve(const WAVEFORMATEX *game_format) { + if (game_format == nullptr || !RESAMPLE_RATE.has_value()) { + return std::nullopt; + } + if (game_format->nSamplesPerSec == 0 + || game_format->nSamplesPerSec == RESAMPLE_RATE.value()) { + return std::nullopt; + } + return RESAMPLE_RATE; + } + + void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, + uint32_t target_rate) { + this->enabled = true; + this->channels = game_format->nChannels; + this->bytes_per_sample = game_format->wBitsPerSample / 8; + this->game_frame_size = this->channels * this->bytes_per_sample; + + this->is_float = is_ieee_float(game_format); + + const bool supported = this->is_float + ? this->bytes_per_sample == 4 + : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); + if (!supported) { + log_fatal( + "audio::resample", + "unsupported sample format ({}-bit {}) for -resample", + game_format->wBitsPerSample, this->is_float ? "float" : "int"); + } + + this->src_rate = game_format->nSamplesPerSec; + this->dst_rate = target_rate; + + // anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating + this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate); + this->half_taps = 16; + + // precompute the windowed-sinc kernel now that cutoff is known + this->build_kernel(); + + // prime the queue with half a window of silence so the first outputs have left history + this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f); + this->in_pos = this->half_taps; + + this->make_device_format(game_format, device_out, target_rate); + } + + void Resampler::make_device_format(const WAVEFORMATEX *game_format, + WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) { + const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize; + + memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE)); + memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE))); + + device_out->Format.nSamplesPerSec = target_rate; + device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign; + } + + HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, + DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, + const WAVEFORMATEX *device_format, LPCGUID session_guid) { + + // the resampler bypasses the OS mixer and talks to the device directly, so it only makes + // sense (and only works) for exclusive streams. shared streams are already resampled by + // the Windows audio engine, so refuse loudly rather than silently doing nothing. + if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) { + log_fatal("audio::resample", + "-resample requires WASAPI exclusive mode, but this stream is shared " + "(Windows already resamples shared streams)"); + } + + // record the pacing model. event-driven streams fill the whole device buffer each period + // (produce_exact); timer-driven streams poll padding and write variable partial chunks, so + // they drain the pending output to the device's free space each call (flush_timer). + this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0; + + return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags, + buffer_duration, periodicity, device_format, session_guid); + } + + UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const { + if (this->dst_rate == 0) { + return device_frames; + } + // round down so the game never believes it has more room than the device can hold + return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate); + } + + UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const { + if (this->dst_rate == 0) { + return device_padding; + } + // round up so the reported free space stays conservative + return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate); + } + + HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) { + const size_t needed = (size_t) frames * this->game_frame_size; + if (this->scratch.size() < needed) { + this->scratch.resize(needed); + } + + *ppData = this->scratch.data(); + + return S_OK; + } + + void Resampler::enqueue_input(UINT32 frames, bool silent) { + const int bps = this->bytes_per_sample; + const int ch = this->channels; + const size_t base = this->in_queue.size(); + + this->in_queue.resize(base + (size_t) frames * ch); + + if (silent || bps <= 0 || ch <= 0) { + std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f); + return; + } + + const BYTE *src = this->scratch.data(); + for (UINT32 f = 0; f < frames; f++) { + for (int c = 0; c < ch; c++) { + const size_t s = (size_t) f * ch + c; + this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float); + } + } + } + + void Resampler::build_kernel() { + const int taps = 2 * this->half_taps; + const int phases = this->kernel_phases; + const double cut = this->cutoff; + const double radius = (double) this->half_taps; + + // one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely + this->kernel_table.resize((size_t) (phases + 1) * taps); + + for (int p = 0; p <= phases; p++) { + const double frac = (double) p / (double) phases; + for (int k = 0; k < taps; k++) { + // tap k maps to input offset t = k - (half_taps - 1), matching emit_frame + const double x = frac - (double) (k - (this->half_taps - 1)); + this->kernel_table[(size_t) p * taps + k] = + (float) (cut * sinc(cut * x) * blackman(x, radius)); + } + } + } + + void Resampler::emit_frame() { + const int ch = this->channels; + const int radius = this->half_taps; + const int taps = 2 * radius; + const long avail = (long) (this->in_queue.size() / ch); + const long center = (long) std::floor(this->in_pos); + + // pick the two kernel rows bracketing this fractional position and the blend between them + const double frac = this->in_pos - (double) center; + const double fp = frac * (double) this->kernel_phases; + const int p0 = (int) fp; + const float blend = (float) (fp - (double) p0); + const float *row0 = &this->kernel_table[(size_t) p0 * taps]; + const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps]; + + // base input index for tap 0 (t = -(radius - 1)) + const long base = center - (radius - 1); + + for (int c = 0; c < ch; c++) { + double acc = 0.0; + for (int k = 0; k < taps; k++) { + const long idx = base + k; + if (idx < 0 || idx >= avail) { + continue; + } + const float w = row0[k] + blend * (row1[k] - row0[k]); + acc += (double) this->in_queue[(size_t) idx * ch + c] * w; + } + this->out_float.push_back((float) acc); + } + } + + void Resampler::drop_consumed() { + const int ch = this->channels; + const long drop = (long) std::floor(this->in_pos) - this->half_taps; + if (drop > 0) { + const size_t drop_samples = (size_t) drop * ch; + if (drop_samples <= this->in_queue.size()) { + this->in_queue.erase(this->in_queue.begin(), + this->in_queue.begin() + drop_samples); + this->in_pos -= drop; + } + } + } + + UINT32 Resampler::produce_exact(UINT32 out_frames) { + const int ch = this->channels; + this->out_float.clear(); + if (ch <= 0 || out_frames == 0) { + return 0; + } + this->out_float.reserve((size_t) out_frames * ch); + + // resample ratio. drive it from the buffer size actually advertised to the game rather + // than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game + // frames, so the game only ever delivers that many input frames per device period. + // consuming at the nominal ratio would eat slightly more input than arrives on any device + // where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly + // draining the queue until it underruns to permanent silence. using the advertised integer + // ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3% + // and inaudible, and it collapses to the exact ratio when the division is integer (160 -> + // 147 stays 147/160 = 44100/48000). + const double step = (double) this->frames_device_to_game(this->device_buffer_frames) + / (double) this->device_buffer_frames; + + // input frames the block will touch: from in_pos through the right edge of the sinc kernel + // at the final output sample. if the queue is short of this, the kernel tail reads past the + // end and distorts every buffer, so buffer one extra block of input before the first output + // (emitting silence without consuming) to build a cushion the kernel can always reach into. + const long avail = (long) (this->in_queue.size() / ch); + const long need = (long) std::ceil(this->in_pos + step * (double) out_frames) + + this->half_taps; + + if (this->priming) { + if (avail < need + (long) out_frames) { + this->out_float.assign((size_t) out_frames * ch, 0.0f); + return out_frames; + } + this->priming = false; + } + + for (UINT32 o = 0; o < out_frames; o++) { + this->emit_frame(); + this->in_pos += step; + } + + this->drop_consumed(); + return out_frames; + } + + UINT32 Resampler::produce_variable() { + const int ch = this->channels; + if (ch <= 0) { + return 0; + } + + // input frames consumed per output frame. timer-driven streams write variable partial + // chunks, so produce however many output frames the currently queued input can fully + // support and leave the rest for the next call; this keeps input and output balanced at + // the exact src/dst ratio over time without depending on the device buffer size. + const double step = (double) this->src_rate / (double) this->dst_rate; + const long avail = (long) (this->in_queue.size() / ch); + + // emit only while the sinc kernel's right edge stays within the queued input. the kernel + // reaches from in_pos out to half_taps frames ahead, so stop once that would read past the + // end; the remaining input becomes the next block's lookahead. + UINT32 produced = 0; + while ((long) std::ceil(this->in_pos) + this->half_taps < avail) { + this->emit_frame(); + this->in_pos += step; + produced++; + } + + this->drop_consumed(); + return produced; + } + + void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const { + const int bps = this->bytes_per_sample; + const int ch = this->channels; + const size_t count = (size_t) frames * ch; + + for (size_t i = 0; i < count; i++) { + write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain); + } + } + + HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, + DWORD flags, float boost) { + if (!this->enabled) { + return S_OK; + } + + // cache the device buffer size once + if (this->device_buffer_frames == 0) { + client->GetBufferSize(&this->device_buffer_frames); + } + if (this->device_buffer_frames == 0) { + return S_OK; + } + + const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; + this->enqueue_input(frames, silent); + + // confirm once that conversion actually started producing output + static std::once_flag active_printed; + std::call_once(active_printed, [this]() { + log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})", + this->src_rate, this->dst_rate, this->channels, + this->event_driven ? "event-driven" : "timer-driven"); + }); + + // the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer + // path, so log it once for parity with that path's "volume boost active" line. + if (boost != 1.0f) { + static std::once_flag boost_printed; + std::call_once(boost_printed, [boost]() { + log_info("audio::resample", "volume boost active (resample): gain={}", boost); + }); + } + + return this->event_driven + ? this->flush_event(real, boost) + : this->flush_timer(real, client, boost); + } + + HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) { + + // event-driven exclusive streams must hand the device a full buffer every period and may + // not push partial counts. resample the whole input block into exactly the device buffer + // size. + const UINT32 produced = this->produce_exact(this->device_buffer_frames); + if (produced == 0) { + return S_OK; + } + + BYTE *dev = nullptr; + HRESULT ret = real->GetBuffer(produced, &dev); + if (FAILED(ret) || dev == nullptr) { + return ret; + } + + // mute the first few buffers to avoid a pop on stream start + float gain = boost; + if (this->buffers_to_mute > 0) { + gain = 0.0f; + this->buffers_to_mute--; + } + + this->write_output(dev, produced, gain); + + return real->ReleaseBuffer(produced, 0); + } + + HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) { + + // convert everything currently queued into the pending output FIFO (out_float). timer- + // driven games write variable partial chunks, so produce only what the queued input can + // fully support and keep the remainder for the next call. + this->produce_variable(); + + const int ch = this->channels; + if (ch <= 0) { + return S_OK; + } + + const UINT32 pending = (UINT32) (this->out_float.size() / ch); + if (pending == 0) { + return S_OK; + } + + // push as many frames as the device currently has free, keeping the rest queued for the + // next call. timer-driven games poll padding and write whenever there is room, so matching + // the device's free space here avoids overflowing the ring while staying device-paced. + UINT32 padding = 0; + if (FAILED(client->GetCurrentPadding(&padding))) { + return S_OK; + } + const UINT32 device_free = this->device_buffer_frames > padding + ? this->device_buffer_frames - padding + : 0; + if (device_free == 0) { + return S_OK; + } + + const UINT32 to_write = std::min(pending, device_free); + + BYTE *dev = nullptr; + HRESULT ret = real->GetBuffer(to_write, &dev); + if (FAILED(ret) || dev == nullptr) { + return ret; + } + + // mute the first few buffers to avoid a pop on stream start + float gain = boost; + if (this->buffers_to_mute > 0) { + gain = 0.0f; + this->buffers_to_mute--; + } + + this->write_output(dev, to_write, gain); + ret = real->ReleaseBuffer(to_write, 0); + + // drop the frames just written from the front of the pending FIFO + this->out_float.erase(this->out_float.begin(), + this->out_float.begin() + (size_t) to_write * ch); + + return ret; + } +} diff --git a/src/spice2x/hooks/audio/backends/wasapi/resample.h b/src/spice2x/hooks/audio/backends/wasapi/resample.h index 314ae26..32bb2d7 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/resample.h +++ b/src/spice2x/hooks/audio/backends/wasapi/resample.h @@ -1,149 +1,149 @@ -#pragma once - -#include -#include -#include - -#include -#include -#include - -#include "hooks/audio/audio.h" - -struct IAudioClient; -struct IAudioRenderClient; - -namespace hooks::audio { - - // Streaming sample-rate converter for the WASAPI render path. The real device is opened at the - // target rate while the game keeps writing its native-rate audio into a scratch buffer; on - // release that buffer is converted with a windowed-sinc kernel and pushed to the device. - // Channel count and sample format are preserved; only the sample rate changes. - // - // Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful: - // a fractional read position and a window of input history carry across ReleaseBuffer calls, - // and the device buffer is only filled up to the space the device currently has free. - struct Resampler { - - // whether the resampler is active for the current stream - bool enabled = false; - - // whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven - // streams instead poll padding and write variable partial chunks, so they drain the - // pending output to the device's free space rather than pushing a full buffer per period. - bool event_driven = true; - - // decide whether the stream should be resampled and to which rate. returns the target rate - // when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt. - static std::optional resolve(const WAVEFORMATEX *game_format); - - // enable resampling for game_format and fill device_out with the equivalent format at the - // target rate to open the real device with. - void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, - uint32_t target_rate); - - // build the device format equivalent to game_format at target_rate (same channels/depth). - static void make_device_format(const WAVEFORMATEX *game_format, - WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate); - - // initialize the real device at the target rate, performing the standard WASAPI buffer - // realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED. - HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, - REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, - const WAVEFORMATEX *device_format, LPCGUID session_guid); - - // translate a device-rate frame count to the equivalent game-rate count, so the buffer-size - // and padding values reported to the game stay paced at the game's native rate. - UINT32 frames_device_to_game(UINT32 device_frames) const; - UINT32 padding_device_to_game(UINT32 device_padding) const; - - // hand the game a scratch buffer sized for `frames` of its native format to write into. - HRESULT get_buffer(UINT32 frames, BYTE **ppData); - - // pointer to the input scratch (sized by get_buffer). when chained after the downmix, the - // downmix writes its stereo output here for the resampler to consume on the next flush. - BYTE *input_data() { return this->scratch.data(); } - - // convert the `frames` the game wrote and push output to the real render client. `boost` - // is applied to the converted output. event-driven streams fill exactly one device buffer - // per period; timer-driven streams push as many converted frames as the device has free. - HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags, - float boost); - - private: - - // append `frames` of the scratch buffer (native format), or silence, to the input queue - void enqueue_input(UINT32 frames, bool silent); - - // event-driven path: produce exactly one full device buffer and push it. - HRESULT flush_event(IAudioRenderClient *real, float boost); - - // timer-driven path: convert all queued input into the pending output FIFO, then push as - // many frames as the device currently has free, keeping the remainder for the next call. - HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost); - - // produce exactly out_frames output frames using the fixed src/dst ratio. event-driven - // exclusive streams must fill the whole device buffer every period; a small input cushion - // is buffered first (see priming) so the sinc kernel always has lookahead. - UINT32 produce_exact(UINT32 out_frames); - - // convert all input the kernel can fully support into the pending output FIFO (out_float), - // appending without clearing. returns the number of frames produced. used by the - // timer-driven path where output is drained to the device in device-paced chunks. - UINT32 produce_variable(); - - // convolve the windowed-sinc kernel at the current in_pos and append the resulting frame - // (one sample per channel) to out_float - void emit_frame(); - - // precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so - // emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too - // expensive to run per sample on the audio callback thread and causes underrun crackle). - void build_kernel(); - - // drop input frames that in_pos has advanced past, keeping a window of history for the - // next block's left context - void drop_consumed(); - - // convert the first `frames` of out_float to the device format, scaled by `gain` - void write_output(BYTE *dst, UINT32 frames, float gain) const; - - // sample format of the stream - int channels = 0; - int bytes_per_sample = 0; - bool is_float = false; - int game_frame_size = 0; - - uint32_t src_rate = 0; - uint32_t dst_rate = 0; - - // sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius - double cutoff = 1.0; - int half_taps = 16; - - // precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the - // fractional sample position (linearly interpolated between adjacent rows in emit_frame) - std::vector kernel_table; - int kernel_phases = 1024; - - // interleaved float input queue and the fractional read position within it (in frames) - std::vector in_queue; - double in_pos = 0.0; - - // emit silence until a full block of input lookahead has accumulated, so the sinc kernel - // never reads past the end of the queue (which would distort the tail of every buffer) - bool priming = true; - - // interleaved float scratch for produced output - std::vector out_float; - - // buffer the game writes its native-rate audio into between get_buffer / flush - std::vector scratch; - - // cached device buffer size (frames); a full buffer is produced every period - UINT32 device_buffer_frames = 0; - - // leading buffers to silence to avoid a pop on stream start - int buffers_to_mute = 16; - }; -} +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "hooks/audio/audio.h" + +struct IAudioClient; +struct IAudioRenderClient; + +namespace hooks::audio { + + // Streaming sample-rate converter for the WASAPI render path. The real device is opened at the + // target rate while the game keeps writing its native-rate audio into a scratch buffer; on + // release that buffer is converted with a windowed-sinc kernel and pushed to the device. + // Channel count and sample format are preserved; only the sample rate changes. + // + // Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful: + // a fractional read position and a window of input history carry across ReleaseBuffer calls, + // and the device buffer is only filled up to the space the device currently has free. + struct Resampler { + + // whether the resampler is active for the current stream + bool enabled = false; + + // whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven + // streams instead poll padding and write variable partial chunks, so they drain the + // pending output to the device's free space rather than pushing a full buffer per period. + bool event_driven = true; + + // decide whether the stream should be resampled and to which rate. returns the target rate + // when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt. + static std::optional resolve(const WAVEFORMATEX *game_format); + + // enable resampling for game_format and fill device_out with the equivalent format at the + // target rate to open the real device with. + void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, + uint32_t target_rate); + + // build the device format equivalent to game_format at target_rate (same channels/depth). + static void make_device_format(const WAVEFORMATEX *game_format, + WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate); + + // initialize the real device at the target rate, performing the standard WASAPI buffer + // realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED. + HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, + REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, + const WAVEFORMATEX *device_format, LPCGUID session_guid); + + // translate a device-rate frame count to the equivalent game-rate count, so the buffer-size + // and padding values reported to the game stay paced at the game's native rate. + UINT32 frames_device_to_game(UINT32 device_frames) const; + UINT32 padding_device_to_game(UINT32 device_padding) const; + + // hand the game a scratch buffer sized for `frames` of its native format to write into. + HRESULT get_buffer(UINT32 frames, BYTE **ppData); + + // pointer to the input scratch (sized by get_buffer). when chained after the downmix, the + // downmix writes its stereo output here for the resampler to consume on the next flush. + BYTE *input_data() { return this->scratch.data(); } + + // convert the `frames` the game wrote and push output to the real render client. `boost` + // is applied to the converted output. event-driven streams fill exactly one device buffer + // per period; timer-driven streams push as many converted frames as the device has free. + HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags, + float boost); + + private: + + // append `frames` of the scratch buffer (native format), or silence, to the input queue + void enqueue_input(UINT32 frames, bool silent); + + // event-driven path: produce exactly one full device buffer and push it. + HRESULT flush_event(IAudioRenderClient *real, float boost); + + // timer-driven path: convert all queued input into the pending output FIFO, then push as + // many frames as the device currently has free, keeping the remainder for the next call. + HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost); + + // produce exactly out_frames output frames using the fixed src/dst ratio. event-driven + // exclusive streams must fill the whole device buffer every period; a small input cushion + // is buffered first (see priming) so the sinc kernel always has lookahead. + UINT32 produce_exact(UINT32 out_frames); + + // convert all input the kernel can fully support into the pending output FIFO (out_float), + // appending without clearing. returns the number of frames produced. used by the + // timer-driven path where output is drained to the device in device-paced chunks. + UINT32 produce_variable(); + + // convolve the windowed-sinc kernel at the current in_pos and append the resulting frame + // (one sample per channel) to out_float + void emit_frame(); + + // precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so + // emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too + // expensive to run per sample on the audio callback thread and causes underrun crackle). + void build_kernel(); + + // drop input frames that in_pos has advanced past, keeping a window of history for the + // next block's left context + void drop_consumed(); + + // convert the first `frames` of out_float to the device format, scaled by `gain` + void write_output(BYTE *dst, UINT32 frames, float gain) const; + + // sample format of the stream + int channels = 0; + int bytes_per_sample = 0; + bool is_float = false; + int game_frame_size = 0; + + uint32_t src_rate = 0; + uint32_t dst_rate = 0; + + // sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius + double cutoff = 1.0; + int half_taps = 16; + + // precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the + // fractional sample position (linearly interpolated between adjacent rows in emit_frame) + std::vector kernel_table; + int kernel_phases = 1024; + + // interleaved float input queue and the fractional read position within it (in frames) + std::vector in_queue; + double in_pos = 0.0; + + // emit silence until a full block of input lookahead has accumulated, so the sinc kernel + // never reads past the end of the queue (which would distort the tail of every buffer) + bool priming = true; + + // interleaved float scratch for produced output + std::vector out_float; + + // buffer the game writes its native-rate audio into between get_buffer / flush + std::vector scratch; + + // cached device buffer size (frames); a full buffer is produced every period + UINT32 device_buffer_frames = 0; + + // leading buffers to silence to avoid a pop on stream start + int buffers_to_mute = 16; + }; +} diff --git a/src/spice2x/hooks/audio/backends/wasapi/shared.cpp b/src/spice2x/hooks/audio/backends/wasapi/shared.cpp index 17a0a4b..a688cd4 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/shared.cpp +++ b/src/spice2x/hooks/audio/backends/wasapi/shared.cpp @@ -1,187 +1,187 @@ -#include "shared.h" - -#include - -#include - -#include "hooks/audio/audio.h" -#include "util/logging.h" - -#include "util.h" -#include "defs.h" - -namespace hooks::audio { - - // whether the engine's PCM converter can handle this format. PCM / float only; non-PCM - // bitstream (AC-3 / DTS passthrough) must be left alone. - static bool is_pcm_or_float(const WAVEFORMATEX *format) { - if (format == nullptr) { - return false; - } - - switch (format->wFormatTag) { - case WAVE_FORMAT_PCM: - case WAVE_FORMAT_IEEE_FLOAT: - return true; - case WAVE_FORMAT_EXTENSIBLE: { - - // SubFormat is only valid when the extra-bytes block is large enough - if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { - return false; - } - const auto *ext = reinterpret_cast(format); - return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM - || ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; - } - default: - return false; - } - } - - bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) { - - // only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can - // handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode, - // so leave it in exclusive untouched. - return hooks::audio::WASAPI_COMPATIBILITY_MODE - && share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE - && is_pcm_or_float(format); - } - - void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, - REFERENCE_TIME *periodicity) { - - // shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's - // native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT). - log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode"); - *share_mode = AUDCLNT_SHAREMODE_SHARED; - *periodicity = 0; - *stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; - this->redirected_from_exclusive = true; - } - - UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, - UINT32 device_frames) { - if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) { - this->reported_frames = device_frames; - return device_frames; - } - - // GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so - // period_frames = period * sample_rate / 10^7. - REFERENCE_TIME period = 0; - if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) { - const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000); - if (period_frames > 0 && period_frames < device_frames) { - this->reported_frames = period_frames; - return period_frames; - } - } - - this->reported_frames = device_frames; - return device_frames; - } - - void SharedRedirect::enable_bridge(int frame_bytes) { - if (!this->redirected_from_exclusive || frame_bytes <= 0) { - return; - } - - this->frame_bytes = frame_bytes; - this->device_buffer_frames = 0; - this->fifo.clear(); - - log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)", - frame_bytes); - } - - BYTE *SharedRedirect::begin_write(UINT32 frames) { - // reserve space at the FIFO tail and let the game write straight into it - no scratch copy. - this->pending_write_offset = this->fifo.size(); - this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes); - - return this->fifo.data() + this->pending_write_offset; - } - - void SharedRedirect::commit_write(UINT32 frames, bool silent) { - // trim the tail reservation to the frames actually written; zero it in place if silent. - const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes; - if (silent) { - std::fill(this->fifo.begin() + this->pending_write_offset, - this->fifo.begin() + end, (BYTE) 0); - } - this->fifo.resize(end); - } - - UINT32 SharedRedirect::pending_frames() const { - if (this->frame_bytes <= 0) { - return 0; - } - - return (UINT32) (this->fifo.size() / this->frame_bytes); - } - - UINT32 SharedRedirect::virtual_padding() const { - const UINT32 pending = this->pending_frames(); - return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending; - } - - HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client, - const WAVEFORMATEXTENSIBLE &device_format, float boost) { - if (!this->bridge_enabled()) { - return S_OK; - } - - // cache the real device buffer size once; it is fixed for the life of the stream. - if (this->device_buffer_frames == 0) { - if (FAILED(client->GetBufferSize(&this->device_buffer_frames)) - || this->device_buffer_frames == 0) { - return S_OK; - } - } - - const UINT32 pending = this->pending_frames(); - if (pending == 0) { - return S_OK; - } - - // push only as many frames as the device currently has free, keeping the rest queued. this - // self-paces to the engine's real consumption so a full-buffer write never overflows. - UINT32 padding = 0; - if (FAILED(client->GetCurrentPadding(&padding))) { - return S_OK; - } - const UINT32 device_free = this->device_buffer_frames > padding - ? this->device_buffer_frames - padding - : 0; - if (device_free == 0) { - return S_OK; - } - - const UINT32 to_write = std::min(pending, device_free); - - BYTE *dev = nullptr; - HRESULT ret = real->GetBuffer(to_write, &dev); - if (FAILED(ret) || dev == nullptr) { - return ret; - } - - const size_t bytes = (size_t) to_write * this->frame_bytes; - std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev); - - // mute the first few buffers to avoid a startup pop, then apply the volume boost. - if (this->buffers_to_mute > 0) { - std::fill(dev, dev + bytes, (BYTE) 0); - this->buffers_to_mute--; - } else if (boost != 1.0f) { - apply_gain(dev, to_write, device_format, boost); - } - - ret = real->ReleaseBuffer(to_write, 0); - - // drop the frames just handed to the device from the front of the FIFO. - this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes); - - return ret; - } -} +#include "shared.h" + +#include + +#include + +#include "hooks/audio/audio.h" +#include "util/logging.h" + +#include "util.h" +#include "defs.h" + +namespace hooks::audio { + + // whether the engine's PCM converter can handle this format. PCM / float only; non-PCM + // bitstream (AC-3 / DTS passthrough) must be left alone. + static bool is_pcm_or_float(const WAVEFORMATEX *format) { + if (format == nullptr) { + return false; + } + + switch (format->wFormatTag) { + case WAVE_FORMAT_PCM: + case WAVE_FORMAT_IEEE_FLOAT: + return true; + case WAVE_FORMAT_EXTENSIBLE: { + + // SubFormat is only valid when the extra-bytes block is large enough + if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { + return false; + } + const auto *ext = reinterpret_cast(format); + return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM + || ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + } + default: + return false; + } + } + + bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) { + + // only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can + // handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode, + // so leave it in exclusive untouched. + return hooks::audio::WASAPI_COMPATIBILITY_MODE + && share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE + && is_pcm_or_float(format); + } + + void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, + REFERENCE_TIME *periodicity) { + + // shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's + // native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT). + log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode"); + *share_mode = AUDCLNT_SHAREMODE_SHARED; + *periodicity = 0; + *stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + this->redirected_from_exclusive = true; + } + + UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, + UINT32 device_frames) { + if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) { + this->reported_frames = device_frames; + return device_frames; + } + + // GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so + // period_frames = period * sample_rate / 10^7. + REFERENCE_TIME period = 0; + if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) { + const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000); + if (period_frames > 0 && period_frames < device_frames) { + this->reported_frames = period_frames; + return period_frames; + } + } + + this->reported_frames = device_frames; + return device_frames; + } + + void SharedRedirect::enable_bridge(int frame_bytes) { + if (!this->redirected_from_exclusive || frame_bytes <= 0) { + return; + } + + this->frame_bytes = frame_bytes; + this->device_buffer_frames = 0; + this->fifo.clear(); + + log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)", + frame_bytes); + } + + BYTE *SharedRedirect::begin_write(UINT32 frames) { + // reserve space at the FIFO tail and let the game write straight into it - no scratch copy. + this->pending_write_offset = this->fifo.size(); + this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes); + + return this->fifo.data() + this->pending_write_offset; + } + + void SharedRedirect::commit_write(UINT32 frames, bool silent) { + // trim the tail reservation to the frames actually written; zero it in place if silent. + const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes; + if (silent) { + std::fill(this->fifo.begin() + this->pending_write_offset, + this->fifo.begin() + end, (BYTE) 0); + } + this->fifo.resize(end); + } + + UINT32 SharedRedirect::pending_frames() const { + if (this->frame_bytes <= 0) { + return 0; + } + + return (UINT32) (this->fifo.size() / this->frame_bytes); + } + + UINT32 SharedRedirect::virtual_padding() const { + const UINT32 pending = this->pending_frames(); + return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending; + } + + HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client, + const WAVEFORMATEXTENSIBLE &device_format, float boost) { + if (!this->bridge_enabled()) { + return S_OK; + } + + // cache the real device buffer size once; it is fixed for the life of the stream. + if (this->device_buffer_frames == 0) { + if (FAILED(client->GetBufferSize(&this->device_buffer_frames)) + || this->device_buffer_frames == 0) { + return S_OK; + } + } + + const UINT32 pending = this->pending_frames(); + if (pending == 0) { + return S_OK; + } + + // push only as many frames as the device currently has free, keeping the rest queued. this + // self-paces to the engine's real consumption so a full-buffer write never overflows. + UINT32 padding = 0; + if (FAILED(client->GetCurrentPadding(&padding))) { + return S_OK; + } + const UINT32 device_free = this->device_buffer_frames > padding + ? this->device_buffer_frames - padding + : 0; + if (device_free == 0) { + return S_OK; + } + + const UINT32 to_write = std::min(pending, device_free); + + BYTE *dev = nullptr; + HRESULT ret = real->GetBuffer(to_write, &dev); + if (FAILED(ret) || dev == nullptr) { + return ret; + } + + const size_t bytes = (size_t) to_write * this->frame_bytes; + std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev); + + // mute the first few buffers to avoid a startup pop, then apply the volume boost. + if (this->buffers_to_mute > 0) { + std::fill(dev, dev + bytes, (BYTE) 0); + this->buffers_to_mute--; + } else if (boost != 1.0f) { + apply_gain(dev, to_write, device_format, boost); + } + + ret = real->ReleaseBuffer(to_write, 0); + + // drop the frames just handed to the device from the front of the FIFO. + this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes); + + return ret; + } +} diff --git a/src/spice2x/hooks/audio/backends/wasapi/shared.h b/src/spice2x/hooks/audio/backends/wasapi/shared.h index 6fcc557..27d678f 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/shared.h +++ b/src/spice2x/hooks/audio/backends/wasapi/shared.h @@ -1,83 +1,83 @@ -#pragma once - -#include -#include - -#include -#include -#include - -struct IAudioRenderClient; - -namespace hooks::audio { - - // The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps - // can play sound and devices that can't open the exclusive format still work, at the cost of - // some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone. - struct SharedRedirect { - - // true once apply() has redirected an exclusive request. gates the buffer clamp; stays false - // for a natively-shared stream (it paces itself, so must not be clamped). - bool redirected_from_exclusive = false; - - // whether an exclusive-mode request should be redirected, given the -wasapishared option. - // only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode. - static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format); - - // redirect an exclusive request to shared mode. caller must have checked wants() first. - void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity); - - // clamp a reported buffer size to one device period. the FIFO bridge below is what prevents - // the overflow; this just keeps the game's per-event writes small so the bridge adds minimal - // latency. caches the chosen value for virtual_padding. a no-op unless redirected. - UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames); - - // FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own - // callback, not the shared engine clock, so a full-buffer write can intermittently exceed the - // double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes - // directly into a FIFO that is drained to the device only as fast as it frees space - the - // same free-space-clamped approach the timer-driven resampler uses. - - // arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size - // in the game's (== device, via AUTOCONVERTPCM) format. - void enable_bridge(int frame_bytes); - - // whether the FIFO bridge is active (a redirect was applied and armed). - bool bridge_enabled() const { return this->frame_bytes > 0; } - - // reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place. - // must be paired with commit_write, which trims the reservation to the frames written. - BYTE *begin_write(UINT32 frames); - - // trim the reservation from begin_write to the `frames` actually written (zeroing if silent). - void commit_write(UINT32 frames, bool silent); - - // padding to report to a game that polls GetCurrentPadding while the bridge is active: the - // FIFO fill level, capped to the reported buffer size so the game's free-space calculation - // (reported_buffer - padding) reflects room in the virtual buffer rather than the device's. - UINT32 virtual_padding() const; - - // push as many queued frames as the real device has free, applying `boost`, keeping the rest - // for the next call. `real` is the wrapped render client's underlying interface; `client` is - // the underlying audio client used to query the device's free space. - HRESULT drain(IAudioRenderClient *real, IAudioClient *client, - const WAVEFORMATEXTENSIBLE &device_format, float boost); - - private: - - // frames currently queued in the FIFO and not yet handed to the device. - UINT32 pending_frames() const; - - // FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the - // game's interleaved frame format; the game writes new frames directly into its tail between - // begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see - // bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write. - int frame_bytes = 0; - UINT32 device_buffer_frames = 0; - UINT32 reported_frames = 0; - int buffers_to_mute = 4; - size_t pending_write_offset = 0; - std::vector fifo; - }; -} - +#pragma once + +#include +#include + +#include +#include +#include + +struct IAudioRenderClient; + +namespace hooks::audio { + + // The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps + // can play sound and devices that can't open the exclusive format still work, at the cost of + // some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone. + struct SharedRedirect { + + // true once apply() has redirected an exclusive request. gates the buffer clamp; stays false + // for a natively-shared stream (it paces itself, so must not be clamped). + bool redirected_from_exclusive = false; + + // whether an exclusive-mode request should be redirected, given the -wasapishared option. + // only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode. + static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format); + + // redirect an exclusive request to shared mode. caller must have checked wants() first. + void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity); + + // clamp a reported buffer size to one device period. the FIFO bridge below is what prevents + // the overflow; this just keeps the game's per-event writes small so the bridge adds minimal + // latency. caches the chosen value for virtual_padding. a no-op unless redirected. + UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames); + + // FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own + // callback, not the shared engine clock, so a full-buffer write can intermittently exceed the + // double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes + // directly into a FIFO that is drained to the device only as fast as it frees space - the + // same free-space-clamped approach the timer-driven resampler uses. + + // arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size + // in the game's (== device, via AUTOCONVERTPCM) format. + void enable_bridge(int frame_bytes); + + // whether the FIFO bridge is active (a redirect was applied and armed). + bool bridge_enabled() const { return this->frame_bytes > 0; } + + // reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place. + // must be paired with commit_write, which trims the reservation to the frames written. + BYTE *begin_write(UINT32 frames); + + // trim the reservation from begin_write to the `frames` actually written (zeroing if silent). + void commit_write(UINT32 frames, bool silent); + + // padding to report to a game that polls GetCurrentPadding while the bridge is active: the + // FIFO fill level, capped to the reported buffer size so the game's free-space calculation + // (reported_buffer - padding) reflects room in the virtual buffer rather than the device's. + UINT32 virtual_padding() const; + + // push as many queued frames as the real device has free, applying `boost`, keeping the rest + // for the next call. `real` is the wrapped render client's underlying interface; `client` is + // the underlying audio client used to query the device's free space. + HRESULT drain(IAudioRenderClient *real, IAudioClient *client, + const WAVEFORMATEXTENSIBLE &device_format, float boost); + + private: + + // frames currently queued in the FIFO and not yet handed to the device. + UINT32 pending_frames() const; + + // FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the + // game's interleaved frame format; the game writes new frames directly into its tail between + // begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see + // bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write. + int frame_bytes = 0; + UINT32 device_buffer_frames = 0; + UINT32 reported_frames = 0; + int buffers_to_mute = 4; + size_t pending_write_offset = 0; + std::vector fifo; + }; +} + diff --git a/src/spice2x/hooks/audio/xact.cpp b/src/spice2x/hooks/audio/xact.cpp index 0fc40c0..5630233 100644 --- a/src/spice2x/hooks/audio/xact.cpp +++ b/src/spice2x/hooks/audio/xact.cpp @@ -1,393 +1,393 @@ -#include "xact.h" - -#include -#include - -#include -#include -#include -#include - -#include "util/deferlog.h" -#include "util/detour.h" -#include "util/logging.h" -#include "util/utils.h" - -namespace hooks::audio::xact { - - // XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2 - // layout, so keep this proxy pinned to the legacy ABI used by libxact. - struct XAudio2DeviceDetails { - WCHAR device_id[256]; - WCHAR display_name[256]; - DWORD role; - WAVEFORMATEXTENSIBLE output_format; - }; - - struct XAudio2EffectChain { - UINT32 effect_count; - const void *effect_descriptors; - }; - - struct IXAudio2_27 { - virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0; - virtual ULONG STDMETHODCALLTYPE AddRef() = 0; - virtual ULONG STDMETHODCALLTYPE Release() = 0; - virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0; - virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails( - UINT32 device_index, - XAudio2DeviceDetails *device_details) = 0; - virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0; - virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0; - virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0; - virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice( - void **source_voice, - const WAVEFORMATEX *source_format, - UINT32 flags, - float max_frequency_ratio, - void *callback, - const void *send_list, - const XAudio2EffectChain *effect_chain) = 0; - virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice( - void **submix_voice, - UINT32 input_channels, - UINT32 input_sample_rate, - UINT32 flags, - UINT32 processing_stage, - const void *send_list, - const XAudio2EffectChain *effect_chain) = 0; - virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice( - void **mastering_voice, - UINT32 input_channels, - UINT32 input_sample_rate, - UINT32 flags, - UINT32 device_index, - const XAudio2EffectChain *effect_chain) = 0; - virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0; - virtual void STDMETHODCALLTYPE StopEngine() = 0; - virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0; - virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0; - virtual void STDMETHODCALLTYPE SetDebugConfiguration( - const void *debug_configuration, - void *reserved) = 0; - }; - - // XAudio2 2.7 COM class and interface. - DEFINE_GUID(CLSID_XAudio2_7_LEGACY, - 0x5a508685, 0xa254, 0x4fba, - 0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf); - DEFINE_GUID(IID_IXAudio2_7_LEGACY, - 0x8bcf1f58, 0x9fe7, 0x4583, - 0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb); - - static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr; - - using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32); - static CreateFX_t CreateFX_orig = nullptr; - - static std::string describe_wave_format(const WAVEFORMATEX *format) { - if (format == nullptr) { - return "null"; - } - - DWORD channel_mask = 0; - if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && - format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { - channel_mask = reinterpret_cast(format)->dwChannelMask; - } - - return fmt::format( - "tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}", - format->wFormatTag, - format->nChannels, - format->nSamplesPerSec, - format->wBitsPerSample, - format->nBlockAlign, - format->nAvgBytesPerSec, - channel_mask); - } - - template - static std::string narrow_fixed(const WCHAR (&value)[Size]) { - size_t length = 0; - while (length < Size && value[length] != L'\0') { - length++; - } - return ws2s(std::wstring(value, length)); - } - - class WrappedXAudio2 final : public IXAudio2_27 { - public: - explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) { - log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast(real)); - } - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override { - if (object == nullptr) { - return E_POINTER; - } - if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) { - *object = this; - AddRef(); - log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid)); - return S_OK; - } - - const auto result = real->QueryInterface(riid, object); - log_info( - "audio::xaudio2", - "IXAudio2::QueryInterface({}) -> {}, object={}", - guid2s(riid), - FMT_HRESULT(result), - object != nullptr ? *object : nullptr); - return result; - } - - ULONG STDMETHODCALLTYPE AddRef() override { - return ++ref_count; - } - - ULONG STDMETHODCALLTYPE Release() override { - const auto remaining = --ref_count; - if (remaining == 0) { - log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy"); - real->Release(); - delete this; - } - return remaining; - } - - HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override { - const auto result = real->GetDeviceCount(device_count); - log_info( - "audio::xaudio2", - "IXAudio2::GetDeviceCount -> {}, count={}", - FMT_HRESULT(result), - SUCCEEDED(result) && device_count != nullptr ? *device_count : 0); - return result; - } - - HRESULT STDMETHODCALLTYPE GetDeviceDetails( - UINT32 device_index, - XAudio2DeviceDetails *device_details) override { - const auto result = real->GetDeviceDetails(device_index, device_details); - if (SUCCEEDED(result) && device_details != nullptr) { - const auto device_name = narrow_fixed(device_details->display_name); - if (!device_details_logged.exchange(true, std::memory_order_relaxed)) { - log_info( - "audio::xaudio2", - "IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}", - device_index, - FMT_HRESULT(result), - narrow_fixed(device_details->device_id), - device_name, - device_details->role, - describe_wave_format(&device_details->output_format.Format)); - } - const auto channels = device_details->output_format.Format.nChannels; - if (channels != 2 && channels != 6 && - !channel_warning_logged.exchange(true, std::memory_order_relaxed)) { - log_warning( - "audio::xaudio2", - "output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output", - device_name, - channels); - deferredlogs::defer_error_messages({ - "unsupported audio output channel count detected!", - fmt::format(" device: {}", device_name), - fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels), - " * configure the default Windows playback device for stereo or 5.1 output", - " * disable 7.1 surround sound or spatial audio for this device", - }); - } - } else { - log_warning( - "audio::xaudio2", - "IXAudio2::GetDeviceDetails({}) -> {}", - device_index, - FMT_HRESULT(result)); - } - return result; - } - - HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override { - const auto result = real->Initialize(flags, processor); - log_info( - "audio::xaudio2", - "IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}", - flags, - processor, - FMT_HRESULT(result)); - return result; - } - - HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override { - const auto result = real->RegisterForCallbacks(callback); - log_info( - "audio::xaudio2", - "IXAudio2::RegisterForCallbacks({}) -> {}", - callback, - FMT_HRESULT(result)); - return result; - } - - void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override { - log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback); - real->UnregisterForCallbacks(callback); - } - - HRESULT STDMETHODCALLTYPE CreateSourceVoice( - void **source_voice, - const WAVEFORMATEX *source_format, - UINT32 flags, - float max_frequency_ratio, - void *callback, - const void *send_list, - const XAudio2EffectChain *effect_chain) override { - return real->CreateSourceVoice( - source_voice, - source_format, - flags, - max_frequency_ratio, - callback, - send_list, - effect_chain); - } - - HRESULT STDMETHODCALLTYPE CreateSubmixVoice( - void **submix_voice, - UINT32 input_channels, - UINT32 input_sample_rate, - UINT32 flags, - UINT32 processing_stage, - const void *send_list, - const XAudio2EffectChain *effect_chain) override { - return real->CreateSubmixVoice( - submix_voice, - input_channels, - input_sample_rate, - flags, - processing_stage, - send_list, - effect_chain); - } - - HRESULT STDMETHODCALLTYPE CreateMasteringVoice( - void **mastering_voice, - UINT32 input_channels, - UINT32 input_sample_rate, - UINT32 flags, - UINT32 device_index, - const XAudio2EffectChain *effect_chain) override { - const auto result = real->CreateMasteringVoice( - mastering_voice, - input_channels, - input_sample_rate, - flags, - device_index, - effect_chain); - log_info( - "audio::xaudio2", - "IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}", - input_channels, - input_sample_rate, - flags, - device_index, - effect_chain != nullptr ? effect_chain->effect_count : 0, - FMT_HRESULT(result), - mastering_voice != nullptr ? *mastering_voice : nullptr); - return result; - } - - HRESULT STDMETHODCALLTYPE StartEngine() override { - const auto result = real->StartEngine(); - log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result)); - return result; - } - - void STDMETHODCALLTYPE StopEngine() override { - log_info("audio::xaudio2", "IXAudio2::StopEngine"); - real->StopEngine(); - } - - HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override { - return real->CommitChanges(operation_set); - } - - void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override { - real->GetPerformanceData(performance_data); - } - - void STDMETHODCALLTYPE SetDebugConfiguration( - const void *debug_configuration, - void *reserved) override { - log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration); - real->SetDebugConfiguration(debug_configuration, reserved); - } - - private: - std::atomic ref_count = 1; - std::atomic_bool device_details_logged = false; - std::atomic_bool channel_warning_logged = false; - IXAudio2_27 *real; - }; - - static HRESULT STDAPICALLTYPE CoCreateInstance_hook( - REFCLSID clsid, - LPUNKNOWN outer, - DWORD class_context, - REFIID iid, - LPVOID *object) { - const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object); - log_info( - "audio::xact", - "CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}", - guid2s(clsid), - guid2s(iid), - class_context, - FMT_HRESULT(result), - object != nullptr ? *object : nullptr); - - if (SUCCEEDED(result) && object != nullptr && *object != nullptr && - IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) && - IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) { - *object = static_cast( - new WrappedXAudio2(static_cast(*object))); - } - return result; - } - - static HRESULT WINAPI CreateFX_hook( - REFCLSID clsid, - IUnknown **effect, - const void *init_data, - UINT32 init_data_size) { - const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size); - log_info( - "audio::xapofx", - "CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}", - guid2s(clsid), - init_data, - init_data_size, - FMT_HRESULT(result), - effect != nullptr ? static_cast(*effect) : nullptr); - return result; - } - - void init() { - const auto libxact = GetModuleHandleW(L"libxact.dll"); - if (libxact == nullptr) { - return; - } - - CoCreateInstance_orig = detour::iat_try( - "CoCreateInstance", CoCreateInstance_hook, libxact); - CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact); - - log_info( - "audio::xact", - "libxact hooks installed: CoCreateInstance={}, CreateFX={}", - CoCreateInstance_orig != nullptr, - CreateFX_orig != nullptr); - } +#include "xact.h" + +#include +#include + +#include +#include +#include +#include + +#include "util/deferlog.h" +#include "util/detour.h" +#include "util/logging.h" +#include "util/utils.h" + +namespace hooks::audio::xact { + + // XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2 + // layout, so keep this proxy pinned to the legacy ABI used by libxact. + struct XAudio2DeviceDetails { + WCHAR device_id[256]; + WCHAR display_name[256]; + DWORD role; + WAVEFORMATEXTENSIBLE output_format; + }; + + struct XAudio2EffectChain { + UINT32 effect_count; + const void *effect_descriptors; + }; + + struct IXAudio2_27 { + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0; + virtual ULONG STDMETHODCALLTYPE AddRef() = 0; + virtual ULONG STDMETHODCALLTYPE Release() = 0; + virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0; + virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails( + UINT32 device_index, + XAudio2DeviceDetails *device_details) = 0; + virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0; + virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0; + virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice( + void **source_voice, + const WAVEFORMATEX *source_format, + UINT32 flags, + float max_frequency_ratio, + void *callback, + const void *send_list, + const XAudio2EffectChain *effect_chain) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice( + void **submix_voice, + UINT32 input_channels, + UINT32 input_sample_rate, + UINT32 flags, + UINT32 processing_stage, + const void *send_list, + const XAudio2EffectChain *effect_chain) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice( + void **mastering_voice, + UINT32 input_channels, + UINT32 input_sample_rate, + UINT32 flags, + UINT32 device_index, + const XAudio2EffectChain *effect_chain) = 0; + virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0; + virtual void STDMETHODCALLTYPE StopEngine() = 0; + virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0; + virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0; + virtual void STDMETHODCALLTYPE SetDebugConfiguration( + const void *debug_configuration, + void *reserved) = 0; + }; + + // XAudio2 2.7 COM class and interface. + DEFINE_GUID(CLSID_XAudio2_7_LEGACY, + 0x5a508685, 0xa254, 0x4fba, + 0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf); + DEFINE_GUID(IID_IXAudio2_7_LEGACY, + 0x8bcf1f58, 0x9fe7, 0x4583, + 0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb); + + static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr; + + using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32); + static CreateFX_t CreateFX_orig = nullptr; + + static std::string describe_wave_format(const WAVEFORMATEX *format) { + if (format == nullptr) { + return "null"; + } + + DWORD channel_mask = 0; + if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && + format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { + channel_mask = reinterpret_cast(format)->dwChannelMask; + } + + return fmt::format( + "tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}", + format->wFormatTag, + format->nChannels, + format->nSamplesPerSec, + format->wBitsPerSample, + format->nBlockAlign, + format->nAvgBytesPerSec, + channel_mask); + } + + template + static std::string narrow_fixed(const WCHAR (&value)[Size]) { + size_t length = 0; + while (length < Size && value[length] != L'\0') { + length++; + } + return ws2s(std::wstring(value, length)); + } + + class WrappedXAudio2 final : public IXAudio2_27 { + public: + explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) { + log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast(real)); + } + + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override { + if (object == nullptr) { + return E_POINTER; + } + if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) { + *object = this; + AddRef(); + log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid)); + return S_OK; + } + + const auto result = real->QueryInterface(riid, object); + log_info( + "audio::xaudio2", + "IXAudio2::QueryInterface({}) -> {}, object={}", + guid2s(riid), + FMT_HRESULT(result), + object != nullptr ? *object : nullptr); + return result; + } + + ULONG STDMETHODCALLTYPE AddRef() override { + return ++ref_count; + } + + ULONG STDMETHODCALLTYPE Release() override { + const auto remaining = --ref_count; + if (remaining == 0) { + log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy"); + real->Release(); + delete this; + } + return remaining; + } + + HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override { + const auto result = real->GetDeviceCount(device_count); + log_info( + "audio::xaudio2", + "IXAudio2::GetDeviceCount -> {}, count={}", + FMT_HRESULT(result), + SUCCEEDED(result) && device_count != nullptr ? *device_count : 0); + return result; + } + + HRESULT STDMETHODCALLTYPE GetDeviceDetails( + UINT32 device_index, + XAudio2DeviceDetails *device_details) override { + const auto result = real->GetDeviceDetails(device_index, device_details); + if (SUCCEEDED(result) && device_details != nullptr) { + const auto device_name = narrow_fixed(device_details->display_name); + if (!device_details_logged.exchange(true, std::memory_order_relaxed)) { + log_info( + "audio::xaudio2", + "IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}", + device_index, + FMT_HRESULT(result), + narrow_fixed(device_details->device_id), + device_name, + device_details->role, + describe_wave_format(&device_details->output_format.Format)); + } + const auto channels = device_details->output_format.Format.nChannels; + if (channels != 2 && channels != 6 && + !channel_warning_logged.exchange(true, std::memory_order_relaxed)) { + log_warning( + "audio::xaudio2", + "output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output", + device_name, + channels); + deferredlogs::defer_error_messages({ + "unsupported audio output channel count detected!", + fmt::format(" device: {}", device_name), + fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels), + " * configure the default Windows playback device for stereo or 5.1 output", + " * disable 7.1 surround sound or spatial audio for this device", + }); + } + } else { + log_warning( + "audio::xaudio2", + "IXAudio2::GetDeviceDetails({}) -> {}", + device_index, + FMT_HRESULT(result)); + } + return result; + } + + HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override { + const auto result = real->Initialize(flags, processor); + log_info( + "audio::xaudio2", + "IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}", + flags, + processor, + FMT_HRESULT(result)); + return result; + } + + HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override { + const auto result = real->RegisterForCallbacks(callback); + log_info( + "audio::xaudio2", + "IXAudio2::RegisterForCallbacks({}) -> {}", + callback, + FMT_HRESULT(result)); + return result; + } + + void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override { + log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback); + real->UnregisterForCallbacks(callback); + } + + HRESULT STDMETHODCALLTYPE CreateSourceVoice( + void **source_voice, + const WAVEFORMATEX *source_format, + UINT32 flags, + float max_frequency_ratio, + void *callback, + const void *send_list, + const XAudio2EffectChain *effect_chain) override { + return real->CreateSourceVoice( + source_voice, + source_format, + flags, + max_frequency_ratio, + callback, + send_list, + effect_chain); + } + + HRESULT STDMETHODCALLTYPE CreateSubmixVoice( + void **submix_voice, + UINT32 input_channels, + UINT32 input_sample_rate, + UINT32 flags, + UINT32 processing_stage, + const void *send_list, + const XAudio2EffectChain *effect_chain) override { + return real->CreateSubmixVoice( + submix_voice, + input_channels, + input_sample_rate, + flags, + processing_stage, + send_list, + effect_chain); + } + + HRESULT STDMETHODCALLTYPE CreateMasteringVoice( + void **mastering_voice, + UINT32 input_channels, + UINT32 input_sample_rate, + UINT32 flags, + UINT32 device_index, + const XAudio2EffectChain *effect_chain) override { + const auto result = real->CreateMasteringVoice( + mastering_voice, + input_channels, + input_sample_rate, + flags, + device_index, + effect_chain); + log_info( + "audio::xaudio2", + "IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}", + input_channels, + input_sample_rate, + flags, + device_index, + effect_chain != nullptr ? effect_chain->effect_count : 0, + FMT_HRESULT(result), + mastering_voice != nullptr ? *mastering_voice : nullptr); + return result; + } + + HRESULT STDMETHODCALLTYPE StartEngine() override { + const auto result = real->StartEngine(); + log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result)); + return result; + } + + void STDMETHODCALLTYPE StopEngine() override { + log_info("audio::xaudio2", "IXAudio2::StopEngine"); + real->StopEngine(); + } + + HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override { + return real->CommitChanges(operation_set); + } + + void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override { + real->GetPerformanceData(performance_data); + } + + void STDMETHODCALLTYPE SetDebugConfiguration( + const void *debug_configuration, + void *reserved) override { + log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration); + real->SetDebugConfiguration(debug_configuration, reserved); + } + + private: + std::atomic ref_count = 1; + std::atomic_bool device_details_logged = false; + std::atomic_bool channel_warning_logged = false; + IXAudio2_27 *real; + }; + + static HRESULT STDAPICALLTYPE CoCreateInstance_hook( + REFCLSID clsid, + LPUNKNOWN outer, + DWORD class_context, + REFIID iid, + LPVOID *object) { + const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object); + log_info( + "audio::xact", + "CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}", + guid2s(clsid), + guid2s(iid), + class_context, + FMT_HRESULT(result), + object != nullptr ? *object : nullptr); + + if (SUCCEEDED(result) && object != nullptr && *object != nullptr && + IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) && + IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) { + *object = static_cast( + new WrappedXAudio2(static_cast(*object))); + } + return result; + } + + static HRESULT WINAPI CreateFX_hook( + REFCLSID clsid, + IUnknown **effect, + const void *init_data, + UINT32 init_data_size) { + const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size); + log_info( + "audio::xapofx", + "CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}", + guid2s(clsid), + init_data, + init_data_size, + FMT_HRESULT(result), + effect != nullptr ? static_cast(*effect) : nullptr); + return result; + } + + void init() { + const auto libxact = GetModuleHandleW(L"libxact.dll"); + if (libxact == nullptr) { + return; + } + + CoCreateInstance_orig = detour::iat_try( + "CoCreateInstance", CoCreateInstance_hook, libxact); + CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact); + + log_info( + "audio::xact", + "libxact hooks installed: CoCreateInstance={}, CreateFX={}", + CoCreateInstance_orig != nullptr, + CreateFX_orig != nullptr); + } } \ No newline at end of file diff --git a/src/spice2x/hooks/audio/xact.h b/src/spice2x/hooks/audio/xact.h index 0c0cb89..06094ed 100644 --- a/src/spice2x/hooks/audio/xact.h +++ b/src/spice2x/hooks/audio/xact.h @@ -1,5 +1,5 @@ -#pragma once - -namespace hooks::audio::xact { - void init(); +#pragma once + +namespace hooks::audio::xact { + void init(); } \ No newline at end of file diff --git a/src/spice2x/hooks/devicehook.cpp b/src/spice2x/hooks/devicehook.cpp index 9f47b0a..038c35b 100644 --- a/src/spice2x/hooks/devicehook.cpp +++ b/src/spice2x/hooks/devicehook.cpp @@ -574,7 +574,7 @@ void devicehook_init(HMODULE module) { STORE(EscapeCommFunction_orig, detour::iat_try("EscapeCommFunction", EscapeCommFunction_hook, module)); STORE(GetCommState_orig, detour::iat_try("GetCommState", GetCommState_hook, module)); STORE(GetFileSize_orig, detour::iat_try("GetFileSize", GetFileSize_hook, module)); - STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSize", GetFileSizeEx_hook, module)); + STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSizeEx", GetFileSizeEx_hook, module)); STORE(GetFileInformationByHandle_orig, detour::iat_try( "GetFileInformationByHandle", GetFileInformationByHandle_hook, module)); STORE(PurgeComm_orig, detour::iat_try("PurgeComm", PurgeComm_hook, module)); diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp index c680fc4..f5d0c8f 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.cpp @@ -1,304 +1,304 @@ -// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports -// the moment those DLLs appear (LDR notification + poll-thread fallback), -// then drives proactive vtable capture so we don't lose the race against -// the execexe loader. per-vtable hook implementations live in the sibling -// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture / -// d3d11_screenshot). -// -// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and -// fails (error 0xa) if they're already in the loader's module list. -// -// 64-bit only. - -#include "d3d11_backend.h" - -#ifndef SPICE_D3D11 - -void graphics_d3d11_init() {} -void graphics_d3d11_shutdown() {} - -#else - -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#include "d3d11_internal.h" -#include "util/nt_loader.h" - -namespace { - -using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)( - IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, - const D3D_FEATURE_LEVEL *, UINT, UINT, - const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **, - ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); -using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **); -using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); -using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); - -D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr; -CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr; -CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr; -CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr; - -std::atomic g_d3d11_exports_hooked { false }; -std::atomic g_dxgi_exports_hooked { false }; - -// ---------------------------------------------------------------------- -// top-level export hooks - -HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook( - IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags, - const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, - const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain, - ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel, - ID3D11DeviceContext **ppImmediateContext) -{ - HRESULT res = D3D11CreateDeviceAndSwapChain_orig( - pAdapter, DriverType, Software, Flags, - pFeatureLevels, FeatureLevels, SDKVersion, - pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext); - if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { - if (pSwapChainDesc) { - d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow); - } - d3d11_hooks::install_swapchain_hooks(*ppSwapChain); - } - return res; -} - -#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \ - HRESULT WINAPI NAME##_hook SIG_PARAMS { \ - HRESULT res = NAME##_orig ORIG_ARGS; \ - if (SUCCEEDED(res) && ppFactory && *ppFactory) { \ - d3d11_hooks::install_factory_hooks( \ - reinterpret_cast(*ppFactory)); \ - } \ - return res; \ - } - -DEFINE_FACTORY_HOOK(CreateDXGIFactory, - (REFIID riid, void **ppFactory), - (riid, ppFactory)) -DEFINE_FACTORY_HOOK(CreateDXGIFactory1, - (REFIID riid, void **ppFactory), - (riid, ppFactory)) -DEFINE_FACTORY_HOOK(CreateDXGIFactory2, - (UINT Flags, REFIID riid, void **ppFactory), - (Flags, riid, ppFactory)) - -#undef DEFINE_FACTORY_HOOK - -// ---------------------------------------------------------------------- -// export trampoline plumbing - -// serializes trampoline_export() so the LDR notification callback and the -// poll thread don't race each other into MinHook against the same target. -std::mutex g_export_mutex; - -bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) { - std::lock_guard lock(g_export_mutex); - if (*orig) { - return true; - } - HMODULE mod = GetModuleHandleA(dll); - if (!mod) { - return false; - } - void *addr = reinterpret_cast(GetProcAddress(mod, name)); - if (!addr) { - return false; - } - *orig = addr; // trampoline_try reads *orig before overwriting it. - if (!detour::trampoline_try(addr, hook, orig)) { - *orig = nullptr; - return false; - } - log_info("graphics::d3d11", "trampolined {}!{}", dll, name); - return true; -} - -void try_install_d3d11_exports() { - if (g_d3d11_exports_hooked) { - return; - } - if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain", - (void *) D3D11CreateDeviceAndSwapChain_hook, - (void **) &D3D11CreateDeviceAndSwapChain_orig)) { - g_d3d11_exports_hooked = true; - } -} - -void try_install_dxgi_exports() { - if (g_dxgi_exports_hooked) { - return; - } - struct entry { const char *name; void *hook; void **orig; }; - const entry entries[] = { - { "CreateDXGIFactory", (void *) CreateDXGIFactory_hook, - (void **) &CreateDXGIFactory_orig }, - { "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook, - (void **) &CreateDXGIFactory1_orig }, - { "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook, - (void **) &CreateDXGIFactory2_orig }, - }; - bool any = false; - for (auto &e : entries) { - any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig); - } - if (any) { - g_dxgi_exports_hooked = true; - } -} - -void try_capture_if_ready() { - if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { - d3d11_hooks::try_capture_vtables(); - } -} - -// ---------------------------------------------------------------------- -// LDR notification + polling fallback - -bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) { - if (!name || !name->Buffer) { - return false; - } - const size_t n = name->Length / sizeof(WCHAR); - const size_t s = wcslen(suffix); - return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0; -} - -VOID CALLBACK ldr_dll_notification( - ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/) -{ - if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) { - return; - } - if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) { - try_install_d3d11_exports(); - } else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) { - try_install_dxgi_exports(); - } -} - -// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the -// notification above never fires for those DLLs and we have to poll. -std::atomic g_stop { false }; -std::thread g_poll_thread; -std::mutex g_init_mutex; -PVOID g_ldr_cookie = nullptr; - -void poll_thread() { - using namespace std::chrono_literals; - for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) { - try_install_d3d11_exports(); - try_install_dxgi_exports(); - if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { - d3d11_hooks::try_capture_vtables(); - return; - } - // sliced so shutdown doesn't have to wait a full second. - for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) { - std::this_thread::sleep_for(100ms); - } - } -} - -// the overlay's imgui dx11 backend needs D3DCompile (d3dcompiler_XX.dll) to -// build its shaders. _43 ships with the DX June 2010 redist on stock Win7; -// _46/_47 come with newer Windows. -bool d3dcompiler_available() { - static const wchar_t *names[] = { - L"d3dcompiler_47.dll", - L"d3dcompiler_46.dll", - L"d3dcompiler_43.dll", - }; - for (auto name : names) { - HMODULE mod = GetModuleHandleW(name); - if (!mod) { - mod = LoadLibraryW(name); - } - if (mod && GetProcAddress(mod, "D3DCompile")) { - return true; - } - } - return false; -} - -} // namespace - -void graphics_d3d11_init() { - // dx11 titles always run under execexe. skipping on pure-dx9 games keeps - // their startup path completely untouched (no exports patched, no poll - // thread, no LDR callback). - if (!GetModuleHandleW(L"execexe.dll")) { - return; - } - - // no d3dcompiler -> overlay can't build shaders; skip dx11 overlay - if (!d3dcompiler_available()) { - log_warning( - "graphics::d3d11", - "d3dcompiler not found; dx11 overlay disabled"); - return; - } - - std::lock_guard lock(g_init_mutex); - if (g_poll_thread.joinable()) { - return; // already initialized - } - - log_info("graphics::d3d11", "initializing"); - - // trampoline now if either DLL is already in the PEB. - try_install_d3d11_exports(); - try_install_dxgi_exports(); - try_capture_if_ready(); - - // catches standard LdrLoadDll loads. - auto reg = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification")); - if (reg) { - NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie); - if (NT_SUCCESS(st)) { - log_info("graphics::d3d11", "registered LDR DLL notification"); - } else { - g_ldr_cookie = nullptr; - log_warning("graphics::d3d11", - "LdrRegisterDllNotification failed: {:#x}", (unsigned long)st); - } - } - - // catches the execexe loader path that bypasses LdrLoadDll. - g_poll_thread = std::thread(poll_thread); -} - -void graphics_d3d11_shutdown() { - std::lock_guard lock(g_init_mutex); - - // unregister first so the callback can't fire mid-teardown. - if (g_ldr_cookie) { - auto unreg = reinterpret_cast( - GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification")); - if (unreg) { - unreg(g_ldr_cookie); - } - g_ldr_cookie = nullptr; - } - - g_stop.store(true); - if (g_poll_thread.joinable()) { - g_poll_thread.join(); - } -} - -#endif // SPICE_D3D11 +// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports +// the moment those DLLs appear (LDR notification + poll-thread fallback), +// then drives proactive vtable capture so we don't lose the race against +// the execexe loader. per-vtable hook implementations live in the sibling +// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture / +// d3d11_screenshot). +// +// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and +// fails (error 0xa) if they're already in the loader's module list. +// +// 64-bit only. + +#include "d3d11_backend.h" + +#ifndef SPICE_D3D11 + +void graphics_d3d11_init() {} +void graphics_d3d11_shutdown() {} + +#else + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" +#include "util/nt_loader.h" + +namespace { + +using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)( + IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, + const D3D_FEATURE_LEVEL *, UINT, UINT, + const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **, + ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); +using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); + +D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr; +CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr; +CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr; +CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr; + +std::atomic g_d3d11_exports_hooked { false }; +std::atomic g_dxgi_exports_hooked { false }; + +// ---------------------------------------------------------------------- +// top-level export hooks + +HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook( + IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags, + const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, + const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain, + ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel, + ID3D11DeviceContext **ppImmediateContext) +{ + HRESULT res = D3D11CreateDeviceAndSwapChain_orig( + pAdapter, DriverType, Software, Flags, + pFeatureLevels, FeatureLevels, SDKVersion, + pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + if (pSwapChainDesc) { + d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow); + } + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \ + HRESULT WINAPI NAME##_hook SIG_PARAMS { \ + HRESULT res = NAME##_orig ORIG_ARGS; \ + if (SUCCEEDED(res) && ppFactory && *ppFactory) { \ + d3d11_hooks::install_factory_hooks( \ + reinterpret_cast(*ppFactory)); \ + } \ + return res; \ + } + +DEFINE_FACTORY_HOOK(CreateDXGIFactory, + (REFIID riid, void **ppFactory), + (riid, ppFactory)) +DEFINE_FACTORY_HOOK(CreateDXGIFactory1, + (REFIID riid, void **ppFactory), + (riid, ppFactory)) +DEFINE_FACTORY_HOOK(CreateDXGIFactory2, + (UINT Flags, REFIID riid, void **ppFactory), + (Flags, riid, ppFactory)) + +#undef DEFINE_FACTORY_HOOK + +// ---------------------------------------------------------------------- +// export trampoline plumbing + +// serializes trampoline_export() so the LDR notification callback and the +// poll thread don't race each other into MinHook against the same target. +std::mutex g_export_mutex; + +bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) { + std::lock_guard lock(g_export_mutex); + if (*orig) { + return true; + } + HMODULE mod = GetModuleHandleA(dll); + if (!mod) { + return false; + } + void *addr = reinterpret_cast(GetProcAddress(mod, name)); + if (!addr) { + return false; + } + *orig = addr; // trampoline_try reads *orig before overwriting it. + if (!detour::trampoline_try(addr, hook, orig)) { + *orig = nullptr; + return false; + } + log_info("graphics::d3d11", "trampolined {}!{}", dll, name); + return true; +} + +void try_install_d3d11_exports() { + if (g_d3d11_exports_hooked) { + return; + } + if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain", + (void *) D3D11CreateDeviceAndSwapChain_hook, + (void **) &D3D11CreateDeviceAndSwapChain_orig)) { + g_d3d11_exports_hooked = true; + } +} + +void try_install_dxgi_exports() { + if (g_dxgi_exports_hooked) { + return; + } + struct entry { const char *name; void *hook; void **orig; }; + const entry entries[] = { + { "CreateDXGIFactory", (void *) CreateDXGIFactory_hook, + (void **) &CreateDXGIFactory_orig }, + { "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook, + (void **) &CreateDXGIFactory1_orig }, + { "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook, + (void **) &CreateDXGIFactory2_orig }, + }; + bool any = false; + for (auto &e : entries) { + any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig); + } + if (any) { + g_dxgi_exports_hooked = true; + } +} + +void try_capture_if_ready() { + if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { + d3d11_hooks::try_capture_vtables(); + } +} + +// ---------------------------------------------------------------------- +// LDR notification + polling fallback + +bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) { + if (!name || !name->Buffer) { + return false; + } + const size_t n = name->Length / sizeof(WCHAR); + const size_t s = wcslen(suffix); + return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0; +} + +VOID CALLBACK ldr_dll_notification( + ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/) +{ + if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) { + return; + } + if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) { + try_install_d3d11_exports(); + } else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) { + try_install_dxgi_exports(); + } +} + +// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the +// notification above never fires for those DLLs and we have to poll. +std::atomic g_stop { false }; +std::thread g_poll_thread; +std::mutex g_init_mutex; +PVOID g_ldr_cookie = nullptr; + +void poll_thread() { + using namespace std::chrono_literals; + for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) { + try_install_d3d11_exports(); + try_install_dxgi_exports(); + if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { + d3d11_hooks::try_capture_vtables(); + return; + } + // sliced so shutdown doesn't have to wait a full second. + for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) { + std::this_thread::sleep_for(100ms); + } + } +} + +// the overlay's imgui dx11 backend needs D3DCompile (d3dcompiler_XX.dll) to +// build its shaders. _43 ships with the DX June 2010 redist on stock Win7; +// _46/_47 come with newer Windows. +bool d3dcompiler_available() { + static const wchar_t *names[] = { + L"d3dcompiler_47.dll", + L"d3dcompiler_46.dll", + L"d3dcompiler_43.dll", + }; + for (auto name : names) { + HMODULE mod = GetModuleHandleW(name); + if (!mod) { + mod = LoadLibraryW(name); + } + if (mod && GetProcAddress(mod, "D3DCompile")) { + return true; + } + } + return false; +} + +} // namespace + +void graphics_d3d11_init() { + // dx11 titles always run under execexe. skipping on pure-dx9 games keeps + // their startup path completely untouched (no exports patched, no poll + // thread, no LDR callback). + if (!GetModuleHandleW(L"execexe.dll")) { + return; + } + + // no d3dcompiler -> overlay can't build shaders; skip dx11 overlay + if (!d3dcompiler_available()) { + log_warning( + "graphics::d3d11", + "d3dcompiler not found; dx11 overlay disabled"); + return; + } + + std::lock_guard lock(g_init_mutex); + if (g_poll_thread.joinable()) { + return; // already initialized + } + + log_info("graphics::d3d11", "initializing"); + + // trampoline now if either DLL is already in the PEB. + try_install_d3d11_exports(); + try_install_dxgi_exports(); + try_capture_if_ready(); + + // catches standard LdrLoadDll loads. + auto reg = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification")); + if (reg) { + NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie); + if (NT_SUCCESS(st)) { + log_info("graphics::d3d11", "registered LDR DLL notification"); + } else { + g_ldr_cookie = nullptr; + log_warning("graphics::d3d11", + "LdrRegisterDllNotification failed: {:#x}", (unsigned long)st); + } + } + + // catches the execexe loader path that bypasses LdrLoadDll. + g_poll_thread = std::thread(poll_thread); +} + +void graphics_d3d11_shutdown() { + std::lock_guard lock(g_init_mutex); + + // unregister first so the callback can't fire mid-teardown. + if (g_ldr_cookie) { + auto unreg = reinterpret_cast( + GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification")); + if (unreg) { + unreg(g_ldr_cookie); + } + g_ldr_cookie = nullptr; + } + + g_stop.store(true); + if (g_poll_thread.joinable()) { + g_poll_thread.join(); + } +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h index 6e06788..4288bad 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_backend.h @@ -1,24 +1,24 @@ -#pragma once - -#include "overlay/overlay.h" - -void graphics_d3d11_init(); -void graphics_d3d11_shutdown(); - -#ifdef SPICE_D3D11 - -struct ID3D11Device; -struct ID3D11DeviceContext; -struct ID3D11RenderTargetView; -struct IDXGISwapChain; - -namespace overlay::d3d11 { - - void render(ID3D11Device *device, - ID3D11DeviceContext *context, - IDXGISwapChain *swapchain, - ID3D11RenderTargetView **rtv); - -} - -#endif +#pragma once + +#include "overlay/overlay.h" + +void graphics_d3d11_init(); +void graphics_d3d11_shutdown(); + +#ifdef SPICE_D3D11 + +struct ID3D11Device; +struct ID3D11DeviceContext; +struct ID3D11RenderTargetView; +struct IDXGISwapChain; + +namespace overlay::d3d11 { + + void render(ID3D11Device *device, + ID3D11DeviceContext *context, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv); + +} + +#endif diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp index 381d3b3..d11f7f7 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_factory.cpp @@ -1,102 +1,102 @@ -// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd -// so we can install_swapchain_hooks against every newly-created swapchain. - -#include "d3d11_backend.h" - -#ifdef SPICE_D3D11 - -#include - -#include -#include -#include -#include - -#include "d3d11_internal.h" - -namespace { - -using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)( - IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **); -using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)( - IDXGIFactory2 *, IUnknown *, HWND, - const DXGI_SWAP_CHAIN_DESC1 *, - const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *, - IDXGIOutput *, IDXGISwapChain1 **); - -CreateSwapChain_t CreateSwapChain_orig = nullptr; -CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr; - -bool g_factory_hooked = false; -bool g_factory2_hooked = false; -std::mutex g_hook_mutex; - -HRESULT STDMETHODCALLTYPE CreateSwapChain_hook( - IDXGIFactory *factory, IUnknown *pDevice, - DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain) -{ - HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain); - if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { - if (pDesc) { - d3d11_hooks::note_main_hwnd(pDesc->OutputWindow); - } - d3d11_hooks::install_swapchain_hooks(*ppSwapChain); - } - return res; -} - -HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook( - IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd, - const DXGI_SWAP_CHAIN_DESC1 *pDesc, - const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, - IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain) -{ - HRESULT res = CreateSwapChainForHwnd_orig( - factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain); - if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { - d3d11_hooks::note_main_hwnd(hWnd); - d3d11_hooks::install_swapchain_hooks(*ppSwapChain); - } - return res; -} - -// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths. -template -void install_on(IUnknown *factory, bool &flag, - size_t vtbl_index, void *hook, void **orig, const char *name) -{ - if (flag) { - return; - } - Iface *f = nullptr; - if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) { - return; - } - if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) { - flag = true; - } - f->Release(); -} - -} // namespace - -namespace d3d11_hooks { - -void install_factory_hooks(IUnknown *factory) { - if (!factory) { - return; - } - std::lock_guard lock(g_hook_mutex); - - install_on(factory, g_factory_hooked, 10, - (void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig, - "IDXGIFactory::CreateSwapChain"); - - install_on(factory, g_factory2_hooked, 15, - (void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig, - "IDXGIFactory2::CreateSwapChainForHwnd"); -} - -} - -#endif // SPICE_D3D11 +// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd +// so we can install_swapchain_hooks against every newly-created swapchain. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +namespace { + +using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **); +using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGIFactory2 *, IUnknown *, HWND, + const DXGI_SWAP_CHAIN_DESC1 *, + const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *, + IDXGIOutput *, IDXGISwapChain1 **); + +CreateSwapChain_t CreateSwapChain_orig = nullptr; +CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr; + +bool g_factory_hooked = false; +bool g_factory2_hooked = false; +std::mutex g_hook_mutex; + +HRESULT STDMETHODCALLTYPE CreateSwapChain_hook( + IDXGIFactory *factory, IUnknown *pDevice, + DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain) +{ + HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + if (pDesc) { + d3d11_hooks::note_main_hwnd(pDesc->OutputWindow); + } + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook( + IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd, + const DXGI_SWAP_CHAIN_DESC1 *pDesc, + const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, + IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain) +{ + HRESULT res = CreateSwapChainForHwnd_orig( + factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain); + if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { + d3d11_hooks::note_main_hwnd(hWnd); + d3d11_hooks::install_swapchain_hooks(*ppSwapChain); + } + return res; +} + +// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths. +template +void install_on(IUnknown *factory, bool &flag, + size_t vtbl_index, void *hook, void **orig, const char *name) +{ + if (flag) { + return; + } + Iface *f = nullptr; + if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) { + return; + } + if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) { + flag = true; + } + f->Release(); +} + +} // namespace + +namespace d3d11_hooks { + +void install_factory_hooks(IUnknown *factory) { + if (!factory) { + return; + } + std::lock_guard lock(g_hook_mutex); + + install_on(factory, g_factory_hooked, 10, + (void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig, + "IDXGIFactory::CreateSwapChain"); + + install_on(factory, g_factory2_hooked, 15, + (void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig, + "IDXGIFactory2::CreateSwapChainForHwnd"); +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h index 165288d..c0e5489 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_internal.h @@ -1,59 +1,59 @@ -#pragma once - -// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11. - -#include "overlay/overlay.h" - -#ifdef SPICE_D3D11 - -#include - -#include "util/detour.h" -#include "util/logging.h" - -struct HWND__; typedef HWND__ *HWND; -struct IUnknown; -struct IDXGISwapChain; - -namespace d3d11_hooks { - - void install_swapchain_hooks(IDXGISwapChain *swapchain); - void install_factory_hooks(IUnknown *factory); - void try_capture_vtables(); - - // first non-null swapchain HWND wins; later ones (sub-screens, IME - // helpers) are ignored. the dummy capture window is exempted via - // ignore_hwnd. - void note_main_hwnd(HWND hwnd); - HWND main_hwnd(); - void ignore_hwnd(HWND hwnd); - - // capture backbuffer to PNG if a screenshot was requested. - void try_screenshot(IDXGISwapChain *swapchain); - - // trampoline a virtual method by vtable index. on failure *orig is null. - inline bool hook_vtbl(void *iface, size_t index, - void *hook, void **orig, const char *name) - { - void **vtbl = *reinterpret_cast(iface); - void *target = vtbl[index]; - // trampoline_try reads *orig before overwriting it. - *orig = target; - if (!detour::trampoline_try(target, hook, orig)) { - *orig = nullptr; - log_warning("graphics::d3d11", "failed to hook {}", name); - return false; - } - log_info("graphics::d3d11", "hooked {}", name); - return true; - } - - // minimal COM RAII used by capture / screenshot paths. - struct com_release { - void operator()(IUnknown *p) const { if (p) p->Release(); } - }; - template using com_ptr = std::unique_ptr; - -} - -#endif +#pragma once + +// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11. + +#include "overlay/overlay.h" + +#ifdef SPICE_D3D11 + +#include + +#include "util/detour.h" +#include "util/logging.h" + +struct HWND__; typedef HWND__ *HWND; +struct IUnknown; +struct IDXGISwapChain; + +namespace d3d11_hooks { + + void install_swapchain_hooks(IDXGISwapChain *swapchain); + void install_factory_hooks(IUnknown *factory); + void try_capture_vtables(); + + // first non-null swapchain HWND wins; later ones (sub-screens, IME + // helpers) are ignored. the dummy capture window is exempted via + // ignore_hwnd. + void note_main_hwnd(HWND hwnd); + HWND main_hwnd(); + void ignore_hwnd(HWND hwnd); + + // capture backbuffer to PNG if a screenshot was requested. + void try_screenshot(IDXGISwapChain *swapchain); + + // trampoline a virtual method by vtable index. on failure *orig is null. + inline bool hook_vtbl(void *iface, size_t index, + void *hook, void **orig, const char *name) + { + void **vtbl = *reinterpret_cast(iface); + void *target = vtbl[index]; + // trampoline_try reads *orig before overwriting it. + *orig = target; + if (!detour::trampoline_try(target, hook, orig)) { + *orig = nullptr; + log_warning("graphics::d3d11", "failed to hook {}", name); + return false; + } + log_info("graphics::d3d11", "hooked {}", name); + return true; + } + + // minimal COM RAII used by capture / screenshot paths. + struct com_release { + void operator()(IUnknown *p) const { if (p) p->Release(); } + }; + template using com_ptr = std::unique_ptr; + +} + +#endif diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp index af4ceac..bb204f0 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_screenshot.cpp @@ -1,165 +1,165 @@ -// dx11 screenshot capture. mirrors the d3d9 backend: copy the current -// backbuffer into a staging texture, force alpha=255, write PNG via -// stb_image_write, push to clipboard and notify. - -#include "d3d11_backend.h" - -#ifdef SPICE_D3D11 - -#include - -#include -#include -#include - -#include "d3d11_internal.h" - -#include "external/stb_image_write.h" -#include "hooks/graphics/graphics.h" -#include "misc/clipboard.h" -#include "overlay/notifications.h" -#include "util/fileutils.h" - -using d3d11_hooks::com_ptr; - -namespace { - -// copy the swapchain backbuffer into a CPU-readable staging texture and -// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled, -// alpha is forced to 255). -bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain, - ID3D11Device *device, - ID3D11DeviceContext *context, - std::vector &out, - uint32_t &out_w, uint32_t &out_h) -{ - ID3D11Texture2D *raw_bb = nullptr; - if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) { - return false; - } - com_ptr backbuffer(raw_bb); - - D3D11_TEXTURE2D_DESC desc {}; - backbuffer->GetDesc(&desc); - - // MSAA backbuffers can't be CopyResource'd into a non-MS staging target. - com_ptr resolved; - ID3D11Texture2D *source = backbuffer.get(); - if (desc.SampleDesc.Count > 1) { - D3D11_TEXTURE2D_DESC rd = desc; - rd.SampleDesc.Count = 1; - rd.SampleDesc.Quality = 0; - rd.Usage = D3D11_USAGE_DEFAULT; - rd.BindFlags = D3D11_BIND_RENDER_TARGET; - rd.CPUAccessFlags = 0; - rd.MiscFlags = 0; - ID3D11Texture2D *r = nullptr; - if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) { - return false; - } - resolved.reset(r); - context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format); - source = resolved.get(); - } - - D3D11_TEXTURE2D_DESC sd {}; - sd.Width = desc.Width; - sd.Height = desc.Height; - sd.MipLevels = 1; - sd.ArraySize = 1; - sd.Format = desc.Format; - sd.SampleDesc.Count = 1; - sd.Usage = D3D11_USAGE_STAGING; - sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; - - ID3D11Texture2D *raw_staging = nullptr; - if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) { - return false; - } - com_ptr staging(raw_staging); - context->CopyResource(staging.get(), source); - - D3D11_MAPPED_SUBRESOURCE mapped {}; - if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { - return false; - } - - // backbuffers from GetDesc are always fully-typed (never _TYPELESS). - const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM - || desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; - - out.resize(static_cast(desc.Width) * desc.Height * 4); - const uint8_t *src_base = reinterpret_cast(mapped.pData); - for (uint32_t y = 0; y < desc.Height; ++y) { - const uint8_t *row = src_base + static_cast(y) * mapped.RowPitch; - uint8_t *dst = out.data() + static_cast(y) * desc.Width * 4; - for (uint32_t x = 0; x < desc.Width; ++x) { - dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)]; - dst[x * 4 + 1] = row[x * 4 + 1]; - dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)]; - dst[x * 4 + 3] = 255; - } - } - - context->Unmap(staging.get(), 0); - - out_w = desc.Width; - out_h = desc.Height; - return true; -} - -} // namespace - -namespace d3d11_hooks { - -void try_screenshot(IDXGISwapChain *swapchain) { - if (!swapchain || !graphics_screenshot_consume()) { - return; - } - - auto file_path = graphics_screenshot_genpath(); - if (file_path.empty()) { - return; - } - - ID3D11Device *raw_device = nullptr; - if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) { - return; - } - com_ptr device(raw_device); - ID3D11DeviceContext *raw_ctx = nullptr; - device->GetImmediateContext(&raw_ctx); - if (!raw_ctx) { - return; - } - com_ptr context(raw_ctx); - - std::vector pixels; - uint32_t w = 0, h = 0; - if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) { - log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer"); - overlay::notifications::add( - overlay::notifications::Severity::Error, - "Screenshot failed to capture"); - return; - } - - log_info("graphics::d3d11", "saving screenshot to {}", file_path); - if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4, - pixels.data(), (int) w * 4)) - { - clipboard::copy_image(file_path); - overlay::notifications::add( - overlay::notifications::Severity::Success, - fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); - } else { - log_warning("graphics::d3d11", "screenshot: stbi_write_png failed"); - overlay::notifications::add( - overlay::notifications::Severity::Error, - "Screenshot failed to save"); - } -} - -} - -#endif // SPICE_D3D11 +// dx11 screenshot capture. mirrors the d3d9 backend: copy the current +// backbuffer into a staging texture, force alpha=255, write PNG via +// stb_image_write, push to clipboard and notify. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include + +#include +#include +#include + +#include "d3d11_internal.h" + +#include "external/stb_image_write.h" +#include "hooks/graphics/graphics.h" +#include "misc/clipboard.h" +#include "overlay/notifications.h" +#include "util/fileutils.h" + +using d3d11_hooks::com_ptr; + +namespace { + +// copy the swapchain backbuffer into a CPU-readable staging texture and +// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled, +// alpha is forced to 255). +bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain, + ID3D11Device *device, + ID3D11DeviceContext *context, + std::vector &out, + uint32_t &out_w, uint32_t &out_h) +{ + ID3D11Texture2D *raw_bb = nullptr; + if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) { + return false; + } + com_ptr backbuffer(raw_bb); + + D3D11_TEXTURE2D_DESC desc {}; + backbuffer->GetDesc(&desc); + + // MSAA backbuffers can't be CopyResource'd into a non-MS staging target. + com_ptr resolved; + ID3D11Texture2D *source = backbuffer.get(); + if (desc.SampleDesc.Count > 1) { + D3D11_TEXTURE2D_DESC rd = desc; + rd.SampleDesc.Count = 1; + rd.SampleDesc.Quality = 0; + rd.Usage = D3D11_USAGE_DEFAULT; + rd.BindFlags = D3D11_BIND_RENDER_TARGET; + rd.CPUAccessFlags = 0; + rd.MiscFlags = 0; + ID3D11Texture2D *r = nullptr; + if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) { + return false; + } + resolved.reset(r); + context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format); + source = resolved.get(); + } + + D3D11_TEXTURE2D_DESC sd {}; + sd.Width = desc.Width; + sd.Height = desc.Height; + sd.MipLevels = 1; + sd.ArraySize = 1; + sd.Format = desc.Format; + sd.SampleDesc.Count = 1; + sd.Usage = D3D11_USAGE_STAGING; + sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + + ID3D11Texture2D *raw_staging = nullptr; + if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) { + return false; + } + com_ptr staging(raw_staging); + context->CopyResource(staging.get(), source); + + D3D11_MAPPED_SUBRESOURCE mapped {}; + if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { + return false; + } + + // backbuffers from GetDesc are always fully-typed (never _TYPELESS). + const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM + || desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; + + out.resize(static_cast(desc.Width) * desc.Height * 4); + const uint8_t *src_base = reinterpret_cast(mapped.pData); + for (uint32_t y = 0; y < desc.Height; ++y) { + const uint8_t *row = src_base + static_cast(y) * mapped.RowPitch; + uint8_t *dst = out.data() + static_cast(y) * desc.Width * 4; + for (uint32_t x = 0; x < desc.Width; ++x) { + dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)]; + dst[x * 4 + 1] = row[x * 4 + 1]; + dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)]; + dst[x * 4 + 3] = 255; + } + } + + context->Unmap(staging.get(), 0); + + out_w = desc.Width; + out_h = desc.Height; + return true; +} + +} // namespace + +namespace d3d11_hooks { + +void try_screenshot(IDXGISwapChain *swapchain) { + if (!swapchain || !graphics_screenshot_consume()) { + return; + } + + auto file_path = graphics_screenshot_genpath(); + if (file_path.empty()) { + return; + } + + ID3D11Device *raw_device = nullptr; + if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) { + return; + } + com_ptr device(raw_device); + ID3D11DeviceContext *raw_ctx = nullptr; + device->GetImmediateContext(&raw_ctx); + if (!raw_ctx) { + return; + } + com_ptr context(raw_ctx); + + std::vector pixels; + uint32_t w = 0, h = 0; + if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) { + log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer"); + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to capture"); + return; + } + + log_info("graphics::d3d11", "saving screenshot to {}", file_path); + if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4, + pixels.data(), (int) w * 4)) + { + clipboard::copy_image(file_path); + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); + } else { + log_warning("graphics::d3d11", "screenshot: stbi_write_png failed"); + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to save"); + } +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp index c7a8b00..92abb78 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_swapchain.cpp @@ -1,294 +1,343 @@ -// dx11 swapchain vtable hooks + per-frame overlay pump. -// -// dxgi shares vtables across swapchain instances, so we only need to patch -// Present / Present1 / ResizeBuffers once on the first instance we see. -// each frame we lazily attach the overlay to whichever swapchain is -// presenting, then drive its imgui update / new_frame / render cycle. - -#include "d3d11_backend.h" - -#ifdef SPICE_D3D11 - -#include -#include - -#include -#include -#include -#include - -#include "d3d11_internal.h" - -#include "external/imgui/imgui.h" -#include "external/imgui/backends/imgui_impl_dx11.h" -#include "overlay/imgui/impl_spice.h" - -#include "games/io.h" -#include "hooks/graphics/graphics.h" -#include "launcher/launcher.h" -#include "misc/eamuse.h" -#include "util/utils.h" - -// -------------------------------------------------------------------------- -// overlay render bridge - -namespace overlay::d3d11 { - - // sRGB backbuffers need a UNORM view: ImGui vertex colors are already - // sRGB-encoded, so an extra linear->sRGB conversion would wash the - // overlay out white. - static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) { - switch (fmt) { - case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; - case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; - default: return fmt; - } - } - - static void ensure_rtv(ID3D11Device *device, - IDXGISwapChain *swapchain, - ID3D11RenderTargetView **rtv) - { - if (*rtv || !device || !swapchain) { - return; - } - ID3D11Texture2D *backbuffer = nullptr; - if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) { - return; - } - D3D11_TEXTURE2D_DESC td {}; - backbuffer->GetDesc(&td); - const DXGI_FORMAT view_fmt = to_unorm_view(td.Format); - if (view_fmt != td.Format) { - D3D11_RENDER_TARGET_VIEW_DESC rtvd {}; - rtvd.Format = view_fmt; - rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; - device->CreateRenderTargetView(backbuffer, &rtvd, rtv); - } else { - device->CreateRenderTargetView(backbuffer, nullptr, rtv); - } - backbuffer->Release(); - } - - // bind the backbuffer (lazily creating the RTV) and draw the imgui - // frame on top. reset_invalidate releases *rtv on ResizeBuffers. - void render(ID3D11Device *device, - ID3D11DeviceContext *context, - IDXGISwapChain *swapchain, - ID3D11RenderTargetView **rtv) - { - ensure_rtv(device, swapchain, rtv); - if (!*rtv || !context) { - return; - } - // present happens immediately after, so no need to save the previous - // RT binding (flip-model resets it anyway). - context->OMSetRenderTargets(1, rtv, nullptr); - ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); - } - -} - -// -------------------------------------------------------------------------- -// file-local state + per-frame helpers - -namespace { - -using Present_t = HRESULT(STDMETHODCALLTYPE *)( - IDXGISwapChain *, UINT, UINT); -using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)( - IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT); -using Present1_t = HRESULT(STDMETHODCALLTYPE *)( - IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *); - -Present_t Present_orig = nullptr; -ResizeBuffers_t ResizeBuffers_orig = nullptr; -Present1_t Present1_orig = nullptr; - -bool g_swapchain_hooked = false; -bool g_swapchain1_hooked = false; - -void try_create_overlay(IDXGISwapChain *swapchain) { - if (!swapchain || overlay::OVERLAY) { - return; - } - - DXGI_SWAP_CHAIN_DESC desc {}; - if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) { - return; - } - - // only attach to the main game window; ignore sub-screens / IME helpers. - HWND main = d3d11_hooks::main_hwnd(); - if (main && desc.OutputWindow != main) { - return; - } - - // theme the native title bar; first present is the only reliable point for - // windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll) - set_window_dark_titlebar(desc.OutputWindow); - - ID3D11Device *device = nullptr; - if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) { - return; - } - ID3D11DeviceContext *context = nullptr; - device->GetImmediateContext(&context); - - if (context) { - overlay::create_d3d11(desc.OutputWindow, device, context, swapchain); - RECT cr {}; - ::GetClientRect(desc.OutputWindow, &cr); - log_info("graphics::d3d11", - "attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}", - (uintptr_t) desc.OutputWindow, - desc.BufferDesc.Width, desc.BufferDesc.Height, - cr.right - cr.left, cr.bottom - cr.top); - context->Release(); - } - device->Release(); -} - -// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour). -void poll_screenshot_hotkey() { - static bool s_down = false; - auto buttons = games::get_buttons_overlay(eamuse_get_game()); - const bool pressed = buttons - && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) - && GameAPI::Buttons::getState(RI_MGR, - buttons->at(games::OverlayButtons::Screenshot)); - if (pressed && !s_down) { - graphics_screenshot_trigger(); - } - s_down = pressed; -} - -void pump_overlay(IDXGISwapChain *swapchain) { - if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) { - return; - } - - poll_screenshot_hotkey(); - - // size imgui to the backbuffer (not window client). dxgi may upscale - // a small backbuffer into a larger client rect; without this override - // imgui would draw past the RTV and the mouse mapping would be off. - DXGI_SWAP_CHAIN_DESC desc {}; - if (SUCCEEDED(swapchain->GetDesc(&desc))) { - ImGui_ImplSpice_SetDisplaySizeOverride( - (float) desc.BufferDesc.Width, - (float) desc.BufferDesc.Height); - } - - overlay::OVERLAY->update(); - overlay::OVERLAY->new_frame(); - overlay::OVERLAY->render(); - - // after overlay render so toasts/menus end up in the saved image. - d3d11_hooks::try_screenshot(swapchain); -} - -// ---------------------------------------------------------------------- -// swapchain method hooks - -HRESULT STDMETHODCALLTYPE Present_hook( - IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags) -{ - try_create_overlay(swapchain); - pump_overlay(swapchain); - return Present_orig(swapchain, SyncInterval, Flags); -} - -HRESULT STDMETHODCALLTYPE Present1_hook( - IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags, - const DXGI_PRESENT_PARAMETERS *pParams) -{ - try_create_overlay(swapchain); - pump_overlay(swapchain); - return Present1_orig(swapchain, SyncInterval, Flags, pParams); -} - -HRESULT STDMETHODCALLTYPE ResizeBuffers_hook( - IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height, - DXGI_FORMAT NewFormat, UINT SwapChainFlags) -{ - const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain); - if (ours) { - log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}", - Width, Height, (int32_t) NewFormat); - overlay::OVERLAY->reset_invalidate(); - } - HRESULT res = ResizeBuffers_orig( - swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags); - if (ours && SUCCEEDED(res)) { - overlay::OVERLAY->reset_recreate(); - } - return res; -} - -} // namespace - -// -------------------------------------------------------------------------- -// d3d11_hooks public surface: main-window tracking + vtable install. - -namespace d3d11_hooks { - -namespace { - std::atomic g_main_hwnd { nullptr }; - std::atomic g_ignored_hwnd { nullptr }; -} - -void note_main_hwnd(HWND hwnd) { - if (!hwnd || hwnd == g_ignored_hwnd.load()) { - return; - } - HWND expected = nullptr; - if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) { - log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}", - (uintptr_t) hwnd); - } -} - -HWND main_hwnd() { - return g_main_hwnd.load(); -} - -void ignore_hwnd(HWND hwnd) { - g_ignored_hwnd.store(hwnd); -} - -// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented) -// IDXGISwapChain1::Present1. idempotent; flag is set only after success -// so failed attempts can be retried on the next swapchain. -void install_swapchain_hooks(IDXGISwapChain *swapchain) { - if (!swapchain) { - return; - } - static std::mutex s_hook_mutex; - std::lock_guard lock(s_hook_mutex); - - if (!g_swapchain_hooked) { - const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook, - (void **) &Present_orig, "IDXGISwapChain::Present"); - const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook, - (void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers"); - if (a && b) { - g_swapchain_hooked = true; - } - } - - if (!g_swapchain1_hooked) { - IDXGISwapChain1 *sc1 = nullptr; - if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) { - if (hook_vtbl(sc1, 22, (void *) Present1_hook, - (void **) &Present1_orig, "IDXGISwapChain1::Present1")) { - g_swapchain1_hooked = true; - } - sc1->Release(); - } - } -} - -} - -#endif // SPICE_D3D11 +// dx11 swapchain vtable hooks + per-frame overlay pump. +// +// dxgi shares vtables across swapchain instances, so we only need to patch +// Present / Present1 / ResizeBuffers once on the first instance we see. +// each frame we lazily attach the overlay to whichever swapchain is +// presenting, then drive its imgui update / new_frame / render cycle. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +#include "external/imgui/imgui.h" +#include "external/imgui/backends/imgui_impl_dx11.h" +#include "overlay/imgui/impl_spice.h" + +#include "hooks/graphics/graphics.h" +#include "util/utils.h" + +// -------------------------------------------------------------------------- +// overlay render bridge + +namespace overlay::d3d11 { + + // sRGB backbuffers need a UNORM view: ImGui vertex colors are already + // sRGB-encoded, so an extra linear->sRGB conversion would wash the + // overlay out white. + static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) { + switch (fmt) { + case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; + case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; + default: return fmt; + } + } + + static void ensure_rtv(ID3D11Device *device, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv) + { + if (*rtv || !device || !swapchain) { + return; + } + ID3D11Texture2D *backbuffer = nullptr; + if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) { + return; + } + D3D11_TEXTURE2D_DESC td {}; + backbuffer->GetDesc(&td); + const DXGI_FORMAT view_fmt = to_unorm_view(td.Format); + if (view_fmt != td.Format) { + D3D11_RENDER_TARGET_VIEW_DESC rtvd {}; + rtvd.Format = view_fmt; + rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; + device->CreateRenderTargetView(backbuffer, &rtvd, rtv); + } else { + device->CreateRenderTargetView(backbuffer, nullptr, rtv); + } + backbuffer->Release(); + } + + // bind the backbuffer (lazily creating the RTV) and draw the imgui + // frame on top. reset_invalidate releases *rtv on ResizeBuffers. + void render(ID3D11Device *device, + ID3D11DeviceContext *context, + IDXGISwapChain *swapchain, + ID3D11RenderTargetView **rtv) + { + ensure_rtv(device, swapchain, rtv); + if (!*rtv || !context) { + return; + } + // present happens immediately after, so no need to save the previous + // RT binding (flip-model resets it anyway). + context->OMSetRenderTargets(1, rtv, nullptr); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + } + +} + +// -------------------------------------------------------------------------- +// file-local state + per-frame helpers + +namespace { + +using Present_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain *, UINT, UINT); +using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT); +using Present1_t = HRESULT(STDMETHODCALLTYPE *)( + IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *); + +Present_t Present_orig = nullptr; +ResizeBuffers_t ResizeBuffers_orig = nullptr; +Present1_t Present1_orig = nullptr; + +bool g_swapchain_hooked = false; +bool g_swapchain1_hooked = false; + +// sub-screens / IME helpers are usually child or zero-sized windows. +// visibility isn't checked - the game may present before showing the window. +bool looks_like_game_window(HWND hwnd) { + RECT client {}; + return GetAncestor(hwnd, GA_ROOT) == hwnd + && GetClientRect(hwnd, &client) + && client.right > client.left + && client.bottom > client.top; +} + +// only the main game window; ignore sub-screens / IME helpers. +bool is_main_game_swapchain(IDXGISwapChain *swapchain) { + DXGI_SWAP_CHAIN_DESC desc {}; + if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) { + return false; + } + + HWND main = d3d11_hooks::main_hwnd(); + if (!main) { + // no creation hook recorded a window, so fall back to the presenting one; + // the choice is permanent, so require a plausible game window + if (!looks_like_game_window(desc.OutputWindow)) { + return false; + } + + log_misc( + "graphics::d3d11", + "try to notemain hwnd from swapchain present: 0x{:x}", + (uintptr_t)desc.OutputWindow); + + d3d11_hooks::note_main_hwnd(desc.OutputWindow); + + // it may have been ignored, or another thread may have won the slot + main = d3d11_hooks::main_hwnd(); + } + return desc.OutputWindow == main; +} + +// checks are ordered cheapest first, since this runs on every present +void try_create_overlay(IDXGISwapChain *swapchain) { + if (!swapchain) { + return; + } + + // overlay is disabled by user + if (!overlay::ENABLED) { + return; + } + + // overlay is already enabled and attached + if (overlay::OVERLAY) { + return; + } + + // ignore sub windows + if (!is_main_game_swapchain(swapchain)) { + return; + } + + DXGI_SWAP_CHAIN_DESC desc {}; + if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) { + return; + } + + // theme the native title bar; first present is the only reliable point for + // windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll) + set_window_dark_titlebar(desc.OutputWindow); + + ID3D11Device *device = nullptr; + if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) { + return; + } + ID3D11DeviceContext *context = nullptr; + device->GetImmediateContext(&context); + + if (context) { + overlay::create_d3d11(desc.OutputWindow, device, context, swapchain); + RECT cr {}; + ::GetClientRect(desc.OutputWindow, &cr); + log_info("graphics::d3d11", + "attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}", + (uintptr_t) desc.OutputWindow, + desc.BufferDesc.Width, desc.BufferDesc.Height, + cr.right - cr.left, cr.bottom - cr.top); + context->Release(); + } + device->Release(); +} + +// screenshots have to keep working with the overlay disabled, so they are not gated on it +void pump_frame(IDXGISwapChain *swapchain) { + const bool has_overlay = + overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain); + if (!has_overlay && !is_main_game_swapchain(swapchain)) { + return; + } + + graphics_poll_screenshot_hotkey(); + + // before the overlay render so the screenshot excludes it + if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { + d3d11_hooks::try_screenshot(swapchain); + } + + if (has_overlay) { + + // size imgui to the backbuffer (not window client). dxgi may upscale + // a small backbuffer into a larger client rect; without this override + // imgui would draw past the RTV and the mouse mapping would be off. + DXGI_SWAP_CHAIN_DESC desc {}; + if (SUCCEEDED(swapchain->GetDesc(&desc))) { + ImGui_ImplSpice_SetDisplaySizeOverride( + (float) desc.BufferDesc.Width, + (float) desc.BufferDesc.Height); + } + + overlay::OVERLAY->update(); + overlay::OVERLAY->new_frame(); + overlay::OVERLAY->render(); + } + + // after the overlay render so the screenshot includes toasts / menus + if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { + d3d11_hooks::try_screenshot(swapchain); + } +} + +// ---------------------------------------------------------------------- +// swapchain method hooks + +HRESULT STDMETHODCALLTYPE Present_hook( + IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags) +{ + // a test present doesn't display anything; don't pick a window or take a screenshot off it + if (!(Flags & DXGI_PRESENT_TEST)) { + try_create_overlay(swapchain); + pump_frame(swapchain); + } + return Present_orig(swapchain, SyncInterval, Flags); +} + +HRESULT STDMETHODCALLTYPE Present1_hook( + IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags, + const DXGI_PRESENT_PARAMETERS *pParams) +{ + if (!(Flags & DXGI_PRESENT_TEST)) { + try_create_overlay(swapchain); + pump_frame(swapchain); + } + return Present1_orig(swapchain, SyncInterval, Flags, pParams); +} + +HRESULT STDMETHODCALLTYPE ResizeBuffers_hook( + IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height, + DXGI_FORMAT NewFormat, UINT SwapChainFlags) +{ + const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain); + if (ours) { + log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}", + Width, Height, (int32_t) NewFormat); + overlay::OVERLAY->reset_invalidate(); + } + HRESULT res = ResizeBuffers_orig( + swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags); + if (ours && SUCCEEDED(res)) { + overlay::OVERLAY->reset_recreate(); + } + return res; +} + +} // namespace + +// -------------------------------------------------------------------------- +// d3d11_hooks public surface: main-window tracking + vtable install. + +namespace d3d11_hooks { + +namespace { + std::atomic g_main_hwnd { nullptr }; + std::atomic g_ignored_hwnd { nullptr }; +} + +void note_main_hwnd(HWND hwnd) { + if (!hwnd || hwnd == g_ignored_hwnd.load()) { + return; + } + HWND expected = nullptr; + if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) { + log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}", + (uintptr_t) hwnd); + } +} + +HWND main_hwnd() { + return g_main_hwnd.load(); +} + +void ignore_hwnd(HWND hwnd) { + g_ignored_hwnd.store(hwnd); +} + +// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented) +// IDXGISwapChain1::Present1. idempotent; flag is set only after success +// so failed attempts can be retried on the next swapchain. +void install_swapchain_hooks(IDXGISwapChain *swapchain) { + if (!swapchain) { + return; + } + static std::mutex s_hook_mutex; + std::lock_guard lock(s_hook_mutex); + + if (!g_swapchain_hooked) { + const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook, + (void **) &Present_orig, "IDXGISwapChain::Present"); + const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook, + (void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers"); + if (a && b) { + g_swapchain_hooked = true; + } + } + + if (!g_swapchain1_hooked) { + IDXGISwapChain1 *sc1 = nullptr; + if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) { + if (hook_vtbl(sc1, 22, (void *) Present1_hook, + (void **) &Present1_orig, "IDXGISwapChain1::Present1")) { + g_swapchain1_hooked = true; + } + sc1->Release(); + } + } +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp index 28601c2..c6d5da2 100644 --- a/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp @@ -1,175 +1,175 @@ -// proactive vtable capture for the dx11 backend. -// -// titles under the execexe loader routinely race past our export-level -// trampolines, so the game's first real swapchain never goes through us. -// we sidestep that by creating a throwaway device + swapchain ourselves -// the moment d3d11.dll + dxgi.dll appear, which patches the shared -// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game. - -#include "d3d11_backend.h" - -#ifdef SPICE_D3D11 - -#include -#include - -#include -#include -#include -#include - -#include "d3d11_internal.h" - -using d3d11_hooks::com_ptr; - -namespace { - -using D3D11CreateDevice_t = HRESULT(WINAPI *)( - IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, - const D3D_FEATURE_LEVEL *, UINT, UINT, - ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); -using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); -using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); - -std::atomic g_vtables_captured { false }; - -template -Fn resolve(HMODULE mod, const char *name) { - return reinterpret_cast(GetProcAddress(mod, name)); -} - -com_ptr create_factory2(CreateDXGIFactory2_t f2, - CreateDXGIFactory1_t f1) -{ - IDXGIFactory2 *raw = nullptr; - if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) { - return com_ptr(raw); - } - IDXGIFactory1 *factory1 = nullptr; - if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) { - factory1->QueryInterface(IID_PPV_ARGS(&raw)); - factory1->Release(); - } - return com_ptr(raw); -} - -bool create_dummy_device(D3D11CreateDevice_t create, - com_ptr &device, - com_ptr &context) -{ - static constexpr D3D_FEATURE_LEVEL levels[] = { - D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, - }; - // hardware first, then WARP so headless / unusual configs still work. - for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) { - ID3D11Device *d = nullptr; - ID3D11DeviceContext *c = nullptr; - D3D_FEATURE_LEVEL got; - if (SUCCEEDED(create(nullptr, type, nullptr, 0, - levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, - &d, &got, &c)) && d) { - device.reset(d); - context.reset(c); - return true; - } - } - return false; -} - -} // namespace - -namespace d3d11_hooks { - -// create a throwaway device + swapchain to patch the shared vtables before -// the game's loader races past our export trampolines. safe to call -// repeatedly; runs at most once. -void try_capture_vtables() { - if (g_vtables_captured.load()) { - return; - } - - HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll"); - HMODULE dxgi = GetModuleHandleW(L"dxgi.dll"); - if (!d3d11 || !dxgi) { - return; - } - - auto create_device = resolve(d3d11, "D3D11CreateDevice"); - auto f2 = resolve(dxgi, "CreateDXGIFactory2"); - auto f1 = resolve(dxgi, "CreateDXGIFactory1"); - if (!create_device || (!f1 && !f2)) { - return; - } - - // serialize concurrent calls (poll thread + LDR notification). only - // flip g_vtables_captured after success so failed attempts remain - // retriable on the next tick. - static std::atomic in_progress { false }; - if (in_progress.exchange(true)) { - return; - } - struct scope_clear { - std::atomic &flag; - ~scope_clear() { flag.store(false); } - } clear { in_progress }; - - // hidden message-only window; STATIC is always registered by user32. - HWND dummy_hwnd = CreateWindowExW( - 0, L"STATIC", L"", 0, 0, 0, 1, 1, - HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr); - if (!dummy_hwnd) { - log_warning("graphics::d3d11", - "vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError()); - return; - } - auto destroy_hwnd = std::unique_ptr( - dummy_hwnd, &DestroyWindow); - - // if the game's CreateDXGIFactory_hook already raced us, our - // CreateSwapChainForHwnd call below would trip the hook and try to - // record dummy_hwnd as the main window. block that. - ignore_hwnd(dummy_hwnd); - - auto factory2 = create_factory2(f2, f1); - if (!factory2) { - log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed"); - return; - } - - com_ptr device; - com_ptr context; - if (!create_dummy_device(create_device, device, context)) { - log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed"); - return; - } - - DXGI_SWAP_CHAIN_DESC1 desc {}; - desc.Width = 1; - desc.Height = 1; - desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - desc.SampleDesc.Count = 1; - desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - desc.BufferCount = 2; - desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; - - IDXGISwapChain1 *raw_sc = nullptr; - HRESULT hr = factory2->CreateSwapChainForHwnd( - device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc); - if (FAILED(hr) || !raw_sc) { - log_warning("graphics::d3d11", - "vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr); - return; - } - com_ptr swapchain(raw_sc); - - install_swapchain_hooks(swapchain.get()); - install_factory_hooks(factory2.get()); - - g_vtables_captured.store(true); - log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)"); -} - -} - -#endif // SPICE_D3D11 +// proactive vtable capture for the dx11 backend. +// +// titles under the execexe loader routinely race past our export-level +// trampolines, so the game's first real swapchain never goes through us. +// we sidestep that by creating a throwaway device + swapchain ourselves +// the moment d3d11.dll + dxgi.dll appear, which patches the shared +// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game. + +#include "d3d11_backend.h" + +#ifdef SPICE_D3D11 + +#include +#include + +#include +#include +#include +#include + +#include "d3d11_internal.h" + +using d3d11_hooks::com_ptr; + +namespace { + +using D3D11CreateDevice_t = HRESULT(WINAPI *)( + IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, + const D3D_FEATURE_LEVEL *, UINT, UINT, + ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); +using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); +using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); + +std::atomic g_vtables_captured { false }; + +template +Fn resolve(HMODULE mod, const char *name) { + return reinterpret_cast(GetProcAddress(mod, name)); +} + +com_ptr create_factory2(CreateDXGIFactory2_t f2, + CreateDXGIFactory1_t f1) +{ + IDXGIFactory2 *raw = nullptr; + if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) { + return com_ptr(raw); + } + IDXGIFactory1 *factory1 = nullptr; + if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) { + factory1->QueryInterface(IID_PPV_ARGS(&raw)); + factory1->Release(); + } + return com_ptr(raw); +} + +bool create_dummy_device(D3D11CreateDevice_t create, + com_ptr &device, + com_ptr &context) +{ + static constexpr D3D_FEATURE_LEVEL levels[] = { + D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, + }; + // hardware first, then WARP so headless / unusual configs still work. + for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) { + ID3D11Device *d = nullptr; + ID3D11DeviceContext *c = nullptr; + D3D_FEATURE_LEVEL got; + if (SUCCEEDED(create(nullptr, type, nullptr, 0, + levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, + &d, &got, &c)) && d) { + device.reset(d); + context.reset(c); + return true; + } + } + return false; +} + +} // namespace + +namespace d3d11_hooks { + +// create a throwaway device + swapchain to patch the shared vtables before +// the game's loader races past our export trampolines. safe to call +// repeatedly; runs at most once. +void try_capture_vtables() { + if (g_vtables_captured.load()) { + return; + } + + HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll"); + HMODULE dxgi = GetModuleHandleW(L"dxgi.dll"); + if (!d3d11 || !dxgi) { + return; + } + + auto create_device = resolve(d3d11, "D3D11CreateDevice"); + auto f2 = resolve(dxgi, "CreateDXGIFactory2"); + auto f1 = resolve(dxgi, "CreateDXGIFactory1"); + if (!create_device || (!f1 && !f2)) { + return; + } + + // serialize concurrent calls (poll thread + LDR notification). only + // flip g_vtables_captured after success so failed attempts remain + // retriable on the next tick. + static std::atomic in_progress { false }; + if (in_progress.exchange(true)) { + return; + } + struct scope_clear { + std::atomic &flag; + ~scope_clear() { flag.store(false); } + } clear { in_progress }; + + // hidden message-only window; STATIC is always registered by user32. + HWND dummy_hwnd = CreateWindowExW( + 0, L"STATIC", L"", 0, 0, 0, 1, 1, + HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr); + if (!dummy_hwnd) { + log_warning("graphics::d3d11", + "vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError()); + return; + } + auto destroy_hwnd = std::unique_ptr( + dummy_hwnd, &DestroyWindow); + + // if the game's CreateDXGIFactory_hook already raced us, our + // CreateSwapChainForHwnd call below would trip the hook and try to + // record dummy_hwnd as the main window. block that. + ignore_hwnd(dummy_hwnd); + + auto factory2 = create_factory2(f2, f1); + if (!factory2) { + log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed"); + return; + } + + com_ptr device; + com_ptr context; + if (!create_dummy_device(create_device, device, context)) { + log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed"); + return; + } + + DXGI_SWAP_CHAIN_DESC1 desc {}; + desc.Width = 1; + desc.Height = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + desc.BufferCount = 2; + desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + + IDXGISwapChain1 *raw_sc = nullptr; + HRESULT hr = factory2->CreateSwapChainForHwnd( + device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc); + if (FAILED(hr) || !raw_sc) { + log_warning("graphics::d3d11", + "vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr); + return; + } + com_ptr swapchain(raw_sc); + + install_swapchain_hooks(swapchain.get()); + install_factory_hooks(factory2.get()); + + g_vtables_captured.store(true); + log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)"); +} + +} + +#endif // SPICE_D3D11 diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 17755d1..0ff5452 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -1478,7 +1478,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) { void graphics_d3d9_on_present( HWND hFocusWindow, IDirect3DDevice9 *device, - IDirect3DDevice9 *wrapped_device) { + WrappedIDirect3DDevice9 *wrapped_device) { // image resize / orientation swap. run here (the present path) rather than from `EndScene`, // which may fire several times per frame on multi-pass / render-to-texture games. this is the @@ -1489,6 +1489,13 @@ void graphics_d3d9_on_present( SurfaceHook(device); } + graphics_poll_screenshot_hotkey(); + + // before the overlay render so the screenshot excludes it + if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { + graphics_d3d9_process_screenshot(device, wrapped_device); + } + // Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never // call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with // `dx9osd.dll` for pfreepanic. @@ -1508,6 +1515,15 @@ void graphics_d3d9_on_present( device->EndScene(); } + // after the overlay render so the screenshot includes toasts / menus + if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { + graphics_d3d9_process_screenshot(device, wrapped_device); + } + + // API capture always includes the overlay; it must run before the subscreen present + // below, which leaves the arena SMALL back buffer black + graphics_d3d9_process_capture(device, wrapped_device); + // for IIDX TDJ / SDVX UFC, handle subscreen const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; @@ -1521,9 +1537,6 @@ void graphics_d3d9_on_present( if (is_mfc) { wintouchemu::update(); } - - graphics_d3d9_poll_screenshot_hotkey(); - graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN); } void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h index 7d4f47d..f08b8e9 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h @@ -4,6 +4,8 @@ #include "d3d9_gfdm.h" +struct WrappedIDirect3DDevice9; + // {EEE9CCF6-53D6-4326-9AE5-60921B3DB394} static const GUID IID_WrappedIDirect3D9 = { 0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 } @@ -13,7 +15,7 @@ void graphics_d3d9_init(); void graphics_d3d9_on_present( HWND hFocusWindow, IDirect3DDevice9 *device, - IDirect3DDevice9 *wrapped_device); + WrappedIDirect3DDevice9 *wrapped_device); void graphics_d3d9_notify_subscreen_present(); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index a341663..26bb855 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -17,12 +17,18 @@ #include "d3d9_backend.h" #include "d3d9_live2d.h" +#include "d3d9_readback.h" #include "d3d9_texture.h" #ifndef SPICE64 #include "shaders/vertex_shader.h" #endif +// maps arena's cached additional swap chains (SMALL, LEFT, RIGHT) to screen numbers. +// MAIN is the implicit swap chain, is not in those slots, and is always screen 0. +// screen 1 is the subscreen for every other game, so SMALL takes that number here too. +static constexpr int GFDM_ARENA_SLOT_SCREENS[] { 1, 2, 3 }; + #define CHECK_RESULT_FMT(x, fmt, ...) \ HRESULT __ret = (x); \ if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \ @@ -151,6 +157,8 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() { } } + d3d9_readback::release_device_resources(this->pReal); + if (overlay::ENABLED) { const std::lock_guard lock(overlay::OVERLAY_MUTEX); @@ -336,20 +344,25 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( int index = 0; bool create_swap_chain = false; bool create_fake_swap_chain = false; + bool arena_slot = false; if (avs::game::is_model({"LDJ", "KFC", "M39"})) { create_swap_chain = true; } else if (games::gitadora::is_arena_model() && - (GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) { + (GRAPHICS_SCREENSHOT_SUBSCREENS || + GRAPHICS_PREVENT_SECONDARY_WINDOWS || + GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) { if (pPresentationParameters->BackBufferWidth == 800) { // SMALL (subscreen) create_swap_chain = true; + arena_slot = true; index = 0; } else if (pPresentationParameters->BackBufferWidth == 1080) { // LEFT/RIGHT create_swap_chain = true; + arena_slot = true; index = 1; if (sub_swapchain[index] || fake_sub_swapchain[index]) { index = 2; @@ -361,6 +374,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( } } + // the api lists screens from this registry, so arena heads need their logical numbers in it + if (arena_slot) { + graphics_screens_register(GFDM_ARENA_SLOT_SCREENS[index]); + } + if (create_fake_swap_chain) { if (!fake_sub_swapchain[index]) { log_info( @@ -539,6 +557,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() { return n; } +void WrappedIDirect3DDevice9::get_screenshot_screens(std::vector &screens) const { + if (games::gitadora::is_arena_model()) { + screens.push_back(0); + + // every head the game renders into, whether or not it reaches a display + for (int slot = 0; slot < 3; slot++) { + if (sub_swapchain[slot] != nullptr || fake_sub_swapchain[slot] != nullptr) { + screens.push_back(GFDM_ARENA_SLOT_SCREENS[slot]); + } + } + return; + } + + graphics_screens_get(screens); + + // the sub screen is only registered once the game asks for it by index + if (sub_swapchain[0] != nullptr && + avs::game::is_model({"LDJ", "KFC", "M39"}) && + std::find(screens.begin(), screens.end(), 1) == screens.end()) + { + screens.push_back(1); + } +} + +HRESULT WrappedIDirect3DDevice9::get_screenshot_swap_chain( + UINT iSwapChain, + IDirect3DSwapChain9 **ppSwapChain) +{ + if (ppSwapChain == nullptr) { + return D3DERR_INVALIDCALL; + } + + // the game numbers the two-head SMALL head itself; keep screen 1 meaning SMALL + if (games::gitadora::is_arena_model() && is_gfdm_two_head_exclusive() && iSwapChain == 1) { + return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain); + } + + if (games::gitadora::is_arena_model()) { + for (int slot = 0; slot < 3; slot++) { + if (GFDM_ARENA_SLOT_SCREENS[slot] != (int) iSwapChain) { + continue; + } + if (sub_swapchain[slot] != nullptr) { + sub_swapchain[slot]->AddRef(); + *ppSwapChain = sub_swapchain[slot]; + return D3D_OK; + } + if (fake_sub_swapchain[slot] != nullptr) { + fake_sub_swapchain[slot]->AddRef(); + *ppSwapChain = fake_sub_swapchain[slot]; + return D3D_OK; + } + } + } + + return GetSwapChain(iSwapChain, ppSwapChain); +} + HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset( D3DPRESENT_PARAMETERS *pPresentationParameters) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h index d9637cb..f5b517c 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -234,6 +235,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override; #pragma endregion + // logical screens the game draws, and the swap chain each one lives on + void get_screenshot_screens(std::vector &screens) const; + HRESULT get_screenshot_swap_chain(UINT iSwapChain, IDirect3DSwapChain9 **ppSwapChain); + bool is_gfdm_two_head_exclusive() const; bool is_gfdm_logical_small_swapchain(UINT swapchain) const; bool is_gfdm_logical_side_swapchain(UINT swapchain) const; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h index 1791621..1c4fd3e 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h @@ -4,10 +4,12 @@ #include +#include "games/gitadora/gitadora.h" + inline constexpr UINT GFDM_SIDE_WIDTH = 1080; inline constexpr UINT GFDM_SIDE_HEIGHT = 1920; -inline constexpr UINT GFDM_SMALL_WIDTH = 800; -inline constexpr UINT GFDM_SMALL_HEIGHT = 1280; +inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH; +inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT; inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4; inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.cpp index a5f81d4..0bd8fcb 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.cpp @@ -1,144 +1,144 @@ -#include "d3d9_live2d.h" - -// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation -// is compiled out of 32-bit builds (the header supplies inline no-op stubs there). -#ifdef SPICE64 - -#include -#include - -#include "hooks/graphics/graphics.h" - -// how the Live2D draw filtering works -// ------------------------------------ -// SDVX draws its Live2D characters with a small, fixed set of pixel and -// vertex shaders. to skip those draws (and save GPU) we have to recognise them at -// the exact moment the game issues a draw call. the d3d9 device hooks feed three -// kinds of events into this module: -// -// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader) -// the game compiles its shaders once at load. we can't trust the shader -// *object pointer* to identify a shader (it's just a heap address that -// varies per run and can be recycled), so instead we hash the shader's -// D3D9 *bytecode* - that fingerprint is stable across runs because the -// game ships the same shaders. if the hash matches a known Live2D shader -// we remember that object pointer in g_live2d_shaders. -// -// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader) -// whenever the game binds a shader we look it up in that set once and cache -// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen -// far less often than draws, so this is where the lookup cost lives. -// -// 3. draw call (should_skip_draw, called from every Draw* hook) -// the per-draw question "is this a Live2D draw?" is then just reading those -// two cached bools - no hashing, no map lookups. if the skip is currently -// active (see graphics_sdvx_live2d_should_skip) and either bound shader is -// Live2D, the Draw* hook drops the call instead of forwarding it. -// -// everything is gated on the feature being enabled (mode != Off); when it's Off -// every entry point is a single predicted-not-taken branch. d3d9 rendering for a -// device is single-threaded, so none of this state needs locking. - -namespace { - -// shader state is tracked whenever the feature might act (mode != Off) so the -// known-shader set is populated before a song starts. when Off, every entry -// point is a single cheap branch. -bool tracking_enabled() { - return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off; -} - -// the set of shader objects (pixel or vertex) whose bytecode matched a known -// Live2D fingerprint. only matching shaders are stored, so this stays tiny. -std::unordered_set g_live2d_shaders; - -// whether the currently-bound shaders are known Live2D shaders. cached at set -// time so the per-draw check is just two bool reads. -bool g_cur_ps_is_live2d = false; -bool g_cur_vs_is_live2d = false; - -// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF) -uint64_t bytecode_hash(const DWORD *func) { - if (func == nullptr) { - return 0; - } - const DWORD *p = func; - const DWORD *cap = func + 65536; // safety bound - while (p < cap && *p != 0x0000FFFF) { - p++; - } - const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD); - uint64_t h = 1469598103934665603ULL; - const auto *bytes = reinterpret_cast(func); - for (size_t i = 0; i < n_bytes; i++) { - h ^= bytes[i]; - h *= 1099511628211ULL; - } - return h; -} - -// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable -// across runs because the game ships fixed shaders. the two sets are disjoint so -// a single shader can be classified by its own hash alone. -bool hash_is_live2d(uint64_t hash) { - switch (hash) { - case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song) - case 0x2d7ce428c6b4775dULL: // pixel: masked model draw - case 0x3ce00cc6111c10e7ULL: // pixel: mask generation - case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant) - case 0xe9cf898c331e2a51ULL: // vertex - case 0x94dc84e7b7c0f437ULL: // vertex - case 0xc872937c5cc04309ULL: // vertex - return true; - } - return false; -} - -// classify a shader at creation time and record it if it is Live2D. erasing on a -// miss keeps the set correct if the runtime reuses a freed shader pointer. -void classify_shader(void *shader, const DWORD *func) { - if (hash_is_live2d(bytecode_hash(func))) { - g_live2d_shaders.insert(shader); - } else { - g_live2d_shaders.erase(shader); - } -} - -} // namespace - -namespace d3d9_live2d { - -// stage 1: fingerprint each shader as the game creates it -void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) { - if (tracking_enabled() && shader != nullptr) [[unlikely]] { - classify_shader(shader, func); - } -} - -void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) { - if (tracking_enabled() && shader != nullptr) [[unlikely]] { - classify_shader(shader, func); - } -} - -// stage 2: remember whether the just-bound shader is a Live2D one -void on_set_vertex_shader(IDirect3DVertexShader9 *shader) { - if (tracking_enabled()) [[unlikely]] { - g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0; - } -} - -void on_set_pixel_shader(IDirect3DPixelShader9 *shader) { - if (tracking_enabled()) [[unlikely]] { - g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0; - } -} - -// stage 3: drop the draw if the skip is active and a Live2D shader is bound -bool should_skip_draw() { - return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d); -} - -} // namespace d3d9_live2d - -#endif // SPICE64 +#include "d3d9_live2d.h" + +// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation +// is compiled out of 32-bit builds (the header supplies inline no-op stubs there). +#ifdef SPICE64 + +#include +#include + +#include "hooks/graphics/graphics.h" + +// how the Live2D draw filtering works +// ------------------------------------ +// SDVX draws its Live2D characters with a small, fixed set of pixel and +// vertex shaders. to skip those draws (and save GPU) we have to recognise them at +// the exact moment the game issues a draw call. the d3d9 device hooks feed three +// kinds of events into this module: +// +// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader) +// the game compiles its shaders once at load. we can't trust the shader +// *object pointer* to identify a shader (it's just a heap address that +// varies per run and can be recycled), so instead we hash the shader's +// D3D9 *bytecode* - that fingerprint is stable across runs because the +// game ships the same shaders. if the hash matches a known Live2D shader +// we remember that object pointer in g_live2d_shaders. +// +// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader) +// whenever the game binds a shader we look it up in that set once and cache +// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen +// far less often than draws, so this is where the lookup cost lives. +// +// 3. draw call (should_skip_draw, called from every Draw* hook) +// the per-draw question "is this a Live2D draw?" is then just reading those +// two cached bools - no hashing, no map lookups. if the skip is currently +// active (see graphics_sdvx_live2d_should_skip) and either bound shader is +// Live2D, the Draw* hook drops the call instead of forwarding it. +// +// everything is gated on the feature being enabled (mode != Off); when it's Off +// every entry point is a single predicted-not-taken branch. d3d9 rendering for a +// device is single-threaded, so none of this state needs locking. + +namespace { + +// shader state is tracked whenever the feature might act (mode != Off) so the +// known-shader set is populated before a song starts. when Off, every entry +// point is a single cheap branch. +bool tracking_enabled() { + return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off; +} + +// the set of shader objects (pixel or vertex) whose bytecode matched a known +// Live2D fingerprint. only matching shaders are stored, so this stays tiny. +std::unordered_set g_live2d_shaders; + +// whether the currently-bound shaders are known Live2D shaders. cached at set +// time so the per-draw check is just two bool reads. +bool g_cur_ps_is_live2d = false; +bool g_cur_vs_is_live2d = false; + +// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF) +uint64_t bytecode_hash(const DWORD *func) { + if (func == nullptr) { + return 0; + } + const DWORD *p = func; + const DWORD *cap = func + 65536; // safety bound + while (p < cap && *p != 0x0000FFFF) { + p++; + } + const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD); + uint64_t h = 1469598103934665603ULL; + const auto *bytes = reinterpret_cast(func); + for (size_t i = 0; i < n_bytes; i++) { + h ^= bytes[i]; + h *= 1099511628211ULL; + } + return h; +} + +// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable +// across runs because the game ships fixed shaders. the two sets are disjoint so +// a single shader can be classified by its own hash alone. +bool hash_is_live2d(uint64_t hash) { + switch (hash) { + case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song) + case 0x2d7ce428c6b4775dULL: // pixel: masked model draw + case 0x3ce00cc6111c10e7ULL: // pixel: mask generation + case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant) + case 0xe9cf898c331e2a51ULL: // vertex + case 0x94dc84e7b7c0f437ULL: // vertex + case 0xc872937c5cc04309ULL: // vertex + return true; + } + return false; +} + +// classify a shader at creation time and record it if it is Live2D. erasing on a +// miss keeps the set correct if the runtime reuses a freed shader pointer. +void classify_shader(void *shader, const DWORD *func) { + if (hash_is_live2d(bytecode_hash(func))) { + g_live2d_shaders.insert(shader); + } else { + g_live2d_shaders.erase(shader); + } +} + +} // namespace + +namespace d3d9_live2d { + +// stage 1: fingerprint each shader as the game creates it +void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) { + if (tracking_enabled() && shader != nullptr) [[unlikely]] { + classify_shader(shader, func); + } +} + +void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) { + if (tracking_enabled() && shader != nullptr) [[unlikely]] { + classify_shader(shader, func); + } +} + +// stage 2: remember whether the just-bound shader is a Live2D one +void on_set_vertex_shader(IDirect3DVertexShader9 *shader) { + if (tracking_enabled()) [[unlikely]] { + g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0; + } +} + +void on_set_pixel_shader(IDirect3DPixelShader9 *shader) { + if (tracking_enabled()) [[unlikely]] { + g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0; + } +} + +// stage 3: drop the draw if the skip is active and a Live2D shader is bound +bool should_skip_draw() { + return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d); +} + +} // namespace d3d9_live2d + +#endif // SPICE64 diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.h index b20431f..4760577 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_live2d.h @@ -1,44 +1,44 @@ -#pragma once - -#include -#include - -// SDVX Live2D draw-skip support for the D3D9 backend. -// -// SDVX renders its Live2D navigator / in-song character through a fixed set of -// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip) -// the matching draw calls are dropped to save GPU. shaders are identified by a -// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode -// does not). the hashes were captured with the draw-call fingerprinting tool. -// -// every entry point is a no-op unless the feature is enabled (mode != Off), and -// d3d9 rendering for a device is single-threaded, so none of this needs locking. -namespace d3d9_live2d { - -#ifdef SPICE64 - - // record a shader's bytecode fingerprint at creation time - void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func); - void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func); - - // remember the currently-bound shaders - void on_set_vertex_shader(IDirect3DVertexShader9 *shader); - void on_set_pixel_shader(IDirect3DPixelShader9 *shader); - - // true if the current draw call should be dropped (skip active AND the bound - // shaders identify it as SDVX Live2D) - bool should_skip_draw(); - -#else // !SPICE64 - - // only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point - // compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their - // call sites. - inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {} - inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {} - inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {} - inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {} - inline bool should_skip_draw() { return false; } - -#endif // SPICE64 -} +#pragma once + +#include +#include + +// SDVX Live2D draw-skip support for the D3D9 backend. +// +// SDVX renders its Live2D navigator / in-song character through a fixed set of +// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip) +// the matching draw calls are dropped to save GPU. shaders are identified by a +// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode +// does not). the hashes were captured with the draw-call fingerprinting tool. +// +// every entry point is a no-op unless the feature is enabled (mode != Off), and +// d3d9 rendering for a device is single-threaded, so none of this needs locking. +namespace d3d9_live2d { + +#ifdef SPICE64 + + // record a shader's bytecode fingerprint at creation time + void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func); + void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func); + + // remember the currently-bound shaders + void on_set_vertex_shader(IDirect3DVertexShader9 *shader); + void on_set_pixel_shader(IDirect3DPixelShader9 *shader); + + // true if the current draw call should be dropped (skip active AND the bound + // shaders identify it as SDVX Live2D) + bool should_skip_draw(); + +#else // !SPICE64 + + // only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point + // compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their + // call sites. + inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {} + inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {} + inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {} + inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {} + inline bool should_skip_draw() { return false; } + +#endif // SPICE64 +} diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp new file mode 100644 index 0000000..1f36d77 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.cpp @@ -0,0 +1,241 @@ +#include "d3d9_readback.h" + +#include +#include + +#include "hooks/graphics/graphics.h" +#include "util/logging.h" + +namespace d3d9_readback { + +namespace { + +SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) { + IDirect3DSurface9 *surface = nullptr; + const HRESULT hr = device->CreateOffscreenPlainSurface( + desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &surface, nullptr); + + if (FAILED(hr) || surface == nullptr) { + log_warning("graphics::d3d9", + "failed to create readback surface, hr={}", + FMT_HRESULT(hr)); + return nullptr; + } + + return SurfacePtr(surface); +} + +size_t surface_bytes(const D3DSURFACE_DESC &desc) { + size_t bytes_per_pixel = 4; + switch (desc.Format) { + case D3DFMT_R5G6B5: + case D3DFMT_X1R5G5B5: + case D3DFMT_A1R5G5B5: + bytes_per_pixel = 2; + break; + default: + break; + } + + return static_cast(desc.Width) * desc.Height * bytes_per_pixel; +} + +// idle surfaces are kept between captures, bucketed by layout so that screens of +// differing resolution do not evict each other. a new device drops everything, +// since system memory surfaces outlive Reset but not the device itself +class ReadbackPool { +public: + SurfacePtr acquire(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) { + { + std::lock_guard lock(this->mutex); + + if (this->device != device) { + this->drop(); + this->device = device; + } + + auto *bucket = this->find(desc); + if (bucket && !bucket->idle.empty()) { + auto surface = std::move(bucket->idle.back()); + bucket->idle.pop_back(); + + const size_t bytes = surface_bytes(desc); + this->idle_bytes = this->idle_bytes > bytes ? this->idle_bytes - bytes : 0; + return surface; + } + } + + return create_readback_surface(device, desc); + } + + void release(IDirect3DDevice9 *device, SurfacePtr surface) { + if (!surface) { + return; + } + + D3DSURFACE_DESC desc {}; + if (FAILED(surface->GetDesc(&desc))) { + return; + } + + const size_t bytes = surface_bytes(desc); + + std::lock_guard lock(this->mutex); + if (this->device != device || this->idle_bytes + bytes > MAX_IDLE_BYTES) { + return; + } + + auto *bucket = this->find(desc); + if (bucket == nullptr) { + if (this->buckets.size() >= MAX_BUCKETS) { + return; + } + + this->buckets.push_back(Bucket { desc.Width, desc.Height, desc.Format, {} }); + bucket = &this->buckets.back(); + } + + if (bucket->idle.size() < MAX_IDLE_PER_BUCKET) { + bucket->idle.push_back(std::move(surface)); + this->idle_bytes += bytes; + } + } + + // every cached surface holds a reference on the device, so they have to go + // before it does or the device never reaches a zero reference count + void clear_device(IDirect3DDevice9 *device) { + std::lock_guard lock(this->mutex); + if (this->device != device) { + return; + } + + this->drop(); + this->device = nullptr; + } + +private: + struct Bucket { + UINT width; + UINT height; + D3DFORMAT format; + std::vector idle; + }; + + static constexpr size_t MAX_BUCKETS = GRAPHICS_CAPTURE_SCREEN_NO; + + // one returning surface plus one for the next capture; a full screen surface + // is several megabytes, so the cap matters + static constexpr size_t MAX_IDLE_PER_BUCKET = 2; + + // a 4K surface is 33MB, so the per bucket count alone does not bound this + static constexpr size_t MAX_IDLE_BYTES = 64u * 1024 * 1024; + + void drop() { + this->buckets.clear(); + this->idle_bytes = 0; + } + + Bucket *find(const D3DSURFACE_DESC &desc) { + for (auto &bucket : this->buckets) { + if (bucket.width == desc.Width + && bucket.height == desc.Height + && bucket.format == desc.Format) { + return &bucket; + } + } + + return nullptr; + } + + std::mutex mutex; + std::vector buckets; + IDirect3DDevice9 *device = nullptr; + size_t idle_bytes = 0; +}; + +// deliberately never destroyed: releasing D3D surfaces during static destruction +// would run after d3d9 may already be unloaded +ReadbackPool &pool() { + static ReadbackPool *instance = new ReadbackPool(); + return *instance; +} + +} // namespace + +void release_device_resources(IDirect3DDevice9 *device) { + pool().clear_device(device); +} + +BackbufferCopy::~BackbufferCopy() { + if (this->pooled && this->surface) { + pool().release(this->device, std::move(this->surface)); + } +} + +std::optional acquire_backbuffer_copy( + IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) { + + IDirect3DSurface9 *buffer = nullptr; + HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); + if (FAILED(hr) || buffer == nullptr) { + log_warning("graphics::d3d9", + "failed to get back buffer for screen {}, hr={}", + screen, + FMT_HRESULT(hr)); + return std::nullopt; + } + + D3DSURFACE_DESC desc {}; + hr = buffer->GetDesc(&desc); + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to acquire back buffer descriptor, hr={}", + FMT_HRESULT(hr)); + buffer->Release(); + return std::nullopt; + } + + // GetRenderTargetData rejects multisampled sources. no supported game has been + // seen presenting one, so resolving is left unimplemented rather than untested + if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) { + static std::once_flag warned; + std::call_once(warned, [&desc] { + log_warning("graphics::d3d9", + "back buffer is multisampled ({}), screenshots and capture are unsupported", + static_cast(desc.MultiSampleType)); + }); + buffer->Release(); + return std::nullopt; + } + + auto destination = pooled + ? pool().acquire(device, desc) + : create_readback_surface(device, desc); + if (!destination) { + buffer->Release(); + return std::nullopt; + } + + hr = device->GetRenderTargetData(buffer, destination.get()); + buffer->Release(); + + if (FAILED(hr)) { + log_warning("graphics::d3d9", + "failed to copy back buffer contents, hr={}", + FMT_HRESULT(hr)); + if (pooled) { + pool().release(device, std::move(destination)); + } + return std::nullopt; + } + + BackbufferCopy copy; + copy.screen = screen; + copy.desc = desc; + copy.device = device; + copy.surface = std::move(destination); + copy.pooled = pooled; + + return copy; +} +} diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h new file mode 100644 index 0000000..d49e595 --- /dev/null +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_readback.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include + +#include + +namespace d3d9_readback { + + struct SurfaceReleaser { + void operator()(IDirect3DSurface9 *surface) const { + surface->Release(); + } + }; + + using SurfacePtr = std::unique_ptr; + + // system memory copy of a back buffer; locking it neither stalls the GPU nor reads over PCIe + struct BackbufferCopy { + int screen {}; + D3DSURFACE_DESC desc {}; + IDirect3DDevice9 *device = nullptr; + SurfacePtr surface; + bool pooled = false; + + BackbufferCopy() = default; + BackbufferCopy(BackbufferCopy &&) noexcept = default; + BackbufferCopy &operator=(BackbufferCopy &&) noexcept = default; + BackbufferCopy(const BackbufferCopy &) = delete; + BackbufferCopy &operator=(const BackbufferCopy &) = delete; + ~BackbufferCopy(); + }; + + // pooled copies reuse surfaces across calls and return them once the copy is destroyed, + // so the caller must keep it alive for as long as the pixels are being read + std::optional acquire_backbuffer_copy( + IDirect3DDevice9 *device, + IDirect3DSwapChain9 *swap_chain, + int screen, + bool pooled); + + // pooled surfaces hold references on the device; call this before releasing it + void release_device_resources(IDirect3DDevice9 *device); +} diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp index 4b53643..e23f09c 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -1,44 +1,34 @@ #include "d3d9_screenshot.h" #include +#include +#include +#include +#include #include +#include #include #include #include +#include #include - -#ifdef __GNUC__ -#include -#endif +#include #include "avs/game.h" -#include "games/io.h" #include "hooks/graphics/graphics.h" -#include "launcher/launcher.h" #include "misc/clipboard.h" -#include "misc/eamuse.h" #include "overlay/notifications.h" -#include "overlay/overlay.h" #include "util/fileutils.h" -#include "util/libutils.h" #include "util/logging.h" #include "util/threadpool.h" -#ifdef __GNUC__ -typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t; -#else -#define D3DXIFF_PNG ((DWORD) 3) +#include "d3d9_device.h" +#include "d3d9_readback.h" -typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)( - LPCSTR pDestFile, - DWORD DestFormat, - LPDIRECT3DSURFACE9 pSrcSurface, - CONST PALETTEENTRY *pSrcPalette, - CONST RECT *pSrcRect); -#endif - -static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false; +// genpath picks filenames by probing the disk, so the whole save has to be serialised: +// a name is only taken once its file exists, not when genpath hands it out +static std::mutex SCREENSHOT_SAVE_M; namespace { @@ -52,55 +42,146 @@ struct ImageRequest { int screen; }; -struct SurfaceReleaser { - void operator()(IDirect3DSurface9 *surface) const { - surface->Release(); +// a screen already read out of its surface, so nothing here touches D3D. the bytes +// are still in the surface's format; converting them is left to the encode +struct PendingWrite { + int screen {}; + D3DFORMAT format {}; + UINT width {}; + UINT height {}; + size_t pitch {}; + std::vector data; + std::string path; + bool saved = false; +}; + +struct PendingCapture { + int screen {}; + D3DFORMAT format {}; + UINT width {}; + UINT height {}; + size_t pitch {}; + std::vector data; +}; + +// packed 24bpp RGB, what both the png encoder and the api capture consume +constexpr size_t RGB_PIXEL_SIZE = 3; + +// the formats surface_to_rgb knows how to convert; the two must stay in sync +static std::optional surface_pixel_size(D3DFORMAT format) { + switch (format) { + // what back buffers are actually created as in practice + case D3DFMT_X8R8G8B8: + case D3DFMT_A8R8G8B8: + + // a valid display format, but no supported game has been seen presenting one + case D3DFMT_A2R10G10B10: + return 4; + + // valid display formats, but no supported game has been seen presenting one + case D3DFMT_R5G6B5: + case D3DFMT_X1R5G5B5: + case D3DFMT_A1R5G5B5: + return 2; + + default: + return std::nullopt; } +} + +struct ImageSize { + size_t row_size {}; + size_t total_size {}; }; -using SurfacePtr = std::unique_ptr; +static std::optional compute_image_size( + UINT width, + UINT height, + size_t bytes_per_pixel) { -struct BackbufferCopy { - D3DSURFACE_DESC desc {}; - SurfacePtr surface; + if (width == 0 || height == 0 + || width > std::numeric_limits::max() / bytes_per_pixel) { + return std::nullopt; + } + + const size_t row_size = static_cast(width) * bytes_per_pixel; + if (height > std::numeric_limits::max() / row_size) { + return std::nullopt; + } + + return ImageSize { row_size, static_cast(height) * row_size }; +} + +static bool resize_pixels(std::vector &pixels, size_t size) { + try { + pixels.resize(size); + return true; + } catch (const std::exception &error) { + log_warning("graphics::d3d9", "failed to allocate image buffer: {}", error.what()); + return false; + } +} + +// the api capture stages a whole back buffer every frame, so the staging buffer +// is recycled rather than reallocated. returned buffers keep their size, which +// leaves the reuse free of a zero fill +class CaptureBuffers { +public: + std::vector take() { + std::lock_guard lock(this->mutex); + if (this->idle.empty()) { + return {}; + } + + auto buffer = std::move(this->idle.back()); + this->idle.pop_back(); + return buffer; + } + + void give(std::vector buffer) { + std::lock_guard lock(this->mutex); + if (this->idle.size() < MAX_IDLE) { + this->idle.push_back(std::move(buffer)); + } + } + +private: + // one per save in flight plus one for the next capture; a full screen is + // several megabytes, so the cap matters + static constexpr size_t MAX_IDLE = 2; + + std::mutex mutex; + std::vector> idle; }; -} // namespace +// deliberately never destroyed, so a save still running at process exit cannot +// hand a buffer back to a dead free list +CaptureBuffers &capture_buffers() { + static CaptureBuffers *instance = new CaptureBuffers(); + return *instance; +} -static void save_capture( - int screen, +// encodes get their own pool: the dispatch below already occupies a worker on its +// pool, so queueing onto that one and waiting could starve itself. never destroyed +// for the same reason as the buffers above +ThreadPool &encode_pool() { + static auto *instance = new ThreadPool(2); + return *instance; +} + +// normalize the supported D3D formats to packed 24bpp RGB. callers screen the +// format through surface_pixel_size first, so the black fill below is a fallback +void surface_to_rgb( D3DFORMAT format, UINT width, UINT height, - IDirect3DSurface9 *surface) { - HRESULT hr; + const uint8_t *data, + size_t pitch, + uint8_t *pixels) { - // lock surface to be able to access the data - D3DLOCKED_RECT finished_copy {}; - hr = surface->LockRect(&finished_copy, nullptr, 0); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); - graphics_capture_skip(screen); - return; - } - - // normalize supported D3D formats to packed RGB for API capture - size_t pitch = finished_copy.Pitch; - auto data = reinterpret_cast(finished_copy.pBits); - auto pixels = std::unique_ptr(new uint8_t[width * height * 3]); for (size_t row = 0; row < height; row++) { size_t offset_row = row * width * 3; switch (format) { - case D3DFMT_R8G8B8: { - for (size_t column = 0; column < width; column++) { - auto cell = data + row * pitch + column * 3; - auto pixel = &pixels[offset_row + column * 3]; - pixel[0] = cell[0]; - pixel[1] = cell[1]; - pixel[2] = cell[2]; - } - break; - } case D3DFMT_X8R8G8B8: case D3DFMT_A8R8G8B8: { for (size_t column = 0; column < width; column++) { @@ -112,14 +193,45 @@ static void save_capture( } break; } - case D3DFMT_X8B8G8R8: - case D3DFMT_A8B8G8R8: { + // the 5 and 6 bit channels are widened by bit replication so that + // full scale stays full scale + case D3DFMT_R5G6B5: { + auto cells = reinterpret_cast(data + row * pitch); for (size_t column = 0; column < width; column++) { - auto cell = data + row * pitch + column * 4; + const uint16_t cell = cells[column]; + const uint8_t red = (cell >> 11) & 0x1F; + const uint8_t green = (cell >> 5) & 0x3F; + const uint8_t blue = cell & 0x1F; auto pixel = &pixels[offset_row + column * 3]; - pixel[0] = cell[0]; - pixel[1] = cell[1]; - pixel[2] = cell[2]; + pixel[0] = (red << 3) | (red >> 2); + pixel[1] = (green << 2) | (green >> 4); + pixel[2] = (blue << 3) | (blue >> 2); + } + break; + } + case D3DFMT_X1R5G5B5: + case D3DFMT_A1R5G5B5: { + auto cells = reinterpret_cast(data + row * pitch); + for (size_t column = 0; column < width; column++) { + const uint16_t cell = cells[column]; + const uint8_t red = (cell >> 10) & 0x1F; + const uint8_t green = (cell >> 5) & 0x1F; + const uint8_t blue = cell & 0x1F; + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = (red << 3) | (red >> 2); + pixel[1] = (green << 3) | (green >> 2); + pixel[2] = (blue << 3) | (blue >> 2); + } + break; + } + case D3DFMT_A2R10G10B10: { + auto cells = reinterpret_cast(data + row * pitch); + for (size_t column = 0; column < width; column++) { + const uint32_t cell = cells[column]; + auto pixel = &pixels[offset_row + column * 3]; + pixel[0] = static_cast((cell >> 22) & 0xFF); + pixel[1] = static_cast((cell >> 12) & 0xFF); + pixel[2] = static_cast((cell >> 2) & 0xFF); } break; } @@ -133,220 +245,169 @@ static void save_capture( } } } +} - // unlock surface - hr = surface->UnlockRect(); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); - graphics_capture_skip(screen); +} // namespace + +using d3d9_readback::BackbufferCopy; + +static void save_capture(PendingCapture capture) { + const auto size = compute_image_size(capture.width, capture.height, RGB_PIXEL_SIZE); + if (!size.has_value()) { + capture_buffers().give(std::move(capture.data)); + graphics_capture_skip(capture.screen); return; } - // enqueue - graphics_capture_enqueue(screen, pixels.release(), width, height); + auto pixels = std::unique_ptr(new (std::nothrow) uint8_t[size->total_size]); + if (!pixels) { + log_warning("graphics::d3d9", "failed to allocate capture image buffer"); + capture_buffers().give(std::move(capture.data)); + graphics_capture_skip(capture.screen); + return; + } + + // a format we cannot read still has to produce a frame, or api clients stall + if (capture.data.empty()) { + std::memset(pixels.get(), 0, size->total_size); + } else { + surface_to_rgb( + capture.format, + capture.width, + capture.height, + capture.data.data(), + capture.pitch, + pixels.get()); + + capture_buffers().give(std::move(capture.data)); + } + + graphics_capture_enqueue(capture.screen, pixels.release(), capture.width, capture.height); } -static void save_screenshot( +enum class SurfaceRead { + Ok, + Unsupported, + Failed, +}; + +// copying the surface touches D3D, so it stays on the caller's thread. the bytes come +// out in the surface's own format; converting them is plain memory work for later +static SurfaceRead read_surface_raw( + const BackbufferCopy ©, + size_t &row_size, + std::vector &out) { + + const auto bytes_per_pixel = surface_pixel_size(copy.desc.Format); + if (!bytes_per_pixel.has_value()) { + static std::once_flag warned; + std::call_once(warned, [©] { + log_warning("graphics::d3d9", + "unsupported surface format {}", + static_cast(copy.desc.Format)); + }); + return SurfaceRead::Unsupported; + } + + const auto size = compute_image_size(copy.desc.Width, copy.desc.Height, *bytes_per_pixel); + if (!size.has_value() || !resize_pixels(out, size->total_size)) { + return SurfaceRead::Failed; + } + + D3DLOCKED_RECT locked {}; + HRESULT hr = copy.surface->LockRect(&locked, nullptr, D3DLOCK_READONLY); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to lock capture surface, hr={}", FMT_HRESULT(hr)); + return SurfaceRead::Failed; + } + + if (locked.Pitch < 0 || static_cast(locked.Pitch) < size->row_size) { + log_warning("graphics::d3d9", "capture surface has invalid pitch {}", locked.Pitch); + copy.surface->UnlockRect(); + return SurfaceRead::Failed; + } + + auto data = reinterpret_cast(locked.pBits); + for (size_t row = 0; row < copy.desc.Height; row++) { + std::memcpy( + out.data() + row * size->row_size, + data + row * locked.Pitch, + size->row_size); + } + + hr = copy.surface->UnlockRect(); + if (FAILED(hr)) { + log_warning("graphics::d3d9", "failed to unlock capture surface, hr={}", FMT_HRESULT(hr)); + return SurfaceRead::Failed; + } + + row_size = size->row_size; + return SurfaceRead::Ok; +} + +static bool read_capture_surface( + const BackbufferCopy ©, + PendingCapture &capture) { + + capture.screen = copy.screen; + capture.format = copy.desc.Format; + capture.width = copy.desc.Width; + capture.height = copy.desc.Height; + + capture.data = capture_buffers().take(); + const auto result = read_surface_raw(copy, capture.pitch, capture.data); + if (result == SurfaceRead::Ok) { + return true; + } + + capture_buffers().give(std::move(capture.data)); + capture.data.clear(); + + // a format we cannot read is reported as a black frame rather than nothing, + // so a client polling the api keeps getting responses + return result == SurfaceRead::Unsupported; +} + +static bool write_screenshot_png( const std::string &file_path, - D3DFORMAT format, UINT width, UINT height, - IDirect3DSurface9 *surface) { - // 32-bit XRGB and ARGB surfaces use byte 3 as alpha; force opaque PNG output - if (format == D3DFMT_X8R8G8B8 || format == D3DFMT_A8R8G8B8 || - format == D3DFMT_X8B8G8R8 || format == D3DFMT_A8B8G8R8) { + const std::vector &pixels) { - D3DLOCKED_RECT finished_copy {}; - HRESULT hr = surface->LockRect(&finished_copy, nullptr, 0); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; - } + // a no-op while FPNG_NO_SSE is set, but fpng requires it before any encode + static std::once_flag fpng_ready; + std::call_once(fpng_ready, [] { fpng::fpng_init(); }); - const size_t pitch = finished_copy.Pitch; - auto data = reinterpret_cast(finished_copy.pBits); - for (size_t row = 0; row < height; row++) { - for (size_t column = 0; column < width; column++) { - data[row * pitch + column * 4 + 3] = 255; - } - } + log_info("graphics::d3d9", "saving screenshot to {}", file_path); - hr = surface->UnlockRect(); - if (FAILED(hr)) { - log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; - } + if (!fpng::fpng_encode_image_to_file( + file_path.c_str(), + pixels.data(), + static_cast(width), + static_cast(height), + 3)) { + log_warning("graphics::d3d9", "failed to write screenshot png"); + return false; } - // lazy load function - static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr; - if (D3DXSaveSurfaceToFileA_ptr == nullptr) { - D3DXSaveSurfaceToFileA_ptr = libutils::try_proc("D3DXSaveSurfaceToFileA"); - - // check if function was not found, likely because d3dx9 is not loaded - if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) { - ATTEMPTED_D3DX9_LOAD_LIBRARY = true; - - // prefer the newest installed helper while supporting older D3DX9 runtimes - for (size_t i = 43; i >= 24; i--) { - auto lib_name = fmt::format("d3dx9_{}.dll", i); - auto d3dx9 = libutils::try_library(lib_name); - - // Check if library was not found - if (d3dx9 == nullptr) { - continue; - } - - D3DXSaveSurfaceToFileA_ptr = libutils::try_proc( - d3dx9, "D3DXSaveSurfaceToFileA"); - - // Check if function was not found - if (D3DXSaveSurfaceToFileA_ptr == nullptr) { - FreeLibrary(d3dx9); - d3dx9 = nullptr; - - continue; - } - - log_info("graphics::d3d9", "found surface save function in '{}'", lib_name); - break; - } - } - } - - if (D3DXSaveSurfaceToFileA_ptr != nullptr) { - - // save to file - log_info("graphics::d3d9", "saving screenshot to {}", file_path); - const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr( - file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr); - - if (FAILED(save_result)) { - log_warning("graphics::d3d9", "Failed to save screenshot"); - overlay::notifications::add( - overlay::notifications::Severity::Error, - "Screenshot failed to save"); - return; - } - - // save to clipboard - clipboard::copy_image(file_path); - - overlay::notifications::add( - overlay::notifications::Severity::Success, - fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); - } else { - log_warning("graphics::d3d9", "Direct3D save helper function not available"); - } + return true; } -void graphics_d3d9_poll_screenshot_hotkey() { - static bool trigger_last = false; - auto buttons = games::get_buttons_overlay(eamuse_get_game()); - if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) && - GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot))) - { - if (!trigger_last) { - graphics_screenshot_trigger(); - } - trigger_last = true; - } else { - trigger_last = false; +// screen 0 keeps the plain name so existing tooling and the clipboard copy are unaffected +static std::string screenshot_path_for_screen(const std::string &primary_path, int screen) { + if (screen == 0) { + return primary_path; } + + const std::filesystem::path path(primary_path); + return (path.parent_path() / + fmt::format("{}_{}{}", path.stem().string(), screen, path.extension().string())) + .string(); } -static std::optional acquire_backbuffer_copy( - IDirect3DDevice9 *device, IDirect3DSwapChain9 *sub_swap_chain, int screen) { - - HRESULT hr = S_OK; - - // TODO: verify screen is a valid swapchain - - IDirect3DSurface9 *buffer = nullptr; - if (sub_swap_chain != nullptr && screen & 1) { - hr = sub_swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } else { - hr = device->GetBackBuffer(screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } - if (FAILED(hr) || buffer == nullptr) { - log_warning("graphics::d3d9", - "failed to get back buffer, hr={}", - FMT_HRESULT(hr)); - return std::nullopt; - } - - D3DSURFACE_DESC desc {}; - hr = buffer->GetDesc(&desc); - if (FAILED(hr)) { - log_warning("graphics::d3d9", - "failed to acquire back buffer descriptor, hr={}", - FMT_HRESULT(hr)); - buffer->Release(); - return std::nullopt; - } - - // TODO: cache render targets - IDirect3DSurface9 *temp_surface = nullptr; - hr = device->CreateRenderTarget( - desc.Width, desc.Height, desc.Format, desc.MultiSampleType, - desc.MultiSampleQuality, TRUE, &temp_surface, nullptr); - if (FAILED(hr) || temp_surface == nullptr) { - log_warning("graphics::d3d9", - "failed to acquire temporary surface, hr={}", - FMT_HRESULT(hr)); - buffer->Release(); - return std::nullopt; - } - - hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE); - if (FAILED(hr)) { - log_warning("graphics::d3d9", - "failed to copy back buffer contents, hr={}", - FMT_HRESULT(hr)); - temp_surface->Release(); - buffer->Release(); - return std::nullopt; - } - - // release original back buffer reference - buffer->Release(); - - return BackbufferCopy { - .desc = desc, - .surface = SurfacePtr(temp_surface), - }; -} - -static void dispatch_surface_save( - const ImageRequest &request, - BackbufferCopy copy) { - auto surface_process = [request, copy = std::move(copy)]() { - switch (request.kind) { - case ImageRequestKind::Capture: - save_capture( - request.screen, - copy.desc.Format, - copy.desc.Width, - copy.desc.Height, - copy.surface.get()); - break; - - case ImageRequestKind::Screenshot: { - auto file_path = graphics_screenshot_genpath(); - if (!file_path.empty()) { - save_screenshot( - file_path, - copy.desc.Format, - copy.desc.Width, - copy.desc.Height, - copy.surface.get()); - } - break; - } - } - }; - - // list of games that crash when running the screenshot processor on another thread +// games that crash or hang when the screenshot processor runs on another thread. +// D3DCREATE_MULTITHREADED is not a predictor of this; MDX omits it and threads fine +static bool image_processing_must_be_inline() { static const robin_hood::unordered_set THREAD_BAN { "JMA", #ifndef SPICE64 @@ -358,52 +419,263 @@ static void dispatch_surface_save( "LMA", }; - // run the save operation on another thread for supported games - if (THREAD_BAN.contains(avs::game::MODEL)) { - surface_process(); + return THREAD_BAN.contains(avs::game::MODEL); +} + +static void dispatch_capture_save(PendingCapture capture) { + auto capture_process = [capture = std::move(capture)]() mutable { + // an escape from here would cross a thread boundary and terminate + try { + save_capture(std::move(capture)); + } catch (const std::exception &error) { + log_warning("graphics::d3d9", "capture save failed: {}", error.what()); + } catch (...) { + log_warning("graphics::d3d9", "capture save failed"); + } + }; + + if (image_processing_must_be_inline()) { + capture_process(); } else { static auto pool = ThreadPool(2); - pool.add(std::move(surface_process)); + pool.add(std::move(capture_process)); } } -static std::optional consume_image_request() { +// by this point the pixels are plain memory, so none of this needs the device +static void dispatch_screenshot_save(std::vector writes, size_t screen_count) { + auto screenshot_process = [writes = std::move(writes), screen_count]() mutable { + std::lock_guard lock(SCREENSHOT_SAVE_M); + + std::vector screens; + screens.reserve(writes.size()); + for (const auto &write : writes) { + screens.push_back(write.screen); + } + + const auto base_path = graphics_screenshot_genpath(screens); + if (base_path.empty()) { + return; + } + + for (auto &write : writes) { + write.path = screenshot_path_for_screen(base_path, write.screen); + } + + // screens missing from writes either failed to be acquired or failed to read + size_t failed = screen_count - writes.size(); + + // a throw here would otherwise reach a thread boundary and terminate + auto encode_one = [](PendingWrite &write) { + try { + const auto rgb = compute_image_size(write.width, write.height, RGB_PIXEL_SIZE); + std::vector pixels; + if (!rgb.has_value() || !resize_pixels(pixels, rgb->total_size)) { + write.saved = false; + return; + } + + surface_to_rgb( + write.format, + write.width, + write.height, + write.data.data(), + write.pitch, + pixels.data()); + + // the encode below is the long part; the raw copy is dead by now + write.data.clear(); + write.data.shrink_to_fit(); + + write.saved = write_screenshot_png( + write.path, write.width, write.height, pixels); + } catch (const std::exception &error) { + log_warning("graphics::d3d9", + "screenshot encode failed for {}: {}", write.path, error.what()); + write.saved = false; + } catch (...) { + log_warning("graphics::d3d9", + "screenshot encode failed for {}", write.path); + write.saved = false; + } + }; + + { + // sized up front and assigned by index: storing a future must not be able + // to throw once its task is queued, or the screen would encode twice + std::vector> pending(writes.empty() ? 0 : writes.size() - 1); + for (size_t i = 1; i < writes.size(); i++) { + try { + pending[i - 1] = encode_pool().add([&writes, &encode_one, i] { + encode_one(writes[i]); + }); + } catch (const std::exception &) { + // nothing to queue onto; encoding it here still makes progress + encode_one(writes[i]); + } + } + + if (!writes.empty()) { + encode_one(writes.front()); + } + + for (auto &task : pending) { + if (task.valid()) { + task.wait(); + } + } + } + + std::string primary_path; + std::string notify_path; + for (const auto &write : writes) { + if (!write.saved) { + failed++; + continue; + } + if (notify_path.empty()) { + notify_path = write.path; + } + if (write.screen == 0) { + primary_path = write.path; + } + } + + // only the primary screen goes to the clipboard, but any saved file is a success + if (!primary_path.empty()) { + clipboard::copy_image(primary_path); + } + if (!notify_path.empty()) { + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Screenshot saved: {}", fileutils::basename(notify_path))); + } else { + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to save"); + } + + if (failed > 0) { + log_warning("graphics::d3d9", "{} screenshot screen(s) missing", failed); + } + }; + + // genpath and the path building below allocate, so an escape from here would + // cross a thread boundary and terminate + auto guarded = [process = std::move(screenshot_process)]() mutable { + try { + process(); + } catch (const std::exception &error) { + log_warning("graphics::d3d9", "screenshot save failed: {}", error.what()); + } catch (...) { + log_warning("graphics::d3d9", "screenshot save failed"); + } + }; + + if (image_processing_must_be_inline()) { + guarded(); + } else { + static auto pool = ThreadPool(2); + pool.add(std::move(guarded)); + } +} + +static void process_image_request( + IDirect3DDevice9 *device, + WrappedIDirect3DDevice9 *wrapped_device, + const ImageRequest &request) { + const bool screenshot = request.kind == ImageRequestKind::Screenshot; + + std::vector screens { request.screen }; + if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) { + screens.clear(); + wrapped_device->get_screenshot_screens(screens); + } + + std::vector copies; + copies.reserve(screens.size()); + for (const int screen : screens) { + std::optional copy; + + IDirect3DSwapChain9 *swap_chain = nullptr; + HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain); + if (FAILED(hr) || swap_chain == nullptr) { + log_warning("graphics::d3d9", + "failed to get swap chain for screen {}, hr={}", + screen, + FMT_HRESULT(hr)); + } else { + // only the API capture path runs often enough to benefit from pooling + copy = d3d9_readback::acquire_backbuffer_copy(device, swap_chain, screen, !screenshot); + swap_chain->Release(); + } + + if (copy.has_value()) { + copies.emplace_back(std::move(*copy)); + } else if (!screenshot) { + graphics_capture_skip(request.screen); + return; + } + } + + if (copies.empty()) { + return; + } + + if (!screenshot) { + PendingCapture capture; + if (!read_capture_surface(copies.front(), capture)) { + graphics_capture_skip(request.screen); + return; + } + + copies.clear(); + dispatch_capture_save(std::move(capture)); + return; + } + + // reading a surface touches the device, and doing that off the present thread + // has been seen to deadlock games whose device has no internal locking + std::vector writes; + writes.reserve(copies.size()); + for (const auto © : copies) { + PendingWrite write; + write.screen = copy.screen; + write.format = copy.desc.Format; + write.width = copy.desc.Width; + write.height = copy.desc.Height; + + if (read_surface_raw(copy, write.pitch, write.data) != SurfaceRead::Ok) { + continue; + } + + writes.push_back(std::move(write)); + } + + copies.clear(); + + dispatch_screenshot_save(std::move(writes), screens.size()); +} + +void graphics_d3d9_process_screenshot( + IDirect3DDevice9 *device, + WrappedIDirect3DDevice9 *wrapped_device) { if (graphics_screenshot_consume()) { - return ImageRequest { + process_image_request(device, wrapped_device, ImageRequest { .kind = ImageRequestKind::Screenshot, .screen = 0, - }; + }); } - - int capture_screen = 0; - if (graphics_capture_consume(&capture_screen)) { - return ImageRequest { - .kind = ImageRequestKind::Capture, - .screen = capture_screen, - }; - } - - return std::nullopt; } -void graphics_d3d9_process_screenshot_and_capture( +void graphics_d3d9_process_capture( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain) { - const auto request = consume_image_request(); - if (!request.has_value()) { - return; + WrappedIDirect3DDevice9 *wrapped_device) { + int screen = 0; + if (graphics_capture_consume(&screen)) { + process_image_request(device, wrapped_device, ImageRequest { + .kind = ImageRequestKind::Capture, + .screen = screen, + }); } - - auto copy = acquire_backbuffer_copy( - device, - sub_swap_chain, - request->screen); - if (!copy.has_value()) { - if (request->kind == ImageRequestKind::Capture) { - graphics_capture_skip(request->screen); - } - return; - } - - dispatch_surface_save(*request, std::move(*copy)); } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h index efa9096..d539c5e 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h @@ -2,8 +2,12 @@ #include -void graphics_d3d9_poll_screenshot_hotkey(); +struct WrappedIDirect3DDevice9; -void graphics_d3d9_process_screenshot_and_capture( +void graphics_d3d9_process_screenshot( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain); + WrappedIDirect3DDevice9 *wrapped_device); + +void graphics_d3d9_process_capture( + IDirect3DDevice9 *device, + WrappedIDirect3DDevice9 *wrapped_device); diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 7e91ad4..6aa5866 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -17,11 +17,14 @@ #include "games/ddr/ddr.h" #include "games/gitadora/gitadora.h" #include "games/iidx/iidx.h" +#include "games/io.h" #include "games/sdvx/sdvx.h" #include "games/popn/popn.h" +#include "hooks/graphics/jpeg_encoder.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "hooks/graphics/backends/d3d11/d3d11_backend.h" #include "launcher/shutdown.h" +#include "misc/hotkeys.h" #include "overlay/overlay.h" #include "touch/touch.h" #include "touch/touch_gestures.h" @@ -31,6 +34,7 @@ #include "util/utils.h" #include "misc/wintouchemu.h" #include "touch/native/inject.h" +#include "touch/native/nativetouchhook.h" #include "util/time.h" #include "rawinput/rawinput.h" @@ -61,7 +65,6 @@ static bool GRAPHICS_SCREENSHOT_TRIGGER = false; static std::set GRAPHICS_SCREENS { 0 }; static std::mutex GRAPHICS_SCREENS_M {}; static std::vector GRAPHICS_CAPTURE_SCREENS; -static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4; static std::mutex GRAPHICS_CAPTURE_SCREENS_M {}; static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; @@ -116,6 +119,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0; // settings std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146"; std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots"; +bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = false; +bool GRAPHICS_SCREENSHOT_SUBSCREENS = false; static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr; static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr; @@ -212,6 +217,18 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name) } else if (window_name == "SMALL") { GFDM_SUBSCREEN_WINDOW = hWnd; } + + // touch belongs to the SMALL panel when it exists, otherwise to the main window + // that draws the subscreen overlay + const bool hosts_touch = window_name == "SMALL" || + (window_name == "GITADORA" && !graphics_gitadora_has_dedicated_subscreen()); + if (nativetouch::is_hooked() && hWnd != nullptr && hosts_touch) { + nativetouch::inject::set_preferred_injection_window(hWnd); + } +} + +bool graphics_gitadora_has_dedicated_subscreen() { + return GFDM_SUBSCREEN_WINDOW != nullptr; } bool graphics_gitadora_prepare_two_head_device_window( @@ -612,8 +629,10 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC } } + const bool is_sdvx = avs::game::is_model("KFC"); bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub"); - bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen"); + bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen"); + bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); const std::string gfdm_window_name = games::gitadora::is_arena_model() ? gitadora_canonical_window_name(effective_window_name) @@ -716,6 +735,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW); } + // SDVX registers touch on both windows, so name the one synthetic touches must land on + // instead of letting window creation order decide: the sub screen window when windowed, + // the main window in fullscreen since the game reads it in primary-display coordinates + if (nativetouch::is_hooked() && + result != nullptr && + (GRAPHICS_WINDOWED ? is_sdvx_sub_window : is_sdvx_main_window)) { + log_misc( + "graphics", + "SDVX touch surface is {}, {}", + fmt::ptr(result), + window_name); + nativetouch::inject::set_preferred_injection_window(result); + } + // only hook touch window if multiple windows are allowed if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") { gitadora_remember_window(result, gfdm_window_name); @@ -727,6 +760,11 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); } + + // the dedicated SMALL window is the touch panel; mouse and API touch target it + if (nativetouch::is_hooked() && result != nullptr) { + nativetouch::inject::register_and_attach_window(result); + } } if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { gitadora_force_window_style(result); @@ -1100,6 +1138,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) { return true; } + // fullscreen SDVX keeps two adapters so the subscreen overlay can draw, so the game still + // creates the sub window even when the user asked for it to be gone + if (avs::game::is_model("KFC") && + GRAPHICS_PREVENT_SECONDARY_WINDOWS && + hWnd == SDVX_SUBSCREEN_WINDOW) { + log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd)); + return true; + } + // call original return ShowWindow_orig(hWnd, nCmdShow); } @@ -1316,7 +1363,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet const bool native_touch_overlay = (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || (games::popn::NATIVE_TOUCH && - games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); + games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) || + (games::gitadora::NATIVE_TOUCH && + games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); if (native_touch_overlay) { nativetouch::inject::register_and_attach_window(hWnd); } @@ -1374,6 +1423,12 @@ void graphics_screens_get(std::vector &screens) { screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end()); } +void graphics_poll_screenshot_hotkey() { + if (hotkeys::consume_screenshot()) { + graphics_screenshot_trigger(); + } +} + void graphics_screenshot_trigger() { GRAPHICS_SCREENSHOT_TRIGGER = true; } @@ -1426,10 +1481,12 @@ void graphics_capture_skip(int screen) { GRAPHICS_CAPTURE_CV[screen].notify_one(); } -bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, - bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp, +bool graphics_capture_receive_raw(int screen, std::shared_ptr &out, + int divide, uint64_t *timestamp, int *width, int *height) { + out = nullptr; + if (screen < 0 || screen >= static_cast(GRAPHICS_CAPTURE_SCREEN_NO)) { return false; } @@ -1501,11 +1558,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, capture_height = height_new; } - // compress - auto success = TooJpeg::writeJpeg( - receiver, capture_data.get(), - capture_width, capture_height, - rgb, quality, downsample); + out = std::move(capture_data); // status if (timestamp) { @@ -1518,11 +1571,45 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, *height = capture_height; } + return true; +} + +bool graphics_capture_receive_jpeg(int screen, std::vector &out, + int quality, int divide, uint64_t *timestamp, + int *width, int *height) { + + out.clear(); + + std::shared_ptr pixels; + int capture_width = 0; + int capture_height = 0; + if (!graphics_capture_receive_raw( + screen, pixels, divide, timestamp, &capture_width, &capture_height)) { + return false; + } + + // compress + const bool success = jpeg_encoder::encode( + out, pixels.get(), + capture_width, capture_height, quality); + + if (!success) { + out.clear(); + } + + // status + if (width) { + *width = capture_width; + } + if (height) { + *height = capture_height; + } + // clean up return success; } -std::string graphics_screenshot_genpath() { +std::string graphics_screenshot_genpath(const std::vector &screens) { // verify dir path if (GRAPHICS_SCREENSHOT_DIR.empty()) { @@ -1547,11 +1634,21 @@ std::string graphics_screenshot_genpath() { auto tm_now = *std::gmtime(&t_now); auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d")); - // find next filename + // find next filename; the whole set has to be free so one shot stays numbered together size_t id = 0; while (true) { auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id); - if (!fileutils::file_exists(filepath)) { + bool available = !fileutils::file_exists(filepath); + for (const auto screen : screens) { + if (!available) { + break; + } + if (screen != 0) { + available = !fileutils::file_exists(fmt::format( + "{}\\{}_{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id, screen)); + } + } + if (available) { return filepath; } diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index 22663bd..4d85ed8 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -12,8 +13,6 @@ #include #endif -#include "external/toojpeg/toojpeg.h" - // order must match spice2x_AutoOrientation UI enum order enum graphics_orientation { ORIENTATION_CW = 0, @@ -111,6 +110,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER; // settings extern std::string GRAPHICS_DEVICEID; extern std::string GRAPHICS_SCREENSHOT_DIR; +extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY; +extern bool GRAPHICS_SCREENSHOT_SUBSCREENS; // Direct3D 9 settings extern std::optional D3D9_ADAPTER; @@ -119,6 +120,7 @@ extern bool D3D9_DEVICE_HOOK_DISABLE; void graphics_init(); void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); +bool graphics_gitadora_has_dedicated_subscreen(); // The native GITADORA two-head D3D9 group uses the game's named SMALL // device window for the native physical SMALL head. The game requests // D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized @@ -134,17 +136,28 @@ void graphics_hook_subscreen_window(HWND hWnd); void graphics_screens_register(int screen); void graphics_screens_unregister(int screen); void graphics_screens_get(std::vector &screens); +void graphics_poll_screenshot_hotkey(); void graphics_screenshot_trigger(); bool graphics_screenshot_consume(); + +inline constexpr size_t GRAPHICS_CAPTURE_SCREEN_NO = 4; + void graphics_capture_trigger(int screen); bool graphics_capture_consume(int *screen); void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height); void graphics_capture_skip(int screen); -bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, - bool rgb = true, int quality = 80, bool downsample = true, int divide = 0, +// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes +bool graphics_capture_receive_raw(int screen, std::shared_ptr &out, + int divide = 0, uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr); -std::string graphics_screenshot_genpath(); +// on success `out` holds the encoded JPEG; its storage is reused across calls +bool graphics_capture_receive_jpeg(int screen, std::vector &out, + int quality = 80, int divide = 0, + uint64_t *timestamp = nullptr, + int *width = nullptr, int *height = nullptr); +// the returned path is for screen 0; any extra screens only reserve their suffixed names +std::string graphics_screenshot_genpath(const std::vector &screens = {}); // graphics_windowed.cpp void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/spice2x/hooks/graphics/jpeg_encoder.cpp b/src/spice2x/hooks/graphics/jpeg_encoder.cpp new file mode 100644 index 0000000..dc5763e --- /dev/null +++ b/src/spice2x/hooks/graphics/jpeg_encoder.cpp @@ -0,0 +1,156 @@ +#include "jpeg_encoder.h" + +#ifdef SPICE_JPEG + +#include +#include + +#include + +namespace jpeg_encoder { + +namespace { + +constexpr size_t CHUNK_SIZE = 16 * 1024; + +// libjpeg writes through a destination manager; this one appends straight into +// the caller's vector so the encoded frame is never copied +struct VectorDestination { + jpeg_destination_mgr mgr {}; + std::vector *out = nullptr; + uint8_t chunk[CHUNK_SIZE] {}; +}; + +void dest_init(j_compress_ptr cinfo) { + auto dest = reinterpret_cast(cinfo->dest); + dest->mgr.next_output_byte = dest->chunk; + dest->mgr.free_in_buffer = CHUNK_SIZE; +} + +// libjpeg cannot unwind a C++ exception out of its own frames, so growing the +// output has to fail by value and be turned into an error_exit by the caller +bool dest_append(VectorDestination *dest, size_t size) { + try { + dest->out->insert(dest->out->end(), dest->chunk, dest->chunk + size); + return true; + } catch (...) { + return false; + } +} + +boolean dest_empty(j_compress_ptr cinfo) { + auto dest = reinterpret_cast(cinfo->dest); + + // returning FALSE would mean suspension to libjpeg, not failure + if (!dest_append(dest, CHUNK_SIZE)) { + (*cinfo->err->error_exit)(reinterpret_cast(cinfo)); + } + + dest->mgr.next_output_byte = dest->chunk; + dest->mgr.free_in_buffer = CHUNK_SIZE; + return TRUE; +} + +void dest_term(j_compress_ptr cinfo) { + auto dest = reinterpret_cast(cinfo->dest); + if (!dest_append(dest, CHUNK_SIZE - dest->mgr.free_in_buffer)) { + (*cinfo->err->error_exit)(reinterpret_cast(cinfo)); + } +} + +// the default handler calls exit(), which is not an option inside a game process +struct ErrorManager { + jpeg_error_mgr mgr {}; + jmp_buf escape {}; +}; + +void on_error(j_common_ptr cinfo) { + longjmp(reinterpret_cast(cinfo->err)->escape, 1); +} + +void on_message(j_common_ptr) { +} + +} // namespace + +bool encode( + std::vector &out, + const uint8_t *pixels, + int width, + int height, + int quality) { + + if (!pixels || width <= 0 || height <= 0) { + return false; + } + + if (quality < 1) { + quality = 1; + } else if (quality > 100) { + quality = 100; + } + + // zero init is load bearing: the trap below is armed before the struct is + // created, and jpeg_destroy_compress only tolerates that on a zeroed struct + jpeg_compress_struct cinfo {}; + ErrorManager err; + VectorDestination dest; + + cinfo.err = jpeg_std_error(&err.mgr); + err.mgr.error_exit = on_error; + err.mgr.output_message = on_message; + + if (setjmp(err.escape)) { + jpeg_destroy_compress(&cinfo); + return false; + } + + jpeg_create_compress(&cinfo); + + dest.out = &out; + dest.mgr.init_destination = dest_init; + dest.mgr.empty_output_buffer = dest_empty; + dest.mgr.term_destination = dest_term; + cinfo.dest = &dest.mgr; + + cinfo.image_width = static_cast(width); + cinfo.image_height = static_cast(height); + cinfo.in_color_space = JCS_RGB; + cinfo.input_components = 3; + + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + + // 4:2:0, matching what the capture path asked the previous encoder for + cinfo.comp_info[0].h_samp_factor = 2; + cinfo.comp_info[0].v_samp_factor = 2; + + jpeg_start_compress(&cinfo, TRUE); + + const size_t pitch = static_cast(width) * 3; + while (cinfo.next_scanline < cinfo.image_height) { + auto row = const_cast(pixels + cinfo.next_scanline * pitch); + JSAMPROW rows[1] = { row }; + jpeg_write_scanlines(&cinfo, rows, 1); + } + + jpeg_finish_compress(&cinfo); + jpeg_destroy_compress(&cinfo); + + return true; +} +} + +#else // SPICE_JPEG + +namespace jpeg_encoder { + +// builds without libjpeg-turbo (the WinXP toolchains) simply cannot encode; +// callers already treat a false return as "no frame available" +bool encode(std::vector &out, const uint8_t *, int, int, int) { + out.clear(); + return false; +} +} + +#endif // SPICE_JPEG diff --git a/src/spice2x/hooks/graphics/jpeg_encoder.h b/src/spice2x/hooks/graphics/jpeg_encoder.h new file mode 100644 index 0000000..b1629bc --- /dev/null +++ b/src/spice2x/hooks/graphics/jpeg_encoder.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace jpeg_encoder { + + // appends a baseline 4:2:0 JPEG of packed 24bpp RGB pixels to `out` + bool encode( + std::vector &out, + const uint8_t *pixels, + int width, + int height, + int quality); +} diff --git a/src/spice2x/hooks/graphics/nvapi_impl.cpp b/src/spice2x/hooks/graphics/nvapi_impl.cpp index dbe6b08..25cfb1c 100644 --- a/src/spice2x/hooks/graphics/nvapi_impl.cpp +++ b/src/spice2x/hooks/graphics/nvapi_impl.cpp @@ -1,480 +1,480 @@ -#include "nvapi_impl.h" - -#ifdef SPICE64 - -#include -#include - -#include - -#include "external/nvapi/nvapi.h" -#include "hooks/libraryhook.h" -#include "util/logging.h" -#include "util/sysutils.h" - -namespace nvapi_impl { - -namespace { - -constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828; -constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F; -constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E; -constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F; -constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2; -constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31; -constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8; -constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE; - -constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll"; - -struct SyntheticDisplay { - NvU32 display_id; - NvU32 width; - NvU32 height; - NvU32 color_depth; - NvS32 x; - NvS32 y; - NvU32 refresh_rate_1k; - NV_ROTATE rotation; - bool primary; -}; - -static bool provider_initialized = false; -static bool nvapi_initialized = false; -static int gpu_handle_storage = 0; -// snapshot of the Win32 display state exposed through synthetic NVAPI -static std::vector displays; - -static NvPhysicalGpuHandle get_gpu_handle() { - return reinterpret_cast(&gpu_handle_storage); -} - -static NV_ROTATE get_rotation(DWORD orientation) { - switch (orientation) { - case DMDO_90: - return NV_ROTATE_90; - case DMDO_180: - return NV_ROTATE_180; - case DMDO_270: - return NV_ROTATE_270; - default: - return NV_ROTATE_0; - } -} - -static std::vector enumerate_displays( - uint32_t main_refresh_hz, - uint32_t sub_refresh_hz) { - - std::vector result; - - // reuse the active monitor list, then read live modes after -mainmonitor changes - for (const auto &monitor : sysutils::enumerate_monitors()) { - DEVMODEA mode {}; - mode.dmSize = sizeof(mode); - if (!EnumDisplaySettingsExA( - monitor.display_name.c_str(), - ENUM_CURRENT_SETTINGS, - &mode, - 0)) { - continue; - } - - const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0; - result.push_back({ - .display_id = 0, - .width = mode.dmPelsWidth, - .height = mode.dmPelsHeight, - .color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32, - .x = mode.dmPosition.x, - .y = mode.dmPosition.y, - .refresh_rate_1k = 0, - .rotation = get_rotation(mode.dmDisplayOrientation), - .primary = primary, - }); - } - - std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) { - return left.primary && !right.primary; - }); - - if (result.size() > 2) { - result.resize(2); - } - - if (result.empty()) { - result.push_back({ - .display_id = 0, - .width = 1920, - .height = 1080, - .color_depth = 32, - .x = 0, - .y = 0, - .refresh_rate_1k = 0, - .rotation = NV_ROTATE_0, - .primary = true, - }); - } - - for (size_t index = 0; index < result.size(); index++) { - auto &display = result[index]; - display.primary = index == 0; - display.display_id = 0x80000000u | static_cast(index + 1); - const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz; - display.refresh_rate_1k = refresh_hz * 1000; - } - - return result; -} - -// initializes NVAPI for the calling process. -// marks the synthetic provider initialized without contacting a driver. -static NvAPI_Status __cdecl NvAPI_Initialize_impl() { - log_misc("nvapi_impl", "NvAPI_Initialize"); - nvapi_initialized = true; - return NVAPI_OK; -} - -// initializes NVAPI with additional client flags. -// accepts the flags and marks the synthetic provider initialized. -static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) { - log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags); - nvapi_initialized = true; - return NVAPI_OK; -} - -// releases NVAPI state held for the calling process. -// clears the synthetic initialization state while leaving the provider installed. -static NvAPI_Status __cdecl NvAPI_Unload_impl() { - log_misc("nvapi_impl", "NvAPI_Unload"); - nvapi_initialized = false; - return NVAPI_OK; -} - -// enumerates physical GPU handles managed by the NVIDIA driver. -// returns one stable synthetic GPU containing all exposed displays. -static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl( - NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS], - NvU32 *gpu_count) { - - log_misc( - "nvapi_impl", - "NvAPI_EnumPhysicalGPUs(handles={}, count={})", - fmt::ptr(gpu_handles), - fmt::ptr(gpu_count)); - - if (!nvapi_initialized) { - return NVAPI_API_NOT_INITIALIZED; - } - if (gpu_handles == nullptr || gpu_count == nullptr) { - return NVAPI_INVALID_ARGUMENT; - } - - gpu_handles[0] = get_gpu_handle(); - *gpu_count = 1; - log_misc( - "nvapi_impl", - "NvAPI_EnumPhysicalGPUs - gpu={}, count={}", - fmt::ptr(gpu_handles[0]), - *gpu_count); - return NVAPI_OK; -} - -// returns connected display descriptors for a physical GPU. -// exposes the monitor snapshot as DP primary and HDMI secondary displays. -static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl( - NvPhysicalGpuHandle gpu_handle, - NV_GPU_DISPLAYIDS *display_ids, - NvU32 *display_id_count, - NvU32 flags) { - - const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0; - log_misc( - "nvapi_impl", - "NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})", - fmt::ptr(gpu_handle), - fmt::ptr(display_ids), - input_count, - flags); - - if (!nvapi_initialized) { - return NVAPI_API_NOT_INITIALIZED; - } - if (gpu_handle != get_gpu_handle()) { - return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE; - } - if (display_id_count == nullptr) { - return NVAPI_INVALID_ARGUMENT; - } - - const NvU32 required_count = static_cast(displays.size()); - if (display_ids == nullptr) { - *display_id_count = required_count; - log_misc( - "nvapi_impl", - "NvAPI_GPU_GetConnectedDisplayIds - required_count={}", - required_count); - return NVAPI_OK; - } - - const NvU32 capacity = *display_id_count; - *display_id_count = required_count; - if (capacity < required_count) { - return NVAPI_INSUFFICIENT_BUFFER; - } - - for (NvU32 index = 0; index < required_count; index++) { - const auto &source = displays[index]; - auto &destination = display_ids[index]; - destination = {}; - destination.version = NV_GPU_DISPLAYIDS_VER; - destination.connectorType = source.primary ? - NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI; - destination.displayId = source.display_id; - destination.isActive = 1; - destination.isOSVisible = 1; - destination.isConnected = 1; - destination.isPhysicallyConnected = 1; - } - - log_misc( - "nvapi_impl", - "NvAPI_GPU_GetConnectedDisplayIds - returned_count={}", - required_count); - return NVAPI_OK; -} - -// returns the NVAPI display ID associated with the Windows GDI primary. -// returns the first synthetic display, ordered from the live desktop origin. -static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) { - log_misc( - "nvapi_impl", - "NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})", - fmt::ptr(display_id)); - - if (!nvapi_initialized) { - return NVAPI_API_NOT_INITIALIZED; - } - if (display_id == nullptr || displays.empty()) { - return NVAPI_INVALID_ARGUMENT; - } - - *display_id = displays.front().display_id; - log_misc( - "nvapi_impl", - "NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}", - *display_id); - return NVAPI_OK; -} - -static void fill_source_mode( - NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination, - const SyntheticDisplay &source) { - - if (destination == nullptr) { - return; - } - - *destination = {}; - destination->resolution.width = source.width; - destination->resolution.height = source.height; - destination->resolution.colorDepth = source.color_depth; - destination->colorFormat = NV_FORMAT_A8R8G8B8; - destination->position.x = source.x; - destination->position.y = source.y; - destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE; - destination->bGDIPrimary = source.primary ? 1 : 0; -} - -static NvAPI_Status fill_target( - NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination, - const SyntheticDisplay &source, - NvU32 target_id) { - - if (destination == nullptr) { - return NVAPI_OK; - } - - auto *details = destination->details; - destination->displayId = source.display_id; - destination->targetId = target_id; - - if (details == nullptr) { - return NVAPI_OK; - } - if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) { - return NVAPI_INCOMPATIBLE_STRUCT_VERSION; - } - - *details = {}; - details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER; - details->rotation = source.rotation; - details->scaling = NV_SCALING_DEFAULT; - details->refreshRate1K = source.refresh_rate_1k; - details->timingOverride = NV_TIMING_OVERRIDE_CURRENT; - return NVAPI_OK; -} - -// retrieves the current global display topology through NVAPI's three-pass contract. -// fills caller-owned buffers from the synthetic monitor snapshot and configured rates. -static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl( - NvU32 *path_info_count, - NV_DISPLAYCONFIG_PATH_INFO *path_info) { - - const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0; - log_misc( - "nvapi_impl", - "NvAPI_DISP_GetDisplayConfig(count={}, paths={})", - input_count, - fmt::ptr(path_info)); - - if (!nvapi_initialized) { - return NVAPI_API_NOT_INITIALIZED; - } - if (path_info_count == nullptr) { - return NVAPI_INVALID_ARGUMENT; - } - - const NvU32 required_count = static_cast(displays.size()); - if (path_info == nullptr) { - *path_info_count = required_count; - log_misc( - "nvapi_impl", - "NvAPI_DISP_GetDisplayConfig - required_count={}", - required_count); - return NVAPI_OK; - } - - const NvU32 capacity = *path_info_count; - *path_info_count = required_count; - if (capacity < required_count) { - return NVAPI_INSUFFICIENT_BUFFER; - } - - for (NvU32 index = 0; index < required_count; index++) { - auto &path = path_info[index]; - if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) { - return NVAPI_INCOMPATIBLE_STRUCT_VERSION; - } - if (path.targetInfo != nullptr && path.targetInfoCount < 1) { - return NVAPI_INSUFFICIENT_BUFFER; - } - - const auto &display = displays[index]; - path.sourceId = index; - path.targetInfoCount = 1; - path.IsNonNVIDIAAdapter = 0; - path.pOSAdapterID = nullptr; - fill_source_mode(path.sourceModeInfo, display); - - const NvAPI_Status status = fill_target(path.targetInfo, display, index); - if (status != NVAPI_OK) { - return status; - } - } - - log_misc( - "nvapi_impl", - "NvAPI_DISP_GetDisplayConfig - returned_count={}", - required_count); - return NVAPI_OK; -} - -// applies a supplied global display topology through the NVIDIA driver. -// accepts the cabinet topology without making any changes to Windows. -static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl( - NvU32 path_info_count, - NV_DISPLAYCONFIG_PATH_INFO *path_info, - NvU32 flags) { - - log_misc( - "nvapi_impl", - "NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})", - path_info_count, - fmt::ptr(path_info), - flags); - - if (!nvapi_initialized) { - return NVAPI_API_NOT_INITIALIZED; - } - - log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success"); - return NVAPI_OK; -} - -template -static uintptr_t *query_result(T function) { - return reinterpret_cast(function); -} - -// resolves an NVAPI function ID to its implementation address. -// exposes only the synthetic entry points used by KFC and rejects all others. -static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) { - uintptr_t *result = nullptr; - switch (function_id) { - case NVAPI_INITIALIZE_ID: - result = query_result(NvAPI_Initialize_impl); - break; - case NVAPI_INITIALIZE_EX_ID: - result = query_result(NvAPI_InitializeEx_impl); - break; - case NVAPI_UNLOAD_ID: - result = query_result(NvAPI_Unload_impl); - break; - case NVAPI_ENUM_PHYSICAL_GPUS_ID: - result = query_result(NvAPI_EnumPhysicalGPUs_impl); - break; - case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID: - result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl); - break; - case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID: - result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl); - break; - case NVAPI_DISP_GET_DISPLAY_CONFIG_ID: - result = query_result(NvAPI_DISP_GetDisplayConfig_impl); - break; - case NVAPI_DISP_SET_DISPLAY_CONFIG_ID: - result = query_result(NvAPI_DISP_SetDisplayConfig_impl); - break; - default: - break; - } - - log_misc( - "nvapi_impl", - "NvAPI_QueryInterface(0x{:x}) - {}", - function_id, - result != nullptr ? "implemented" : "unsupported"); - return result; -} - -} - -bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) { - if (provider_initialized) { - return true; - } - if (dll == nullptr) { - log_warning("nvapi_impl", "invalid synthetic module handle"); - return false; - } - - displays = enumerate_displays(main_refresh_hz, sub_refresh_hz); - libraryhook_hook_library(NVAPI_DLL_NAME_A, dll); - libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl); - libraryhook_enable(); - - provider_initialized = true; - log_info( - "nvapi_impl", - "synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz", - NVAPI_DLL_NAME_A, - displays.size(), - main_refresh_hz, - sub_refresh_hz); - return true; -} - -} - -#endif +#include "nvapi_impl.h" + +#ifdef SPICE64 + +#include +#include + +#include + +#include "external/nvapi/nvapi.h" +#include "hooks/libraryhook.h" +#include "util/logging.h" +#include "util/sysutils.h" + +namespace nvapi_impl { + +namespace { + +constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828; +constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F; +constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E; +constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F; +constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2; +constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31; +constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8; +constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE; + +constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll"; + +struct SyntheticDisplay { + NvU32 display_id; + NvU32 width; + NvU32 height; + NvU32 color_depth; + NvS32 x; + NvS32 y; + NvU32 refresh_rate_1k; + NV_ROTATE rotation; + bool primary; +}; + +static bool provider_initialized = false; +static bool nvapi_initialized = false; +static int gpu_handle_storage = 0; +// snapshot of the Win32 display state exposed through synthetic NVAPI +static std::vector displays; + +static NvPhysicalGpuHandle get_gpu_handle() { + return reinterpret_cast(&gpu_handle_storage); +} + +static NV_ROTATE get_rotation(DWORD orientation) { + switch (orientation) { + case DMDO_90: + return NV_ROTATE_90; + case DMDO_180: + return NV_ROTATE_180; + case DMDO_270: + return NV_ROTATE_270; + default: + return NV_ROTATE_0; + } +} + +static std::vector enumerate_displays( + uint32_t main_refresh_hz, + uint32_t sub_refresh_hz) { + + std::vector result; + + // reuse the active monitor list, then read live modes after -mainmonitor changes + for (const auto &monitor : sysutils::enumerate_monitors()) { + DEVMODEA mode {}; + mode.dmSize = sizeof(mode); + if (!EnumDisplaySettingsExA( + monitor.display_name.c_str(), + ENUM_CURRENT_SETTINGS, + &mode, + 0)) { + continue; + } + + const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0; + result.push_back({ + .display_id = 0, + .width = mode.dmPelsWidth, + .height = mode.dmPelsHeight, + .color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32, + .x = mode.dmPosition.x, + .y = mode.dmPosition.y, + .refresh_rate_1k = 0, + .rotation = get_rotation(mode.dmDisplayOrientation), + .primary = primary, + }); + } + + std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) { + return left.primary && !right.primary; + }); + + if (result.size() > 2) { + result.resize(2); + } + + if (result.empty()) { + result.push_back({ + .display_id = 0, + .width = 1920, + .height = 1080, + .color_depth = 32, + .x = 0, + .y = 0, + .refresh_rate_1k = 0, + .rotation = NV_ROTATE_0, + .primary = true, + }); + } + + for (size_t index = 0; index < result.size(); index++) { + auto &display = result[index]; + display.primary = index == 0; + display.display_id = 0x80000000u | static_cast(index + 1); + const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz; + display.refresh_rate_1k = refresh_hz * 1000; + } + + return result; +} + +// initializes NVAPI for the calling process. +// marks the synthetic provider initialized without contacting a driver. +static NvAPI_Status __cdecl NvAPI_Initialize_impl() { + log_misc("nvapi_impl", "NvAPI_Initialize"); + nvapi_initialized = true; + return NVAPI_OK; +} + +// initializes NVAPI with additional client flags. +// accepts the flags and marks the synthetic provider initialized. +static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) { + log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags); + nvapi_initialized = true; + return NVAPI_OK; +} + +// releases NVAPI state held for the calling process. +// clears the synthetic initialization state while leaving the provider installed. +static NvAPI_Status __cdecl NvAPI_Unload_impl() { + log_misc("nvapi_impl", "NvAPI_Unload"); + nvapi_initialized = false; + return NVAPI_OK; +} + +// enumerates physical GPU handles managed by the NVIDIA driver. +// returns one stable synthetic GPU containing all exposed displays. +static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl( + NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS], + NvU32 *gpu_count) { + + log_misc( + "nvapi_impl", + "NvAPI_EnumPhysicalGPUs(handles={}, count={})", + fmt::ptr(gpu_handles), + fmt::ptr(gpu_count)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (gpu_handles == nullptr || gpu_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + gpu_handles[0] = get_gpu_handle(); + *gpu_count = 1; + log_misc( + "nvapi_impl", + "NvAPI_EnumPhysicalGPUs - gpu={}, count={}", + fmt::ptr(gpu_handles[0]), + *gpu_count); + return NVAPI_OK; +} + +// returns connected display descriptors for a physical GPU. +// exposes the monitor snapshot as DP primary and HDMI secondary displays. +static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl( + NvPhysicalGpuHandle gpu_handle, + NV_GPU_DISPLAYIDS *display_ids, + NvU32 *display_id_count, + NvU32 flags) { + + const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0; + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})", + fmt::ptr(gpu_handle), + fmt::ptr(display_ids), + input_count, + flags); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (gpu_handle != get_gpu_handle()) { + return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE; + } + if (display_id_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + const NvU32 required_count = static_cast(displays.size()); + if (display_ids == nullptr) { + *display_id_count = required_count; + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds - required_count={}", + required_count); + return NVAPI_OK; + } + + const NvU32 capacity = *display_id_count; + *display_id_count = required_count; + if (capacity < required_count) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + for (NvU32 index = 0; index < required_count; index++) { + const auto &source = displays[index]; + auto &destination = display_ids[index]; + destination = {}; + destination.version = NV_GPU_DISPLAYIDS_VER; + destination.connectorType = source.primary ? + NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI; + destination.displayId = source.display_id; + destination.isActive = 1; + destination.isOSVisible = 1; + destination.isConnected = 1; + destination.isPhysicallyConnected = 1; + } + + log_misc( + "nvapi_impl", + "NvAPI_GPU_GetConnectedDisplayIds - returned_count={}", + required_count); + return NVAPI_OK; +} + +// returns the NVAPI display ID associated with the Windows GDI primary. +// returns the first synthetic display, ordered from the live desktop origin. +static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) { + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})", + fmt::ptr(display_id)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (display_id == nullptr || displays.empty()) { + return NVAPI_INVALID_ARGUMENT; + } + + *display_id = displays.front().display_id; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}", + *display_id); + return NVAPI_OK; +} + +static void fill_source_mode( + NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination, + const SyntheticDisplay &source) { + + if (destination == nullptr) { + return; + } + + *destination = {}; + destination->resolution.width = source.width; + destination->resolution.height = source.height; + destination->resolution.colorDepth = source.color_depth; + destination->colorFormat = NV_FORMAT_A8R8G8B8; + destination->position.x = source.x; + destination->position.y = source.y; + destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE; + destination->bGDIPrimary = source.primary ? 1 : 0; +} + +static NvAPI_Status fill_target( + NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination, + const SyntheticDisplay &source, + NvU32 target_id) { + + if (destination == nullptr) { + return NVAPI_OK; + } + + auto *details = destination->details; + destination->displayId = source.display_id; + destination->targetId = target_id; + + if (details == nullptr) { + return NVAPI_OK; + } + if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) { + return NVAPI_INCOMPATIBLE_STRUCT_VERSION; + } + + *details = {}; + details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER; + details->rotation = source.rotation; + details->scaling = NV_SCALING_DEFAULT; + details->refreshRate1K = source.refresh_rate_1k; + details->timingOverride = NV_TIMING_OVERRIDE_CURRENT; + return NVAPI_OK; +} + +// retrieves the current global display topology through NVAPI's three-pass contract. +// fills caller-owned buffers from the synthetic monitor snapshot and configured rates. +static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl( + NvU32 *path_info_count, + NV_DISPLAYCONFIG_PATH_INFO *path_info) { + + const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig(count={}, paths={})", + input_count, + fmt::ptr(path_info)); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + if (path_info_count == nullptr) { + return NVAPI_INVALID_ARGUMENT; + } + + const NvU32 required_count = static_cast(displays.size()); + if (path_info == nullptr) { + *path_info_count = required_count; + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig - required_count={}", + required_count); + return NVAPI_OK; + } + + const NvU32 capacity = *path_info_count; + *path_info_count = required_count; + if (capacity < required_count) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + for (NvU32 index = 0; index < required_count; index++) { + auto &path = path_info[index]; + if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) { + return NVAPI_INCOMPATIBLE_STRUCT_VERSION; + } + if (path.targetInfo != nullptr && path.targetInfoCount < 1) { + return NVAPI_INSUFFICIENT_BUFFER; + } + + const auto &display = displays[index]; + path.sourceId = index; + path.targetInfoCount = 1; + path.IsNonNVIDIAAdapter = 0; + path.pOSAdapterID = nullptr; + fill_source_mode(path.sourceModeInfo, display); + + const NvAPI_Status status = fill_target(path.targetInfo, display, index); + if (status != NVAPI_OK) { + return status; + } + } + + log_misc( + "nvapi_impl", + "NvAPI_DISP_GetDisplayConfig - returned_count={}", + required_count); + return NVAPI_OK; +} + +// applies a supplied global display topology through the NVIDIA driver. +// accepts the cabinet topology without making any changes to Windows. +static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl( + NvU32 path_info_count, + NV_DISPLAYCONFIG_PATH_INFO *path_info, + NvU32 flags) { + + log_misc( + "nvapi_impl", + "NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})", + path_info_count, + fmt::ptr(path_info), + flags); + + if (!nvapi_initialized) { + return NVAPI_API_NOT_INITIALIZED; + } + + log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success"); + return NVAPI_OK; +} + +template +static uintptr_t *query_result(T function) { + return reinterpret_cast(function); +} + +// resolves an NVAPI function ID to its implementation address. +// exposes only the synthetic entry points used by KFC and rejects all others. +static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) { + uintptr_t *result = nullptr; + switch (function_id) { + case NVAPI_INITIALIZE_ID: + result = query_result(NvAPI_Initialize_impl); + break; + case NVAPI_INITIALIZE_EX_ID: + result = query_result(NvAPI_InitializeEx_impl); + break; + case NVAPI_UNLOAD_ID: + result = query_result(NvAPI_Unload_impl); + break; + case NVAPI_ENUM_PHYSICAL_GPUS_ID: + result = query_result(NvAPI_EnumPhysicalGPUs_impl); + break; + case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID: + result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl); + break; + case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID: + result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl); + break; + case NVAPI_DISP_GET_DISPLAY_CONFIG_ID: + result = query_result(NvAPI_DISP_GetDisplayConfig_impl); + break; + case NVAPI_DISP_SET_DISPLAY_CONFIG_ID: + result = query_result(NvAPI_DISP_SetDisplayConfig_impl); + break; + default: + break; + } + + log_misc( + "nvapi_impl", + "NvAPI_QueryInterface(0x{:x}) - {}", + function_id, + result != nullptr ? "implemented" : "unsupported"); + return result; +} + +} + +bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) { + if (provider_initialized) { + return true; + } + if (dll == nullptr) { + log_warning("nvapi_impl", "invalid synthetic module handle"); + return false; + } + + displays = enumerate_displays(main_refresh_hz, sub_refresh_hz); + libraryhook_hook_library(NVAPI_DLL_NAME_A, dll); + libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl); + libraryhook_enable(); + + provider_initialized = true; + log_info( + "nvapi_impl", + "synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz", + NVAPI_DLL_NAME_A, + displays.size(), + main_refresh_hz, + sub_refresh_hz); + return true; +} + +} + +#endif diff --git a/src/spice2x/hooks/graphics/nvapi_impl.h b/src/spice2x/hooks/graphics/nvapi_impl.h index a43c9bc..d2c19bf 100644 --- a/src/spice2x/hooks/graphics/nvapi_impl.h +++ b/src/spice2x/hooks/graphics/nvapi_impl.h @@ -1,14 +1,14 @@ -#pragma once - -#ifdef SPICE64 - -#include -#include - -namespace nvapi_impl { - -bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz); - -} - -#endif +#pragma once + +#ifdef SPICE64 + +#include +#include + +namespace nvapi_impl { + +bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz); + +} + +#endif diff --git a/src/spice2x/hooks/lang.cpp b/src/spice2x/hooks/lang.cpp index 553b966..0567dc2 100644 --- a/src/spice2x/hooks/lang.cpp +++ b/src/spice2x/hooks/lang.cpp @@ -26,13 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932; static decltype(GetACP) *GetACP_orig = nullptr; static decltype(GetOEMCP) *GetOEMCP_orig = nullptr; static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr; +static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr; static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr; #ifdef SPICE64 static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr; static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr; static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr; -static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr; static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr; static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr; #endif @@ -209,6 +209,8 @@ static BOOL WINAPI IsDBCSLeadByteEx_hook( return IsDBCSLeadByteEx_orig(CodePage, TestChar); } +#endif + static int WINAPI @@ -244,6 +246,8 @@ WideCharToMultiByte_hook( lpUsedDefaultChar); } +#ifdef SPICE64 + int WINAPI GetLocaleInfoA_hook( @@ -343,15 +347,6 @@ void hooks::lang::early_init() { &IsDBCSLeadByte_orig); } - if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) { - log_info("hooks::lang", "hooking WideCharToMultiByte"); - detour::trampoline_try( - "kernel32.dll", - "WideCharToMultiByte", - WideCharToMultiByte_hook, - &WideCharToMultiByte_orig); - } - if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) { detour::trampoline_try( "kernel32.dll", @@ -362,6 +357,23 @@ void hooks::lang::early_init() { #endif +#ifdef SPICE64 + const auto hook_wide_char_to_multi_byte = + games::gitadora::is_arena_model() || avs::game::is_model("T44"); +#else + // XG2 converts UTF-8 property strings through CP_ACP before rendering. + const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" }); +#endif + + if (hook_wide_char_to_multi_byte) { + log_info("hooks::lang", "hooking WideCharToMultiByte"); + detour::trampoline_try( + "kernel32.dll", + "WideCharToMultiByte", + WideCharToMultiByte_hook, + &WideCharToMultiByte_orig); + } + } void hooks::lang::init() { diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 83a06b0..87a627a 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -14,6 +14,7 @@ #include "acio/icca/icca.h" #include "acio/mdxf/mdxf.h" #include "api/controller.h" +#include "api/stream_server.h" #include "avs/automap.h" #include "avs/core.h" #include "avs/ea3.h" @@ -90,13 +91,13 @@ #include "launcher/launcher.h" #include "launcher/logger.h" #include "launcher/signal.h" -#include "launcher/superexit.h" #include "launcher/richpresence.h" #include "launcher/shutdown.h" #include "launcher/options.h" #include "misc/bt5api.h" #include "misc/device.h" #include "misc/eamuse.h" +#include "misc/hotkeys.h" #include "misc/extdev.h" #include "misc/ami2000.h" #include "misc/sciunit.h" @@ -153,6 +154,7 @@ std::string CARD_OVERRIDES[2]; // sub-systems std::unique_ptr API_CONTROLLER; +std::unique_ptr API_STREAM_SERVER; std::unique_ptr RI_MGR; // trigger NVIDIA Optimus & AMD Enduro High Performance Graphics @@ -201,6 +203,7 @@ int main_implementation(int argc, char *argv[]) { bool api_pretty = false; bool api_debug = false; unsigned short api_port = 1337; + bool api_stream_enable = false; std::string api_pass = ""; std::vector api_serial_port; std::vector api_serial_baud; @@ -1035,6 +1038,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::APIScreenMirrorDivide].is_active()) { api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32(); } + if (options[launcher::Options::APIStreamEnable].value_bool() && !cfg::CONFIGURATOR_STANDALONE) { + api_stream_enable = true; + } if (options[launcher::Options::DisableDebugHooks].value_bool()) { debughook::DEBUGHOOK_LOGGING = false; @@ -1106,6 +1112,12 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::ScreenshotFolder].is_active()) { GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text(); } + if (options[launcher::Options::ScreenshotIncludeOverlay].value_bool()) { + GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = true; + } + if (options[launcher::Options::ScreenshotSubscreens].value_bool()) { + GRAPHICS_SCREENSHOT_SUBSCREENS = true; + } if (options[launcher::Options::DisableColoredOutput].value_bool()) { logger::COLOR = false; } @@ -1680,6 +1692,26 @@ int main_implementation(int argc, char *argv[]) { }); } + if (options[launcher::Options::PathToModules].is_active() && !cfg::CONFIGURATOR_STANDALONE) { + log_warning( + "launcher", + "WARNING - user specified -modules option\n\n\n" + "!!! !!!\n" + "!!! Using -modules changes which game DLLs get loaded! !!!\n" + "!!! Unless you know exactly what you are doing, clear -modules !!!\n" + "!!! and try again; usually this is accidentally set by users !!!\n" + "!!! without understanding the implications. !!!\n" + "!!! !!!\n" + ); + deferredlogs::defer_error_messages({ + "-modules option specified by user", + " game DLLs and patches are loaded from that folder instead of the spice folder,", + " and it is also prepended to the DLL search path, so dependencies may resolve to", + " unexpected copies; instead, clear -modules option and place spice binaries in", + " the intended game directory", + }); + } + if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) { log_warning( "launcher", @@ -1780,9 +1812,8 @@ int main_implementation(int argc, char *argv[]) { nvapi::initialize(); // add application profile to nvcp nvapi::set_profile_settings(); - // enable super exit - superexit::enable(); - + // keep ALT+F4 available during lengthy non-standalone boot + hotkeys::start(); // enable subscreen touch emulation if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) { games::iidx::ENABLE_POKE = true; @@ -2456,6 +2487,7 @@ int main_implementation(int argc, char *argv[]) { // initialize raw input RI_MGR = std::make_unique(); + hotkeys::enable_raw_input(); for (const auto &device : sextet_devices) { RI_MGR->sextet_register(device); } @@ -2478,6 +2510,9 @@ int main_implementation(int argc, char *argv[]) { log_misc("rawinput", "Analog mappings:"); dump_analog_bindings(); + // mappings are ready; begin screenshot and coin polling during late startup + hotkeys::enable_input(); + // for certain games, show cursor if no touch is available (must be called after RI_MGR is available) if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) { GRAPHICS_SHOW_CURSOR = true; @@ -2713,9 +2748,20 @@ int main_implementation(int argc, char *argv[]) { for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) { API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]); } - - // start coin input thread - eamuse_coin_start_thread(); + // the websocket already sits on the API port plus one, so the stream takes plus two + if (api_stream_enable) { + if (!api_enable) { + log_fatal("launcher", "video stream requires API port to be set (-api)"); + } else if (api_port + 2 > 65535) { + log_fatal( + "launcher", + "ignoring the video stream, API port {} leaves no room for port plus two", + api_port); + } else { + API_STREAM_SERVER = std::make_unique( + static_cast(api_port + 2)); + } + } // pin macro if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) { @@ -2765,6 +2811,9 @@ int main_implementation(int argc, char *argv[]) { // cleanup procedure launcher::on_shutdown = [&]() { + // stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active + hotkeys::disable_input(); + // clear presence richpresence::shutdown(); @@ -2803,11 +2852,9 @@ int main_implementation(int argc, char *argv[]) { } // free api controller + API_STREAM_SERVER.reset(); API_CONTROLLER.reset(); - // stop coin input thread - eamuse_coin_stop_thread(); - eamuse_pin_macro_stop_thread(); // BT5API @@ -2818,6 +2865,7 @@ int main_implementation(int argc, char *argv[]) { sdk::fini_sdk_modules(); // stop raw input + hotkeys::disable_raw_input(); RI_MGR.reset(); // debug hook @@ -2850,8 +2898,8 @@ int main_implementation(int argc, char *argv[]) { // dispose crypt crypt::dispose(); - // disable super exit - superexit::disable(); + // end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit + hotkeys::stop(); // disable poke games::iidx::poke::disable(); diff --git a/src/spice2x/launcher/logger.cpp b/src/spice2x/launcher/logger.cpp index 8c5ad67..aad50af 100644 --- a/src/spice2x/launcher/logger.cpp +++ b/src/spice2x/launcher/logger.cpp @@ -1,6 +1,7 @@ #include "logger.h" #include +#include #include #include #include @@ -25,13 +26,16 @@ namespace logger { bool COLOR = true; // state - static bool RUNNING = false; + static std::atomic RUNNING = false; static WORD DEFAULT_ATTRIBUTES = 0; static std::mutex EVENT_MUTEX; static std::condition_variable EVENT_CV; static std::thread *THREAD = nullptr; + static HANDLE THREAD_FINISHED = nullptr; + static std::atomic THREAD_ABANDONED = false; static std::mutex OUTPUT_MUTEX; - static bool OUTPUT_BUFFER_HOT = false; + static std::mutex FLUSH_MUTEX; + static std::atomic OUTPUT_BUFFER_HOT = false; static std::vector> OUTPUT_BUFFER1; static std::vector> OUTPUT_BUFFER2; static std::vector> *OUTPUT_BUFFER = &OUTPUT_BUFFER1; @@ -71,7 +75,9 @@ namespace logger { SetConsoleTextAttribute(hTerminal, info.wAttributes); } - static void output_buffer_flush() { + // the buffer is swapped under OUTPUT_MUTEX but drained outside of it, so two concurrent + // drains would leave one of them iterating a buffer that push() has started appending to + static void output_buffer_flush_locked() { // get buffer and swap auto buffer = output_buffer_swap(); @@ -142,6 +148,23 @@ namespace logger { } } + static void output_buffer_flush() { + + // a detached logging thread can hold FLUSH_MUTEX forever, so never wait on it + if (THREAD_ABANDONED) { + std::unique_lock guard(FLUSH_MUTEX, std::try_to_lock); + if (guard.owns_lock()) { + output_buffer_flush_locked(); + } + + return; + } + + std::lock_guard guard(FLUSH_MUTEX); + + output_buffer_flush_locked(); + } + void start() { // don't start if blocking @@ -151,6 +174,7 @@ namespace logger { // start logging thread RUNNING = true; + THREAD_FINISHED = CreateEvent(nullptr, TRUE, FALSE, nullptr); THREAD = new std::thread([] { std::unique_lock lock(EVENT_MUTEX); @@ -160,7 +184,7 @@ namespace logger { while (RUNNING) { // wait for hot buffer - EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT; }); + EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT.load(); }); OUTPUT_BUFFER_HOT = false; // flush buffer @@ -180,22 +204,51 @@ namespace logger { HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES); } + + if (THREAD_FINISHED) { + SetEvent(THREAD_FINISHED); + } }); } void stop() { - log_info("logger", "stop"); + + // NOTE: don't log to the logger here! + + RUNNING = false; // clean up thread if required - RUNNING = false; if (THREAD) { // fake notify to exit wait loop OUTPUT_BUFFER_HOT = true; EVENT_CV.notify_all(); - // join and clean up - THREAD->join(); + // never block forever - this also runs on the fatal/crash path, where the logging + // thread may be suspended or wedged and would take the whole process down with it + const bool finished = THREAD_FINISHED != nullptr && + WaitForSingleObject(THREAD_FINISHED, 1000) == WAIT_OBJECT_0; + + if (finished) { + THREAD->join(); + + CloseHandle(THREAD_FINISHED); + THREAD_FINISHED = nullptr; + } else { + THREAD->detach(); + THREAD_ABANDONED = true; + + // THREAD_FINISHED is leaked on purpose: the detached thread can still wake up and + // signal it, and closing it here risks signaling an unrelated recycled handle + + // write out whatever the logging thread never got to + output_buffer_flush(); + + if (LOG_FILE && LOG_FILE != INVALID_HANDLE_VALUE) { + FlushFileBuffers(LOG_FILE); + } + } + delete THREAD; THREAD = nullptr; } @@ -229,17 +282,14 @@ namespace logger { // check if blocking or the logging thread is not running if (BLOCKING || !RUNNING) { - // blocking guard - static std::mutex blocking_lock; - std::lock_guard blocking_guard(blocking_lock); - // immediately process logs output_buffer_flush(); } else { - // mark buffer as hot - std::unique_lock lock(EVENT_MUTEX); + // never block here - the logging thread can be suspended while holding EVENT_MUTEX, + // and it re-checks OUTPUT_BUFFER_HOT before waiting again + std::unique_lock lock(EVENT_MUTEX, std::try_to_lock); OUTPUT_BUFFER_HOT = true; EVENT_CV.notify_one(); } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 38ffd44..f789bb4 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1743,6 +1743,21 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "1", .category = "Companion & API", }, + { + // APIStreamEnable + .title = "API Video Stream Server Enable", + .name = "apistream", + .desc = "Serves the mirrored screen as a video stream, on the API port plus two; " + "alternative to API screen capture. Requires -api.\n\n" + "http://host:apiport+2/stream.mjpg - MJPEG\n\n" + "http://host:apiport+2/stream.h264 - H.264\n\n" + "Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n" + "Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n" + "VIEW ONLY - touch input still requires -api. " + "No password protection or encryption of any kind; video sent in the clear!", + .type = OptionType::Bool, + .category = "Companion & API", + }, { .title = "Enable All IO Modules", .name = "io", @@ -3399,6 +3414,22 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "OBS Control", }, + { + // ScreenshotIncludeOverlay + .title = "Include Overlay in Screenshots", + .name = "screenshotoverlay", + .desc = "Includes Spice overlay in screenshots.", + .type = OptionType::Bool, + .category = "General Overlay", + }, + { + // ScreenshotSubscreens + .title = "Include Subscreens in Screenshots", + .name = "screenshotsub", + .desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.", + .type = OptionType::Bool, + .category = "General Overlay", + }, }; const std::vector &launcher::get_categories(Options::OptionsCategory category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 7737fec..24ff56a 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -170,6 +170,7 @@ namespace launcher { APIDebugMode, APIScreenMirrorQuality, APIScreenMirrorDivide, + APIStreamEnable, EnableAllIOModules, EnableACIOModule, EnableICCAModule, @@ -320,7 +321,9 @@ namespace launcher { OBSWebSocketHost, OBSWebSocketPort, OBSWebSocketPassword, - OBSWebSocketDebug + OBSWebSocketDebug, + ScreenshotIncludeOverlay, + ScreenshotSubscreens }; enum class OptionsCategory { diff --git a/src/spice2x/launcher/superexit.cpp b/src/spice2x/launcher/superexit.cpp index 5219905..9db98a1 100644 --- a/src/spice2x/launcher/superexit.cpp +++ b/src/spice2x/launcher/superexit.cpp @@ -1,21 +1,14 @@ #include "superexit.h" -#include - #include "windows.h" + +#include "cfg/configurator.h" #include "launcher/shutdown.h" -#include "rawinput/rawinput.h" -#include "util/logging.h" #include "touch/touch.h" -#include "misc/eamuse.h" -#include "games/io.h" -#include "overlay/overlay.h" +#include "util/logging.h" namespace superexit { - static std::thread *THREAD = nullptr; - static bool THREAD_RUNNING = false; - bool has_focus() { HWND fg_wnd = GetForegroundWindow(); if (fg_wnd == NULL) { @@ -29,92 +22,22 @@ namespace superexit { return fg_pid == GetCurrentProcessId(); } - void enable() { - - // check if already running - if (THREAD) - return; - - // create new thread - THREAD_RUNNING = true; - THREAD = new std::thread([] { - - // log - log_info("superexit", "enabled"); - - // set variable to false to stop - while (THREAD_RUNNING) { - - // check rawinput for ALT+F4 - bool rawinput_exit = false; - if (RI_MGR != nullptr) { - auto devices = RI_MGR->devices_get(); - for (auto &device : devices) { - switch (device.type) { - case rawinput::KEYBOARD: { - auto &key_states = device.keyboardInfo->key_states; - for (int page_index = 0; page_index < 1024; page_index += 256) { - if (key_states[page_index + VK_MENU] - && key_states[page_index + VK_F4]) { - rawinput_exit = true; - } - } - break; - } - default: - break; - } - } - } - - bool async_key_exit = - (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 && - (GetAsyncKeyState(VK_F4) & 0x8000) != 0; - - bool overlay_exit = false; - auto buttons = games::get_buttons_overlay(eamuse_get_game()); - if (buttons && - (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) && - GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) { - overlay_exit = true; - } - - // check for exit - if (rawinput_exit || async_key_exit) { - if (has_focus()) { - log_info("superexit", "detected ALT+F4, exiting..."); - launcher::shutdown(); - } - } - if (overlay_exit) { - if (has_focus()) { - log_info("superexit", "detected Force Exit Game overlay shortcut, exiting..."); - launcher::shutdown(); - } - } - - // slow down - Sleep(100); - } - - return nullptr; - }); - } - - void disable() { - if (!THREAD) { + void handle_hotkeys(bool alt_f4, bool mapped_exit) { + if (!alt_f4 && !mapped_exit) { return; } - - // stop old thread - THREAD_RUNNING = false; - THREAD->join(); - - // delete thread - delete THREAD; - THREAD = nullptr; - - // log - log_info("superexit", "disabled"); + if (cfg::CONFIGURATOR_STANDALONE) { + return; + } + if (!has_focus()) { + return; + } + if (alt_f4) { + log_info("superexit", "detected ALT+F4, exiting..."); + launcher::shutdown(); + return; + } + log_info("superexit", "detected Force Exit Game overlay shortcut, exiting..."); + launcher::shutdown(); } } diff --git a/src/spice2x/launcher/superexit.h b/src/spice2x/launcher/superexit.h index 193727a..575cd01 100644 --- a/src/spice2x/launcher/superexit.h +++ b/src/spice2x/launcher/superexit.h @@ -2,6 +2,5 @@ namespace superexit { bool has_focus(); - void enable(); - void disable(); + void handle_hotkeys(bool alt_f4, bool mapped_exit); } diff --git a/src/spice2x/licenses.txt b/src/spice2x/licenses.txt index d7a9b3a..e552c1f 100644 --- a/src/spice2x/licenses.txt +++ b/src/spice2x/licenses.txt @@ -527,25 +527,78 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -toojpeg (zlib) +fpng (Unlicense) ------------------------------------------- -zlib License +Copyright (c) 2021 Richard Geldreich, Jr. +Incorporates public domain code by Alex Evans, the original miniz by +Richard Geldreich, Jr., and Huffman code size work by Alistair Moffat and +Jyrki Katajainen. -Copyright (c) 2011-2016 Stephan Brumme +This is free and unencumbered software released into the public domain. -This software is provided 'as-is', without any express or implied warranty. In -no event will the authors be held liable for any damages arising from the use -of this software. -Permission is granted to anyone to use this software for any purpose, including -commercial applications, and to alter it and redistribute it freely, subject to -the following restrictions: -1. The origin of this software must not be misrepresented; you must not claim - that you wrote the original software. If you use this software in a product, - an acknowledgment in the product documentation would be appreciated but is - not required. -2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. -3. This notice may not be removed or altered from any source distribution. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute +this software, either in source code form or as a compiled binary, for any +purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. + +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 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. + +libjpeg-turbo (IJG) +------------------------------------------- +This software is based in part on the work of the Independent JPEG Group. + +Only the libjpeg API library is used here. Per the libjpeg-turbo licensing +terms that portion is covered by the IJG License, reproduced below; the SIMD +sources bear the zlib License, whose terms are subsumed by the IJG License in +the context of the overall libjpeg API library. The Modified (3-clause) BSD +License covers the TurboJPEG API library and build system, neither of which is +distributed here. + +The authors make NO WARRANTY or representation, either express or implied, +with respect to this software, its quality, accuracy, merchantability, or +fitness for a particular purpose. This software is provided "AS IS", and you, +its user, assume the entire risk as to its quality and accuracy. + +This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. +All Rights Reserved except as specified below. + +Permission is hereby granted to use, copy, modify, and distribute this +software (or portions thereof) for any purpose, without fee, subject to these +conditions: +(1) If any part of the source code for this software is distributed, then this +README file must be included, with this copyright and no-warranty notice +unaltered; and any additions, deletions, or changes to the original files +must be clearly indicated in accompanying documentation. +(2) If only executable code is distributed, then the accompanying +documentation must state that "this software is based in part on the work of +the Independent JPEG Group". +(3) Permission for use of this software is granted only if the user accepts +full responsibility for any undesirable consequences; the authors accept +NO LIABILITY for damages of any kind. + +These conditions apply to any software derived from or based on the IJG code, +not just to the unmodified library. If you use our work, you ought to +acknowledge us. + +Permission is NOT granted for the use of any IJG author's name or company name +in advertising or publicity relating to this software or products derived from +it. This software may be referred to only as "the Independent JPEG Group's +software". + +We specifically permit and encourage the use of this software as the basis of +commercial products, provided that all warranty or liability claims are +assumed by the product vendor. robin_hood.h (MIT) ------------------------------------------- @@ -1353,3 +1406,23 @@ Contributions ------------------------------------------- cardio - Felix - MIT License scard - nolm - MIT License + +x264 (GPL-2.0-or-later) +------------------------------------------- +https://www.videolan.org/developers/x264.html +Statically linked for the API video stream H.264 encoder. + +Copyright (C) 2003-2024 x264 project + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 2 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., 51 Franklin +Street, Fifth Floor, Boston, MA 02110-1301, USA. \ No newline at end of file diff --git a/src/spice2x/misc/clipboard.cpp b/src/spice2x/misc/clipboard.cpp index 0a4c2f5..5cf17fd 100644 --- a/src/spice2x/misc/clipboard.cpp +++ b/src/spice2x/misc/clipboard.cpp @@ -58,17 +58,18 @@ namespace clipboard { return; } - // If the screenshot button is print screen, the OpenClipboard call seems to fail often if we only - // call it once, probably due to a race condition. So, we can try calling a lot until we can open it. + // print screen key leaves the OS briefly holding the clipboard, so retry + // spinning without yielding starves the thread we are waiting on and looks like a hang bool clipboard_open = false; - for (int i = 0; i < 1000000; i++) { + for (int i = 0; i < 100; i++) { if (OpenClipboard(nullptr)) { clipboard_open = true; break; } + Sleep(5); } if (!clipboard_open) { - log_warning("clipboard", "Failed to open clipboard"); + log_warning("clipboard", "failed to open clipboard"); return; } diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index a89d2ad..7e054f5 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0}; static double CARD_INSERT_TIMEOUT = 2.0; static char CARD_INSERT_UID[2][8] = {{0}, {0}}; static char CARD_INSERT_UID_ENABLE[2] = {false, false}; -static int COIN_STOCK = 0; -static bool COIN_BLOCK = false; -static std::thread *COIN_INPUT_THREAD; -static bool COIN_INPUT_THREAD_ACTIVE = false; +static std::atomic_int COIN_STOCK {0}; +static std::atomic_bool COIN_BLOCK {false}; static uint16_t KEYPAD_STATE[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0}; @@ -292,78 +290,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) { } bool eamuse_coin_get_block() { - return COIN_BLOCK; + return COIN_BLOCK.load(std::memory_order_relaxed); } void eamuse_coin_set_block(bool block) { - COIN_BLOCK = block; + COIN_BLOCK.store(block, std::memory_order_relaxed); } int eamuse_coin_get_stock() { - return COIN_STOCK; + return COIN_STOCK.load(std::memory_order_relaxed); } void eamuse_coin_set_stock(int amount) { - COIN_STOCK = amount; + COIN_STOCK.store(amount, std::memory_order_relaxed); } bool eamuse_coin_consume(int amount) { - if (COIN_STOCK < amount) { - return false; - } else { - COIN_STOCK -= amount; - return true; + auto stock = COIN_STOCK.load(std::memory_order_relaxed); + while (stock >= amount) { + if (COIN_STOCK.compare_exchange_weak( + stock, + stock - amount, + std::memory_order_relaxed)) { + return true; + } } + return false; } int eamuse_coin_consume_stock() { - int stock = COIN_STOCK; - COIN_STOCK = 0; - return stock; + return COIN_STOCK.exchange(0, std::memory_order_relaxed); } int eamuse_coin_add() { - return ++COIN_STOCK; + return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1; } -void eamuse_coin_start_thread() { - - // set active - COIN_INPUT_THREAD_ACTIVE = true; - - // create thread - COIN_INPUT_THREAD = new std::thread([]() { - auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game()); - static bool COIN_INPUT_KEY_STATE = false; - while (COIN_INPUT_THREAD_ACTIVE) { - - // check input key - if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at( - games::OverlayButtons::InsertCoin))) { - if (!COIN_INPUT_KEY_STATE) { - if (COIN_BLOCK) - log_info("eamuse", "coin inserted while blocked"); - else { - log_info("eamuse", "coin insert"); - COIN_STOCK++; - } - } - COIN_INPUT_KEY_STATE = true; - } else { - COIN_INPUT_KEY_STATE = false; - } - - // once every two frames - std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30)); - } - }); -} - -void eamuse_coin_stop_thread() { - COIN_INPUT_THREAD_ACTIVE = false; - COIN_INPUT_THREAD->join(); - delete COIN_INPUT_THREAD; - COIN_INPUT_THREAD = nullptr; +void eamuse_coin_insert() { + if (COIN_BLOCK.load(std::memory_order_relaxed)) { + log_info("eamuse", "coin inserted while blocked"); + } else { + log_info("eamuse", "coin insert"); + COIN_STOCK.fetch_add(1, std::memory_order_relaxed); + } } void eamuse_pin_macro_start_thread() { diff --git a/src/spice2x/misc/eamuse.h b/src/spice2x/misc/eamuse.h index b2bd51d..d772bb5 100644 --- a/src/spice2x/misc/eamuse.h +++ b/src/spice2x/misc/eamuse.h @@ -58,9 +58,7 @@ bool eamuse_coin_consume(int amount); int eamuse_coin_consume_stock(); int eamuse_coin_add(); - -void eamuse_coin_start_thread(); -void eamuse_coin_stop_thread(); +void eamuse_coin_insert(); void eamuse_pin_macro_start_thread(); void eamuse_pin_macro_stop_thread(); diff --git a/src/spice2x/misc/hotkeys.cpp b/src/spice2x/misc/hotkeys.cpp new file mode 100644 index 0000000..1b53679 --- /dev/null +++ b/src/spice2x/misc/hotkeys.cpp @@ -0,0 +1,154 @@ +#include "hotkeys.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "games/io.h" +#include "launcher/superexit.h" +#include "misc/eamuse.h" +#include "overlay/overlay.h" +#include "rawinput/rawinput.h" +#include "util/logging.h" + +namespace hotkeys { + + namespace { + + // 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler + // interval during early boot or when process timer adjustments are disabled + constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8); + + std::atomic_bool SCREENSHOT_PENDING {false}; + std::mutex INPUT_MUTEX; + bool INPUT_ENABLED = false; + bool RAW_INPUT_ENABLED = false; + std::jthread WORKER; + + bool read_button(std::vector