diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index c60b11807..2173e1523 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -129,6 +129,8 @@ jobs: submodules: 'recursive' - run: pacman -S --noconfirm mingw-w64-x86_64-gcc mingw-w64-x86_64-freetype mingw-w64-x86_64-pkg-config make mingw-w64-x86_64-python3 mingw-w64-x86_64-libjpeg-turbo mingw-w64-x86_64-libpng - run: make -j2 PLATFORM=simulator + - run: make -j2 PLATFORM=simulator test.exe + - run: cmd /c output\release\simulator\windows\test.exe --headless - uses: actions/upload-artifact@master with: name: epsilon-windows.exe @@ -143,6 +145,8 @@ jobs: with: submodules: 'recursive' - run: make -j2 PLATFORM=simulator TARGET=web + - run: make -j2 PLATFORM=simulator TARGET=web test.js + - run: node output/release/simulator/web/test.js --headless - uses: actions/upload-artifact@master with: name: epsilon-web.zip @@ -155,6 +159,8 @@ jobs: with: submodules: 'recursive' - run: make -j2 PLATFORM=simulator + - run: make -j2 PLATFORM=simulator test.bin + - run: output/release/simulator/linux/test.bin --headless - uses: actions/upload-artifact@master with: name: epsilon-linux.bin @@ -168,6 +174,8 @@ jobs: with: submodules: 'recursive' - run: make -j2 PLATFORM=simulator + - run: make -j2 PLATFORM=simulator ARCH=x86_64 test.bin + - run: output/release/simulator/macos/x86_64/test.bin --headless - uses: actions/upload-artifact@master with: name: epsilon-macos.zip diff --git a/apps/Makefile b/apps/Makefile index 69f88165e..820ff690c 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -85,9 +85,6 @@ language_preferences = apps/language_preferences.csv SFLAGS += -I$(BUILD_DIR) i18n_files += $(addprefix apps/language_,$(addsuffix .universal.i18n, $(EPSILON_I18N))) -ifeq ($(EPSILON_GETOPT),1) -i18n_files += $(addprefix apps/language_,$(addsuffix _iso6391.universal.i18n, $(EPSILON_I18N))) -endif i18n_files += $(call i18n_with_universal_for,shared) i18n_files += $(call i18n_with_universal_for,toolbox) @@ -97,7 +94,7 @@ $(eval $(call rule_for, \ I18N, \ apps/i18n.cpp, \ $(i18n_files), \ - $$(PYTHON) apps/i18n.py --codepoints $(code_points) --countrypreferences $(country_preferences) --languagepreferences $(language_preferences) --header $$(subst .cpp,.h,$$@) --implementation $$@ --locales $$(EPSILON_I18N) --countries $$(EPSILON_COUNTRIES) --files $$^ --generateISO6391locales $$(EPSILON_GETOPT), \ + $$(PYTHON) apps/i18n.py --codepoints $(code_points) --countrypreferences $(country_preferences) --languagepreferences $(language_preferences) --header $$(subst .cpp,.h,$$@) --implementation $$@ --locales $$(EPSILON_I18N) --countries $$(EPSILON_COUNTRIES) --files $$^, \ global \ )) @@ -113,6 +110,8 @@ $(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/apps/i18n.h $(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/apps/home/apps_layout.h $(call object_for,$(apps_src) $(tests_src)): $(BUILD_DIR)/python/port/genhdr/qstrdefs.generated.h +$(call object_for,$(apps_src)): $(BUILD_DIR)/apps/home/apps_layout.h + apps_tests_src = $(app_calculation_test_src) $(app_code_test_src) $(app_graph_test_src) $(app_probability_test_src) $(app_regression_test_src) $(app_sequence_test_src) $(app_shared_test_src) $(app_statistics_test_src) $(app_settings_test_src) $(app_solver_test_src) apps_tests_src += $(addprefix apps/,\ diff --git a/apps/i18n.py b/apps/i18n.py index b883f557b..561275b2a 100644 --- a/apps/i18n.py +++ b/apps/i18n.py @@ -23,13 +23,9 @@ parser.add_argument('--codepoints', help='the code_points.h file') parser.add_argument('--countrypreferences', help='the country_preferences.csv file') parser.add_argument('--languagepreferences', help='the language_preferences.csv file') parser.add_argument('--files', nargs='+', help='an i18n file') -parser.add_argument('--generateISO6391locales', type=int, nargs='+', help='whether to generate the ISO6391 codes for the languages (for instance "en" for english)') args = parser.parse_args() -def generate_ISO6391(): - return args.generateISO6391locales[0] == 1 - def has_glyph(glyph): return glyph in codepoints @@ -196,13 +192,6 @@ def print_header(data, path, locales, countries): lambda arg: arg.upper(), " Message::Language") - if generate_ISO6391(): - print_block_from_list(f, - "constexpr const Message LanguageISO6391Names[NumberOfLanguages] = {\n", - locales, - lambda arg: arg.upper(), - " Message::LanguageISO6391") - # Countries enumeration print_block_from_list(f, "enum class Country : uint8_t {\n", @@ -236,6 +225,12 @@ def print_header(data, path, locales, countries): f.write(line[:-2] + "),\n") f.write("};\n\n") + # Language ISO639-1 codes + f.write("constexpr const char * LanguageISO6391Codes[NumberOfLanguages] = {\n"); + for locale in locales: + f.write(" \"" + locale + "\",\n") + f.write("};\n\n") + f.write("}\n\n") f.write("#endif\n") f.close() diff --git a/apps/language_de_iso6391.universal.i18n b/apps/language_de_iso6391.universal.i18n deleted file mode 100644 index 8d811650d..000000000 --- a/apps/language_de_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391DE = "de" diff --git a/apps/language_en_iso6391.universal.i18n b/apps/language_en_iso6391.universal.i18n deleted file mode 100644 index 54972ae84..000000000 --- a/apps/language_en_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391EN = "en" diff --git a/apps/language_es_iso6391.universal.i18n b/apps/language_es_iso6391.universal.i18n deleted file mode 100644 index 615cbf978..000000000 --- a/apps/language_es_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391ES = "es" diff --git a/apps/language_fr_iso6391.universal.i18n b/apps/language_fr_iso6391.universal.i18n deleted file mode 100644 index 905e6f179..000000000 --- a/apps/language_fr_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391FR = "fr" diff --git a/apps/language_it_iso6391.universal.i18n b/apps/language_it_iso6391.universal.i18n deleted file mode 100644 index 6a72c7216..000000000 --- a/apps/language_it_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391IT = "it" diff --git a/apps/language_nl_iso6391.universal.i18n b/apps/language_nl_iso6391.universal.i18n deleted file mode 100644 index 0fef88fd0..000000000 --- a/apps/language_nl_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391NL = "nl" diff --git a/apps/language_pt_iso6391.universal.i18n b/apps/language_pt_iso6391.universal.i18n deleted file mode 100644 index 15218e760..000000000 --- a/apps/language_pt_iso6391.universal.i18n +++ /dev/null @@ -1 +0,0 @@ -LanguageISO6391PT = "pt" diff --git a/apps/main.cpp b/apps/main.cpp index d501a0d9a..39b0d488c 100644 --- a/apps/main.cpp +++ b/apps/main.cpp @@ -19,6 +19,10 @@ void ion_main(int argc, const char * const argv[]) { #else void ion_main(int argc, const char * const argv[]) { + /* Lock OTP on older devices to prevent garbage being written where the PCB + * version is read. */ + Ion::Board::lockUnlockedPCBVersion(); + // Initialize Poincare::TreePool::sharedPool Poincare::Init(); @@ -36,7 +40,7 @@ void ion_main(int argc, const char * const argv[]) { continue; } for (int j = 0; j < I18n::NumberOfLanguages; j++) { - if (strcmp(requestedLanguageId, I18n::translate(I18n::LanguageISO6391Names[j])) == 0) { + if (strcmp(requestedLanguageId, I18n::LanguageISO6391Codes[j]) == 0) { GlobalPreferences::sharedGlobalPreferences()->setLanguage((I18n::Language)j); GlobalPreferences::sharedGlobalPreferences()->setCountry(I18n::DefaultCountryForLanguage[j]); break; diff --git a/apps/settings/sub_menu/about_controller.cpp b/apps/settings/sub_menu/about_controller.cpp index d5f884aa8..220a2c396 100644 --- a/apps/settings/sub_menu/about_controller.cpp +++ b/apps/settings/sub_menu/about_controller.cpp @@ -48,11 +48,15 @@ bool AboutController::handleEvent(Ion::Events::Event event) { if (!(event == Ion::Events::Right)) { if (childLabel == I18n::Message::SoftwareVersion) { MessageTableCellWithBuffer * myCell = (MessageTableCellWithBuffer *)m_selectableTableView.selectedCell(); - if (strcmp(myCell->accessoryText(), Ion::patchLevel()) == 0) { + const char * currentText = myCell->accessoryText(); + if (strcmp(currentText, Ion::patchLevel()) == 0) { + myCell->setAccessoryText(Ion::pcbVersion()); + } else if (strcmp(currentText, Ion::pcbVersion()) == 0) { myCell->setAccessoryText(Ion::softwareVersion()); - return true; + } else { + assert(strcmp(currentText, Ion::softwareVersion()) == 0); + myCell->setAccessoryText(Ion::patchLevel()); } - myCell->setAccessoryText(Ion::patchLevel()); return true; } if (childLabel == I18n::Message::OmegaVersion) { diff --git a/build/config.mak b/build/config.mak index 27ad582c1..054484cbe 100644 --- a/build/config.mak +++ b/build/config.mak @@ -4,7 +4,7 @@ PLATFORM ?= device DEBUG ?= 0 HOME_DISPLAY_EXTERNALS ?= 1 -EPSILON_VERSION ?= 15.3.1 +EPSILON_VERSION ?= 15.5.0 OMEGA_VERSION ?= 1.22.0 # OMEGA_USERNAME ?= N/A OMEGA_STATE ?= dev @@ -13,7 +13,6 @@ SUBMODULES_APPS = atomic rpn EPSILON_I18N ?= en fr nl pt it de es hu EPSILON_COUNTRIES ?= WW CA DE ES FR GB IT NL PT US EPSILON_GETOPT ?= 0 -EPSILON_TELEMETRY ?= 0 ESCHER_LOG_EVENTS_BINARY ?= 0 THEME_NAME ?= omega_light THEME_REPO ?= local diff --git a/build/platform.blackbox.mak b/build/platform.blackbox.mak index 235feb87a..5c4683d06 100644 --- a/build/platform.blackbox.mak +++ b/build/platform.blackbox.mak @@ -1,4 +1,6 @@ TOOLCHAIN ?= host-gcc USE_LIBA ?= 0 ION_KEYBOARD_LAYOUT = layout_B2 -EXE = bin \ No newline at end of file +EXE = bin + +EPSILON_TELEMETRY ?= 0 \ No newline at end of file diff --git a/build/platform.device.mak b/build/platform.device.mak index 60559e357..34ca783d0 100644 --- a/build/platform.device.mak +++ b/build/platform.device.mak @@ -2,6 +2,8 @@ MODEL ?= n0110 USE_LIBA = 1 EXE = elf +EPSILON_TELEMETRY ?= 0 + BUILD_DIR := $(BUILD_DIR)/$(MODEL) $(BUILD_DIR)/python/port/port.o: CXXFLAGS += -DMP_PORT_USE_STACK_SYMBOLS=1 diff --git a/build/platform.device.n0100.mak b/build/platform.device.n0100.mak index 8f41cc583..debb5dbc9 100644 --- a/build/platform.device.n0100.mak +++ b/build/platform.device.n0100.mak @@ -1,2 +1,3 @@ TOOLCHAIN ?= arm-gcc-m4f ION_KEYBOARD_LAYOUT = layout_B2 +PCB_LATEST = 0 diff --git a/build/platform.device.n0110.mak b/build/platform.device.n0110.mak index 8767597e6..256dd180d 100644 --- a/build/platform.device.n0110.mak +++ b/build/platform.device.n0110.mak @@ -1,2 +1,3 @@ TOOLCHAIN ?= arm-gcc-m7f ION_KEYBOARD_LAYOUT = layout_B3 +PCB_LATEST = 343 # PCB version 3.43 diff --git a/build/platform.simulator.linux.mak b/build/platform.simulator.linux.mak index 87e07660d..695fa0b55 100644 --- a/build/platform.simulator.linux.mak +++ b/build/platform.simulator.linux.mak @@ -1,4 +1,4 @@ TOOLCHAIN = host-gcc EXE = bin -EPSILON_SIMULATOR_HAS_LIBPNG = 1 +EPSILON_TELEMETRY ?= 0 diff --git a/build/platform.simulator.macos.mak b/build/platform.simulator.macos.mak index 5f2c492e5..dcfeac04b 100644 --- a/build/platform.simulator.macos.mak +++ b/build/platform.simulator.macos.mak @@ -3,11 +3,10 @@ EXE = bin APPLE_PLATFORM = macos APPLE_PLATFORM_MIN_VERSION = 10.10 +EPSILON_TELEMETRY ?= 0 ARCHS = x86_64 -EPSILON_SIMULATOR_HAS_LIBPNG = 1 - ifdef ARCH BUILD_DIR := $(BUILD_DIR)/$(ARCH) else diff --git a/build/platform.simulator.mak b/build/platform.simulator.mak index c6a904e30..07f1edd9e 100644 --- a/build/platform.simulator.mak +++ b/build/platform.simulator.mak @@ -8,13 +8,4 @@ TARGET ?= $(HOST) BUILD_DIR := $(BUILD_DIR)/$(TARGET) -EPSILON_SIMULATOR_HAS_LIBPNG ?= 0 - include build/platform.simulator.$(TARGET).mak - -SFLAGS += -DEPSILON_SIMULATOR_HAS_LIBPNG=$(EPSILON_SIMULATOR_HAS_LIBPNG) - -ifeq ($(EPSILON_SIMULATOR_HAS_LIBPNG),1) -SFLAGS += `libpng-config --cflags` -LDFLAGS += `libpng-config --ldflags` -endif diff --git a/build/platform.simulator.web.mak b/build/platform.simulator.web.mak index aa105edbd..f3433497e 100644 --- a/build/platform.simulator.web.mak +++ b/build/platform.simulator.web.mak @@ -1,4 +1,4 @@ TOOLCHAIN = emscripten EXE = js -HANDY_TARGETS_EXTENSIONS += zip +EPSILON_TELEMETRY ?= 0 diff --git a/build/platform.simulator.windows.mak b/build/platform.simulator.windows.mak index 1d71772f4..a1718feac 100644 --- a/build/platform.simulator.windows.mak +++ b/build/platform.simulator.windows.mak @@ -1,2 +1,4 @@ TOOLCHAIN = windows EXE = exe + +EPSILON_TELEMETRY ?= 0 diff --git a/build/rules.mk b/build/rules.mk index 9089cebce..050c4b805 100644 --- a/build/rules.mk +++ b/build/rules.mk @@ -14,7 +14,7 @@ $(eval $(call rule_for, \ $(eval $(call rule_for, \ CPP, %, %.inc, \ - $$(CPP) -P $$< $$@, \ + $$(CPP) $$(addprefix -I,$$(dir $$^)) -P $$< $$@, \ global \ )) @@ -60,6 +60,12 @@ $(eval $(call rule_for, \ global \ )) +$(eval $(call rule_for, \ + ZIP, %.zip, , \ + rm -rf $$(basename $$@) && mkdir -p $$(basename $$@) && cp $$^ $$(basename $$@) && zip -r -9 -j $$@ $$(basename $$@) > /dev/null && rm -rf $$(basename $$@), \ + global \ +)) + ifdef EXE ifeq ($(OS),Windows_NT) # Work around command-line length limit diff --git a/build/targets.device.mak b/build/targets.device.mak index 9498371c4..891b1d097 100644 --- a/build/targets.device.mak +++ b/build/targets.device.mak @@ -54,21 +54,3 @@ $(BUILD_DIR)/bench.ram.$(EXE): LDFLAGS += -Lion/src/$(PLATFORM)/bench $(BUILD_DIR)/bench.ram.$(EXE): LDSCRIPT = ion/src/$(PLATFORM)/shared/ram.ld $(BUILD_DIR)/bench.flash.$(EXE): $(call flavored_object_for,$(bench_src),consoleuart usbxip) $(BUILD_DIR)/bench.flash.$(EXE): LDSCRIPT = ion/src/$(PLATFORM)/$(MODEL)/internal_flash.ld - -.PHONY: %.two_binaries -%.two_binaries: %.elf - @echo "Building an internal and an external binary for $<" - $(Q) $(OBJCOPY) -O binary -j .text.external -j .rodata.external -j .exam_mode_buffer $< $(basename $<).external.bin - $(Q) $(OBJCOPY) -O binary -R .text.external -R .rodata.external -R .exam_mode_buffer $< $(basename $<).internal.bin - @echo "Padding $(basename $<).external.bin and $(basename $<).internal.bin" - $(Q) printf "\xFF\xFF\xFF\xFF" >> $(basename $<).external.bin - $(Q) printf "\xFF\xFF\xFF\xFF" >> $(basename $<).internal.bin - -.PHONY: binpack -binpack: $(BUILD_DIR)/flasher.light.bin $(BUILD_DIR)/epsilon.onboarding.two_binaries - rm -rf $(BUILD_DIR)/binpack - mkdir -p $(BUILD_DIR)/binpack - cp $(BUILD_DIR)/flasher.light.bin $(BUILD_DIR)/binpack - cp $(BUILD_DIR)/epsilon.onboarding.internal.bin $(BUILD_DIR)/epsilon.onboarding.external.bin $(BUILD_DIR)/binpack - cd $(BUILD_DIR) && for binary in flasher.light.bin epsilon.onboarding.internal.bin epsilon.onboarding.external.bin; do shasum -a 256 -b binpack/$${binary} > binpack/$${binary}.sha256;done - cd $(BUILD_DIR) && tar cvfz binpack-$(MODEL)-`git rev-parse HEAD | head -c 7`.tgz binpack/* diff --git a/build/targets.device.n0110.mak b/build/targets.device.n0110.mak index 8860e90ba..27d4c8805 100644 --- a/build/targets.device.n0110.mak +++ b/build/targets.device.n0110.mak @@ -20,3 +20,38 @@ $(BUILD_DIR)/test.external_flash.write.$(EXE): $(BUILD_DIR)/quiz/src/test_ion_ex fi $(Q) $(PYTHON) build/device/dfu.py -u $(word 1,$^) +.PHONY: %.two_binaries +%.two_binaries: %.elf + @echo "Building an internal and an external binary for $<" + $(Q) $(OBJCOPY) -O binary -j .text.external -j .rodata.external -j .exam_mode_buffer $(BUILD_DIR)/$< $(BUILD_DIR)/$(basename $<).external.bin + $(Q) $(OBJCOPY) -O binary -R .text.external -R .rodata.external -R .exam_mode_buffer $(BUILD_DIR)/$< $(BUILD_DIR)/$(basename $<).internal.bin + @echo "Padding $(basename $<).external.bin and $(basename $<).internal.bin" + $(Q) printf "\xFF\xFF\xFF\xFF" >> $(BUILD_DIR)/$(basename $<).external.bin + $(Q) printf "\xFF\xFF\xFF\xFF" >> $(BUILD_DIR)/$(basename $<).internal.bin + +USE_IN_FACTORY := 1 + +.PHONY: binpack +binpack: +ifndef USE_IN_FACTORY + @echo "CAUTION: You are building a binpack." + @echo "You must specify where this binpack will be used." + @echo "Please set the USE_IN_FACTORY environment variable to either:" + @echo " - 0 for use in diagnostic" + @echo " - 1 for use in production" + @exit -1 +endif + rm -rf output/binpack + mkdir -p output/binpack + $(MAKE) clean + $(MAKE) IN_FACTORY=$(USE_IN_FACTORY) $(BUILD_DIR)/flasher.light.bin + cp $(BUILD_DIR)/flasher.light.bin output/binpack + $(MAKE) IN_FACTORY=$(USE_IN_FACTORY) $(BUILD_DIR)/bench.flash.bin + $(MAKE) IN_FACTORY=$(USE_IN_FACTORY) $(BUILD_DIR)/bench.ram.bin + cp $(BUILD_DIR)/bench.ram.bin $(BUILD_DIR)/bench.flash.bin output/binpack + $(MAKE) IN_FACTORY=$(USE_IN_FACTORY) epsilon.official.onboarding.update.two_binaries + cp $(BUILD_DIR)/epsilon.official.onboarding.update.internal.bin $(BUILD_DIR)/epsilon.official.onboarding.update.external.bin output/binpack + $(MAKE) clean + cd output && for binary in flasher.light.bin bench.flash.bin bench.ram.bin epsilon.official.onboarding.update.internal.bin epsilon.official.onboarding.update.external.bin; do shasum -a 256 -b binpack/$${binary} > binpack/$${binary}.sha256;done + cd output && tar cvfz binpack-`git rev-parse HEAD | head -c 7`.tgz binpack + rm -rf output/binpack diff --git a/build/targets.simulator.mak b/build/targets.simulator.mak index 6e6ed0fe6..55101ffc2 100644 --- a/build/targets.simulator.mak +++ b/build/targets.simulator.mak @@ -1,8 +1 @@ -# Headless targets -$(eval $(call rule_for_epsilon_flavor,headless)) -HANDY_TARGETS += epsilon.headless - -$(BUILD_DIR)/test.headless.$(EXE): $(call flavored_object_for,$(test_runner_src),headless) -HANDY_TARGETS += test.headless - -include build/targets.simulator.$(TARGET).mak diff --git a/build/targets.simulator.web.mak b/build/targets.simulator.web.mak index ead0b515e..123ea969f 100644 --- a/build/targets.simulator.web.mak +++ b/build/targets.simulator.web.mak @@ -1 +1,13 @@ -$(BUILD_DIR)/test.headless.js: EMSCRIPTEN_MODULARIZE = 0 +$(BUILD_DIR)/test.js: EMSCRIPTEN_MODULARIZE = 0 + +HANDY_TARGETS += htmlpack htmlpack.official +HANDY_TARGETS_EXTENSIONS += zip + +htmlpack_targets = .\ + .official. \ + +define rule_htmlpack +$$(BUILD_DIR)/htmlpack$(1)zip: $$(addprefix $$(BUILD_DIR)/ion/src/simulator/web/,calculator.html calculator.css) $$(BUILD_DIR)/epsilon$(1)js ion/src/simulator/web/calculator.js +endef + +$(foreach target,$(htmlpack_targets),$(eval $(call rule_htmlpack,$(target)))) diff --git a/ion/Makefile b/ion/Makefile index b0e5bc80a..45461e1c7 100644 --- a/ion/Makefile +++ b/ion/Makefile @@ -25,7 +25,6 @@ initializer_list = $(shell echo $(1) | sed "s/\(.\)/'\1',/g")0 $(call object_for,ion/src/shared/platform_info.cpp): SFLAGS += -DPATCH_LEVEL="$(call initializer_list,$(PATCH_LEVEL))" -DEPSILON_VERSION="$(call initializer_list,$(EPSILON_VERSION))" -DOMEGA_VERSION="$(call initializer_list,$(OMEGA_VERSION))" -DOMEGA_USERNAME="$(call initializer_list,$(OMEGA_USERNAME))" ion_src += $(addprefix ion/src/shared/, \ - console_display.cpp:+consoledisplay \ console_line.cpp \ crc32_eat_byte.cpp \ decompress.cpp \ diff --git a/ion/include/ion.h b/ion/include/ion.h index fb6bac017..854c3c539 100644 --- a/ion/include/ion.h +++ b/ion/include/ion.h @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -37,6 +38,7 @@ const char * softwareVersion(); const char * omegaVersion(); const char * patchLevel(); const char * fccId(); +const char * pcbVersion(); // CRC32 : non xor-ed, non reversed, direct, polynomial 4C11DB7 uint32_t crc32Word(const uint32_t * data, size_t length); // Only accepts whole 32bit values diff --git a/ion/include/ion/board.h b/ion/include/ion/board.h new file mode 100644 index 000000000..6c7f09a42 --- /dev/null +++ b/ion/include/ion/board.h @@ -0,0 +1,12 @@ +#ifndef ION_BOARD_H +#define ION_BOARD_H + +namespace Ion { +namespace Board { + +void lockUnlockedPCBVersion(); + +} +} + +#endif diff --git a/ion/include/ion/events.h b/ion/include/ion/events.h index d74273038..1764eb595 100644 --- a/ion/include/ion/events.h +++ b/ion/include/ion/events.h @@ -53,6 +53,18 @@ enum class ShiftAlphaStatus { // Timeout is decremented Event getEvent(int * timeout); +#if ION_EVENTS_JOURNAL +class Journal { +public: + virtual void pushEvent(Event e) = 0; + virtual Event popEvent() = 0; + virtual bool isEmpty() = 0; +}; + +void replayFrom(Journal * l); +void logTo(Journal * l); +#endif + ShiftAlphaStatus shiftAlphaStatus(); void setShiftAlphaStatus(ShiftAlphaStatus s); void removeShift(); diff --git a/ion/src/device/Makefile b/ion/src/device/Makefile index b4f6a6b11..963690440 100644 --- a/ion/src/device/Makefile +++ b/ion/src/device/Makefile @@ -12,7 +12,9 @@ endif ion_src += ion/src/shared/collect_registers.cpp -ION_DEVICE_SFLAGS = -Iion/src/device/$(MODEL) -Iion/src/device/shared +IN_FACTORY ?= 0 + +ION_DEVICE_SFLAGS = -Iion/src/device/$(MODEL) -Iion/src/device/shared -DPCB_LATEST=$(PCB_LATEST) -DIN_FACTORY=$(IN_FACTORY) $(call object_for,$(ion_device_src) $(ion_device_flasher_src) $(ion_device_bench_src)): SFLAGS += $(ION_DEVICE_SFLAGS) diff --git a/ion/src/device/bench/Makefile b/ion/src/device/bench/Makefile index 523883299..9d3a22d2f 100644 --- a/ion/src/device/bench/Makefile +++ b/ion/src/device/bench/Makefile @@ -29,4 +29,5 @@ ion_device_bench_src += $(addprefix ion/src/device/bench/command/, \ standby.cpp \ usb_plugged.cpp \ vblank.cpp \ + write_pcb_version.cpp \ ) diff --git a/ion/src/device/bench/bench.cpp b/ion/src/device/bench/bench.cpp index 8bf29de66..f732a3116 100644 --- a/ion/src/device/bench/bench.cpp +++ b/ion/src/device/bench/bench.cpp @@ -31,6 +31,7 @@ constexpr CommandHandler handles[] = { CommandHandler("STANDBY", Command::Standby), CommandHandler("USB_PLUGGED", Command::USBPlugged), CommandHandler("VBLANK", Command::VBlank), + CommandHandler("WRITE_PCB_VERSION", Command::WritePCBVersion), CommandHandler(nullptr, nullptr) }; diff --git a/ion/src/device/bench/command/command.h b/ion/src/device/bench/command/command.h index a179d7454..3067fe824 100644 --- a/ion/src/device/bench/command/command.h +++ b/ion/src/device/bench/command/command.h @@ -24,6 +24,7 @@ void LCDPins(const char * input); void LCDTiming(const char * input); void LED(const char * input); void MCUSerial(const char * input); +void WritePCBVersion(const char * input); void Ping(const char * input); void Print(const char * input); void ScreenID(const char * input); diff --git a/ion/src/device/bench/command/write_pcb_version.cpp b/ion/src/device/bench/command/write_pcb_version.cpp new file mode 100644 index 000000000..e8399e001 --- /dev/null +++ b/ion/src/device/bench/command/write_pcb_version.cpp @@ -0,0 +1,34 @@ +#include "command.h" +#include + +namespace Ion { +namespace Device { +namespace Bench { +namespace Command { + +void WritePCBVersion(const char * input) { + if (input != nullptr) { + reply(sSyntaxError); + return; + } + + /* When running the bench for a diagnostic, we must absolutely not write the + * OTP, as N0110 built prior to the PCB revision would still have their OTP + * blank and unlocked. */ +#if IN_FACTORY + Board::writePCBVersion(PCB_LATEST); + /* Read directly from memory, as when IN_FACTORY is true, the method + * pcbVersion always returns PCB_LATEST. */ + if (Board::readPCBVersionInMemory() != PCB_LATEST) { + reply(sKO); + return; + } + Board::lockPCBVersion(); +#endif + reply(sOK); +} + +} +} +} +} diff --git a/ion/src/device/n0100/Makefile b/ion/src/device/n0100/Makefile index 4111b2025..ccacc84ab 100644 --- a/ion/src/device/n0100/Makefile +++ b/ion/src/device/n0100/Makefile @@ -1,8 +1,10 @@ ion_device_src += $(addprefix ion/src/device/n0100/drivers/, \ board.cpp \ + external_flash.cpp \ led.cpp \ power.cpp \ reset.cpp \ + usb.cpp \ ) LDSCRIPT ?= ion/src/device/n0100/flash.ld diff --git a/ion/src/device/n0100/drivers/board.cpp b/ion/src/device/n0100/drivers/board.cpp index a70b48e61..3e37fcb41 100644 --- a/ion/src/device/n0100/drivers/board.cpp +++ b/ion/src/device/n0100/drivers/board.cpp @@ -231,6 +231,23 @@ void shutdownClocks(bool keepLEDAwake) { RCC.AHB1ENR()->set(ahb1enr); } +/* The following methods regarding PCB version are dummy implementations. + * Handling the PCB version is only necessary on the N0110. */ + +PCBVersion pcbVersion() { + return PCB_LATEST; +} + +PCBVersion readPCBVersionInMemory() { + return PCB_LATEST; +} + +void writePCBVersion(PCBVersion) {} + +void lockPCBVersion() {} + +bool pcbVersionIsLocked() { return true; } + } } } diff --git a/ion/src/device/n0100/drivers/external_flash.cpp b/ion/src/device/n0100/drivers/external_flash.cpp new file mode 100644 index 000000000..3c86f5bdf --- /dev/null +++ b/ion/src/device/n0100/drivers/external_flash.cpp @@ -0,0 +1,18 @@ +#include + +namespace Ion { +namespace Device { +namespace ExternalFlash { + +void init() {} +void shutdown() {} + +void MassErase() {} +int SectorAtAddress(uint32_t) { return 0; } +void EraseSector(int) {} +void WriteMemory(uint8_t *, const uint8_t *, size_t) {} +void JDECid(uint8_t *, uint8_t *, uint8_t *) {} + +} +} +} diff --git a/ion/src/device/n0100/drivers/usb.cpp b/ion/src/device/n0100/drivers/usb.cpp new file mode 100644 index 000000000..198402779 --- /dev/null +++ b/ion/src/device/n0100/drivers/usb.cpp @@ -0,0 +1,14 @@ +#include +#include + +namespace Ion { +namespace Device { +namespace USB { + +void initVbus() { + Config::VbusPin.init(); +} + +} +} +} diff --git a/ion/src/device/n0110/Makefile b/ion/src/device/n0110/Makefile index afdfcb7ce..3e6297f6a 100644 --- a/ion/src/device/n0110/Makefile +++ b/ion/src/device/n0110/Makefile @@ -1,9 +1,11 @@ ion_device_src += $(addprefix ion/src/device/n0110/drivers/, \ board.cpp \ cache.cpp \ + external_flash.cpp \ led.cpp \ power.cpp \ reset.cpp \ + usb.cpp \ ) LDSCRIPT ?= ion/src/device/n0110/flash.ld diff --git a/ion/src/device/n0110/drivers/board.cpp b/ion/src/device/n0110/drivers/board.cpp index 9a6c1971a..0ed24868c 100644 --- a/ion/src/device/n0110/drivers/board.cpp +++ b/ion/src/device/n0110/drivers/board.cpp @@ -1,6 +1,8 @@ #include #include +#include #include +#include #include #include #include @@ -372,6 +374,44 @@ void shutdownClocks(bool keepLEDAwake) { RCC.AHB1ENR()->set(ahb1enr); } +constexpr int k_pcbVersionOTPIndex = 0; + +/* As we want the PCB versions to be in ascending order chronologically, and + * because the OTP are initialized with 1s, we store the bitwise-not of the + * version number. This way, devices with blank OTP are considered version 0. */ + +PCBVersion pcbVersion() { +#if IN_FACTORY + /* When flashing for the first time, we want all systems that depend on the + * PCB version to function correctly before flashing the PCB version. This + * way, flashing the PCB version can be done last. */ + return PCB_LATEST; +#else + PCBVersion version = readPCBVersionInMemory(); + return (version == k_alternateBlankVersion ? 0 : version); +#endif +} + +PCBVersion readPCBVersionInMemory() { + return ~(*reinterpret_cast(InternalFlash::Config::OTPAddress(k_pcbVersionOTPIndex))); +} + +void writePCBVersion(PCBVersion version) { + uint8_t * destination = reinterpret_cast(InternalFlash::Config::OTPAddress(k_pcbVersionOTPIndex)); + PCBVersion formattedVersion = ~version; + InternalFlash::WriteMemory(destination, reinterpret_cast(&formattedVersion), sizeof(formattedVersion)); +} + +void lockPCBVersion() { + uint8_t * destination = reinterpret_cast(InternalFlash::Config::OTPLockAddress(k_pcbVersionOTPIndex)); + uint8_t zero = 0; + InternalFlash::WriteMemory(destination, &zero, sizeof(zero)); +} + +bool pcbVersionIsLocked() { + return *reinterpret_cast(InternalFlash::Config::OTPLockAddress(k_pcbVersionOTPIndex)) == 0; +} + } } } diff --git a/ion/src/device/n0110/drivers/config/internal_flash.h b/ion/src/device/n0110/drivers/config/internal_flash.h index fc494c00d..9fcbeed14 100644 --- a/ion/src/device/n0110/drivers/config/internal_flash.h +++ b/ion/src/device/n0110/drivers/config/internal_flash.h @@ -16,6 +16,13 @@ constexpr static uint32_t SectorAddresses[NumberOfSectors+1] = { 0x08010000 }; +constexpr static uint32_t OTPStartAddress = 0x1FF07800; +constexpr static uint32_t OTPLocksAddress = 0x1FF07A00; +constexpr static int NumberOfOTPBlocks = 16; +constexpr static uint32_t OTPBlockSize = 0x20; +constexpr uint32_t OTPAddress(int block) { return OTPStartAddress + block * OTPBlockSize; }; +constexpr uint32_t OTPLockAddress(int block) { return OTPLocksAddress + block; } + } } } diff --git a/ion/src/device/n0110/drivers/config/usb.h b/ion/src/device/n0110/drivers/config/usb.h index bbd3fa5a1..0c63ed474 100644 --- a/ion/src/device/n0110/drivers/config/usb.h +++ b/ion/src/device/n0110/drivers/config/usb.h @@ -10,7 +10,14 @@ namespace Config { using namespace Regs; +/* On the STM32F730, PA9 does not actually support alternate function 10. + * However, because of the wiring of the USB connector on old N0110, detection + * of when the device is plugged required the use of this undocumented setting. + * After the revision of the USB connector and ESD protection, we can now + * follow the specification and configure the Vbus pin as a floating-input GPIO. + */ constexpr static AFGPIOPin VbusPin = AFGPIOPin(GPIOA, 9, GPIO::AFR::AlternateFunction::AF10, GPIO::PUPDR::Pull::None, GPIO::OSPEEDR::OutputSpeed::Fast); + constexpr static AFGPIOPin DmPin = AFGPIOPin(GPIOA, 11, GPIO::AFR::AlternateFunction::AF10, GPIO::PUPDR::Pull::None, GPIO::OSPEEDR::OutputSpeed::Fast); constexpr static AFGPIOPin DpPin = AFGPIOPin(GPIOA, 12, GPIO::AFR::AlternateFunction::AF10, GPIO::PUPDR::Pull::None, GPIO::OSPEEDR::OutputSpeed::Fast); diff --git a/ion/src/device/shared/drivers/external_flash.cpp b/ion/src/device/n0110/drivers/external_flash.cpp similarity index 68% rename from ion/src/device/shared/drivers/external_flash.cpp rename to ion/src/device/n0110/drivers/external_flash.cpp index a2b6cfe59..34de8b591 100644 --- a/ion/src/device/shared/drivers/external_flash.cpp +++ b/ion/src/device/n0110/drivers/external_flash.cpp @@ -1,4 +1,5 @@ -#include "external_flash.h" +#include +#include #include #include #include @@ -9,9 +10,9 @@ namespace ExternalFlash { using namespace Regs; -/* The external flash and the Quad-SPI peripheral support - * several operating modes, corresponding to different numbers of signals - * used to communicate during each phase of the command sequence. +/* The external flash and the Quad-SPI peripheral support several operating + * modes, corresponding to different numbers of signals used to communicate + * during each phase of the command sequence. * * Mode name for | Number of signals used during each phase: * external flash | Instruction | Address | Alt. bytes | Data @@ -23,8 +24,8 @@ using namespace Regs; * Quad-I/O SPI | 1 | 4 | 4 | 4 * QPI | 4 | 4 | 4 | 4 * - * The external flash supports clock frequencies up to 104MHz for all instructions, - * except for Read Data (0x03) which is supported up to 50Mhz. + * The external flash supports clock frequencies up to 104MHz for all + * instructions, except for Read Data (0x03) which is supported up to 50Mhz. * * * Quad-SPI block diagram @@ -37,47 +38,45 @@ using namespace Regs; * matrix --> | register --> FIFO | --> | | * +----------------------+ write +------------+ * - * Any data transmitted to or from the external flash memory go through a 32-byte FIFO. + * Any data transmitted to or from the external flash memory go through a + * 32-byte FIFO. * - * Read or write operations are performed in burst mode, that is, - * after any data byte is transmitted between the Quad-SPI and the flash memory, - * the latter automatically increments the specified address and - * the next byte to read or write is respectively pushed in or popped from the FIFO. - * and so on, as long as the clock continues. + * Read or write operations are performed in burst mode, that is, after any data + * byte is transmitted between the Quad-SPI and the flash memory, the latter + * automatically increments the specified address and the next byte to read or + * write is respectively pushed in or popped from the FIFO. + * And so on, as long as the clock continues. * * If the FIFO gets full in a read operation or * if the FIFO gets empty in a write operation, * the operation stalls and CLK stays low until firmware services the FIFO. * - * If the FIFO gets full in a write operation, - * the operation is stalled until the FIFO has enough space to accept the amount of data being written. - * If the FIFO does not have as many bytes as requested by the read operation and if BUSY=1, - * the operation is stalled until enough data is present or until the transfer is complete, whichever happens first. */ + * If the FIFO gets full in a write operation, the operation is stalled until + * the FIFO has enough space to accept the amount of data being written. + * If the FIFO does not have as many bytes as requested by the read operation + * and if BUSY=1, the operation is stalled until enough data is present or until + * the transfer is complete, whichever happens first. */ enum class Command : uint8_t { - ReadStatusRegister1 = 0x05, - ReadStatusRegister2 = 0x35, - WriteStatusRegister = 0x01, - WriteStatusRegister2 = 0x31, - WriteEnable = 0x06, - ReadData = 0x03, - FastRead = 0x0B, - FastReadQuadIO = 0xEB, - // Program previously erased memory areas as being "0" - PageProgram = 0x02, - QuadPageProgram = 0x33, - EnableQPI = 0x38, - EnableReset = 0x66, - Reset = 0x99, - // Erase the whole chip or a 64-Kbyte block as being "1" - ChipErase = 0xC7, - Erase4KbyteBlock = 0x20, - Erase32KbyteBlock = 0x52, - Erase64KbyteBlock = 0xD8, - SetReadParameters = 0xC0, - DeepPowerDown = 0xB9, - ReleaseDeepPowerDown = 0xAB, - ReadJEDECID = 0x9F + WriteStatusRegister = 0x01, + PageProgram = 0x02, // Program previously erased memory areas as being "0" + ReadData = 0x03, + ReadStatusRegister1 = 0x05, + WriteEnable = 0x06, + Erase4KbyteBlock = 0x20, + WriteStatusRegister2 = 0x31, + QuadPageProgramW25Q64JV = 0x32, + QuadPageProgramAT25F641 = 0x33, + ReadStatusRegister2 = 0x35, + Erase32KbyteBlock = 0x52, + EnableReset = 0x66, + Reset = 0x99, + ReadJEDECID = 0x9F, + ReleaseDeepPowerDown = 0xAB, + DeepPowerDown = 0xB9, + ChipErase = 0xC7, // Erase the whole chip or a 64-Kbyte block as being "1" + Erase64KbyteBlock = 0xD8, + FastReadQuadIO = 0xEB }; static constexpr uint8_t NumberOfAddressBitsIn64KbyteBlock = 16; @@ -98,53 +97,95 @@ public: }; }; -static constexpr QUADSPI::CCR::OperatingMode DefaultOperatingMode = QUADSPI::CCR::OperatingMode::Quad; -static constexpr int ClockFrequencyDivisor = 2; -static constexpr bool ajustNumberOfDummyCycles = Clocks::Config::AHBFrequency > (80 * ClockFrequencyDivisor); -static constexpr int FastReadDummyCycles = (DefaultOperatingMode == QUADSPI::CCR::OperatingMode::Quad && ajustNumberOfDummyCycles) ? 4 : 2; +class OperatingModes { +public: + constexpr OperatingModes( + QUADSPI::CCR::OperatingMode instruction, + QUADSPI::CCR::OperatingMode address, + QUADSPI::CCR::OperatingMode data) : + m_instructionOperatingMode(instruction), + m_addressOperatingMode(address), + m_dataOperatingMode(data) + {} + QUADSPI::CCR::OperatingMode instructionOperatingMode() const { return m_instructionOperatingMode; } + QUADSPI::CCR::OperatingMode addressOperatingMode() const { return m_addressOperatingMode; } + QUADSPI::CCR::OperatingMode dataOperatingMode() const { return m_dataOperatingMode; } +private: + QUADSPI::CCR::OperatingMode m_instructionOperatingMode; + QUADSPI::CCR::OperatingMode m_addressOperatingMode; + QUADSPI::CCR::OperatingMode m_dataOperatingMode; +}; -static void send_command_full(QUADSPI::CCR::FunctionalMode functionalMode, QUADSPI::CCR::OperatingMode operatingMode, Command c, uint8_t * address, uint32_t altBytes, size_t numberOfAltBytes, uint8_t dummyCycles, uint8_t * data, size_t dataLength); +/* W25Q64JV does not implement QPI-4-4-4, so we always send the instructions on + * one wire only.*/ +static constexpr OperatingModes sOperatingModes100(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::NoData, QUADSPI::CCR::OperatingMode::NoData); +static constexpr OperatingModes sOperatingModes101(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::NoData, QUADSPI::CCR::OperatingMode::Single); +static constexpr OperatingModes sOperatingModes110(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::NoData); +static constexpr OperatingModes sOperatingModes111(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Single); +static constexpr OperatingModes sOperatingModes114(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Quad); +static constexpr OperatingModes sOperatingModes144(QUADSPI::CCR::OperatingMode::Single, QUADSPI::CCR::OperatingMode::Quad, QUADSPI::CCR::OperatingMode::Quad); -static inline void send_command(Command c, QUADSPI::CCR::OperatingMode operatingMode = DefaultOperatingMode) { +static QUADSPI::CCR::OperatingMode sOperatingMode = QUADSPI::CCR::OperatingMode::Single; + +static constexpr int ClockFrequencyDivisor = 2; // F(QUADSPI) = F(AHB) / ClockFrequencyDivisor +static constexpr int FastReadQuadIODummyCycles = 4; // Must be 4 for W25Q64JV (Fig 24.A page 34) and for AT25F641 (table 7.19 page 28) +/* According to datasheets, the CS signal should stay high (deselect the device) + * for t_SHSL = 50ns at least. + * -> Max of 30ns (see AT25F641 Sections 8.7 and 8.8), + * 10ns and 50ns (see W25Q64JV Section 9.6). */ +static constexpr float ChipSelectHighTimeInNanoSeconds = 50.0f; + +static void send_command_full( + QUADSPI::CCR::FunctionalMode functionalMode, + OperatingModes operatingModes, + Command c, + uint8_t * address, + uint32_t altBytes, + size_t numberOfAltBytes, + uint8_t dummyCycles, + uint8_t * data, + size_t dataLength); + +static inline void send_command(Command c) { send_command_full( QUADSPI::CCR::FunctionalMode::IndirectWrite, - operatingMode, + sOperatingModes100, c, reinterpret_cast(FlashAddressSpaceSize), 0, 0, 0, - nullptr, 0 - ); + nullptr, 0); } -static inline void send_write_command(Command c, uint8_t * address, const uint8_t * data, size_t dataLength, QUADSPI::CCR::OperatingMode operatingMode = DefaultOperatingMode) { +static inline void send_write_command(Command c, uint8_t * address, const uint8_t * data, size_t dataLength, OperatingModes operatingModes) { send_command_full( QUADSPI::CCR::FunctionalMode::IndirectWrite, - operatingMode, + operatingModes, c, address, 0, 0, 0, - const_cast(data), dataLength - ); + const_cast(data), dataLength); } -static inline void send_read_command(Command c, uint8_t * address, uint8_t * data, size_t dataLength, QUADSPI::CCR::OperatingMode operatingMode = DefaultOperatingMode) { +static inline void send_read_command(Command c, uint8_t * address, uint8_t * data, size_t dataLength) { send_command_full( QUADSPI::CCR::FunctionalMode::IndirectRead, - operatingMode, + sOperatingModes101, c, address, 0, 0, 0, - data, dataLength - ); + data, dataLength); } -static inline void wait(QUADSPI::CCR::OperatingMode operatingMode = DefaultOperatingMode) { +static inline void wait() { + /* The DSB instruction guarantees the completion of a write operation before + * polling the status register. */ + Cache::dsb(); ExternalFlashStatusRegister::StatusRegister1 statusRegister1(0); do { - send_read_command(Command::ReadStatusRegister1, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(&statusRegister1), sizeof(statusRegister1), operatingMode); + send_read_command(Command::ReadStatusRegister1, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(&statusRegister1), sizeof(statusRegister1)); } while (statusRegister1.getBUSY()); } @@ -160,11 +201,11 @@ static void set_as_memory_mapped() { * (Flash memories tend to consume more when nCS is held low.) */ send_command_full( QUADSPI::CCR::FunctionalMode::MemoryMapped, - DefaultOperatingMode, + sOperatingModes144, Command::FastReadQuadIO, reinterpret_cast(FlashAddressSpaceSize), 0xA0, 1, - FastReadDummyCycles, + FastReadQuadIODummyCycles, nullptr, 0 ); } @@ -174,16 +215,16 @@ static void unset_memory_mapped_mode() { uint8_t dummyData; send_command_full( QUADSPI::CCR::FunctionalMode::IndirectRead, - DefaultOperatingMode, + sOperatingModes144, Command::FastReadQuadIO, 0, ~(0xA0), 1, - FastReadDummyCycles, + FastReadQuadIODummyCycles, &dummyData, 1 ); } -static void send_command_full(QUADSPI::CCR::FunctionalMode functionalMode, QUADSPI::CCR::OperatingMode operatingMode, Command c, uint8_t * address, uint32_t altBytes, size_t numberOfAltBytes, uint8_t dummyCycles, uint8_t * data, size_t dataLength) { +static void send_command_full(QUADSPI::CCR::FunctionalMode functionalMode, OperatingModes operatingModes, Command c, uint8_t * address, uint32_t altBytes, size_t numberOfAltBytes, uint8_t dummyCycles, uint8_t * data, size_t dataLength) { /* According to ST's Errata Sheet ES0360, "Wrong data can be read in * memory-mapped after an indirect mode operation". This is the workaround. */ if (functionalMode == QUADSPI::CCR::FunctionalMode::MemoryMapped) { @@ -214,22 +255,22 @@ static void send_command_full(QUADSPI::CCR::FunctionalMode functionalMode, QUADS class QUADSPI::CCR ccr(0); ccr.setFMODE(functionalMode); if (data != nullptr || functionalMode == QUADSPI::CCR::FunctionalMode::MemoryMapped) { - ccr.setDMODE(operatingMode); + ccr.setDMODE(operatingModes.dataOperatingMode()); } if (functionalMode != QUADSPI::CCR::FunctionalMode::MemoryMapped) { QUADSPI.DLR()->set((dataLength > 0) ? dataLength-1 : 0); } ccr.setDCYC(dummyCycles); if (numberOfAltBytes > 0) { - ccr.setABMODE(operatingMode); + ccr.setABMODE(operatingModes.addressOperatingMode()); // Seems to always be the same as address mode ccr.setABSIZE(static_cast(numberOfAltBytes - 1)); QUADSPI.ABR()->set(altBytes); } if (address != reinterpret_cast(FlashAddressSpaceSize) || functionalMode == QUADSPI::CCR::FunctionalMode::MemoryMapped) { - ccr.setADMODE(operatingMode); + ccr.setADMODE(operatingModes.addressOperatingMode()); ccr.setADSIZE(QUADSPI::CCR::Size::ThreeBytes); } - ccr.setIMODE(operatingMode); + ccr.setIMODE(operatingModes.instructionOperatingMode()); ccr.setINSTRUCTION(static_cast(c)); if (functionalMode == QUADSPI::CCR::FunctionalMode::MemoryMapped) { ccr.setSIOO(true); @@ -275,11 +316,8 @@ static void initQSPI() { // Configure controller for target device class QUADSPI::DCR dcr(0); dcr.setFSIZE(NumberOfAddressBitsInChip - 1); - /* According to the device's datasheet (see Sections 8.7 and 8.8), the CS - * signal should stay high (deselect the device) for t_SHSL = 30ns at least. - * */ - constexpr int ChipSelectHighTime = (30 * Clocks::Config::AHBFrequency + ClockFrequencyDivisor * 1000 - 1) / (ClockFrequencyDivisor * 1000); - dcr.setCSHT(ChipSelectHighTime - 1); + constexpr int ChipSelectHighTimeCycles = (ChipSelectHighTimeInNanoSeconds * static_cast(Clocks::Config::AHBFrequency)) / (static_cast(ClockFrequencyDivisor) * 1000.0f) + 1.0f; + dcr.setCSHT(ChipSelectHighTimeCycles - 1); dcr.setCKMODE(true); QUADSPI.DCR()->set(dcr); class QUADSPI::CR cr(0); @@ -288,36 +326,20 @@ static void initQSPI() { QUADSPI.CR()->set(cr); } -static QUADSPI::CCR::OperatingMode sOperatingMode = QUADSPI::CCR::OperatingMode::Single; - static void initChip() { // Release sleep deep - send_command(Command::ReleaseDeepPowerDown, sOperatingMode); + send_command(Command::ReleaseDeepPowerDown); Timing::usleep(3); /* The chip initially expects commands in SPI mode. We need to use SPI to tell - * it to switch to QPI. */ - if (sOperatingMode == QUADSPI::CCR::OperatingMode::Single && DefaultOperatingMode == QUADSPI::CCR::OperatingMode::Quad) { - send_command(Command::WriteEnable, QUADSPI::CCR::OperatingMode::Single); + * it to switch to QuadSPI/QPI. */ + if (sOperatingMode == QUADSPI::CCR::OperatingMode::Single) { + send_command(Command::WriteEnable); ExternalFlashStatusRegister::StatusRegister2 statusRegister2(0); statusRegister2.setQE(true); - wait(QUADSPI::CCR::OperatingMode::Single); - send_write_command(Command::WriteStatusRegister2, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(&statusRegister2), sizeof(statusRegister2), QUADSPI::CCR::OperatingMode::Single); - wait(QUADSPI::CCR::OperatingMode::Single); - send_command(Command::EnableQPI, QUADSPI::CCR::OperatingMode::Single); wait(); - if (ajustNumberOfDummyCycles) { - class ReadParameters : Register8 { - public: - /* Parameters sent along with SetReadParameters instruction in order - * to configure the number of dummy cycles for the QPI Read instructions. */ - using Register8::Register8; - REGS_BOOL_FIELD_W(P5, 5); - }; - ReadParameters readParameters(0); - readParameters.setP5(true); - send_write_command(Command::SetReadParameters, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(&readParameters), sizeof(readParameters)); - } + send_write_command(Command::WriteStatusRegister2, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(&statusRegister2), sizeof(statusRegister2), sOperatingModes101); + wait(); sOperatingMode = QUADSPI::CCR::OperatingMode::Quad; } set_as_memory_mapped(); @@ -346,10 +368,10 @@ static void shutdownChip() { send_command(Command::EnableReset); send_command(Command::Reset); sOperatingMode = QUADSPI::CCR::OperatingMode::Single; - Ion::Timing::usleep(30); + Timing::usleep(30); // Sleep deep - send_command(Command::DeepPowerDown, sOperatingMode); + send_command(Command::DeepPowerDown); Timing::usleep(3); } @@ -402,7 +424,7 @@ void unlockFlash() { statusRegister2.setQE(currentStatusRegister2.getQE()); uint8_t registers[] = {statusRegister1.get(), statusRegister2.get()}; - send_write_command(Command::WriteStatusRegister, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(registers), sizeof(registers)); + send_write_command(Command::WriteStatusRegister, reinterpret_cast(FlashAddressSpaceSize), reinterpret_cast(registers), sizeof(registers), sOperatingModes101); wait(); } @@ -428,18 +450,18 @@ void __attribute__((noinline)) EraseSector(int i) { /* WARNING: this code assumes that the flash sectors are of increasing size: * first all 4K sectors, then all 32K sectors, and finally all 64K sectors. */ if (i < Config::NumberOf4KSectors) { - send_write_command(Command::Erase4KbyteBlock, reinterpret_cast(i << NumberOfAddressBitsIn4KbyteBlock), nullptr, 0); + send_write_command(Command::Erase4KbyteBlock, reinterpret_cast(i << NumberOfAddressBitsIn4KbyteBlock), nullptr, 0, sOperatingModes110); } else if (i < Config::NumberOf4KSectors + Config::NumberOf32KSectors) { /* If the sector is the number Config::NumberOf4KSectors, we want to write * at the address 1 << NumberOfAddressBitsIn32KbyteBlock, hence the formula * (i - Config::NumberOf4KSectors + 1). */ - send_write_command(Command::Erase32KbyteBlock, reinterpret_cast((i - Config::NumberOf4KSectors + 1) << NumberOfAddressBitsIn32KbyteBlock), nullptr, 0); + send_write_command(Command::Erase32KbyteBlock, reinterpret_cast((i - Config::NumberOf4KSectors + 1) << NumberOfAddressBitsIn32KbyteBlock), nullptr, 0, sOperatingModes110); } else { /* If the sector is the number * Config::NumberOf4KSectors - Config::NumberOf32KSectors, we want to write * at the address 1 << NumberOfAddressBitsIn32KbyteBlock, hence the formula * (i - Config::NumberOf4KSectors - Config::NumberOf32KSectors + 1). */ - send_write_command(Command::Erase64KbyteBlock, reinterpret_cast((i - Config::NumberOf4KSectors - Config::NumberOf32KSectors + 1) << NumberOfAddressBitsIn64KbyteBlock), nullptr, 0); + send_write_command(Command::Erase64KbyteBlock, reinterpret_cast((i - Config::NumberOf4KSectors - Config::NumberOf32KSectors + 1) << NumberOfAddressBitsIn64KbyteBlock), nullptr, 0, sOperatingModes110); } wait(); set_as_memory_mapped(); @@ -455,7 +477,6 @@ void __attribute__((noinline)) WriteMemory(uint8_t * destination, const uint8_t * However, when the end of a page is reached, the addressing wraps to the beginning. * Hence a Page Program instruction must be issued for each page. */ static constexpr size_t PageSize = 256; - constexpr Command pageProgram = (DefaultOperatingMode == QUADSPI::CCR::OperatingMode::Single) ? Command::PageProgram : Command::QuadPageProgram; uint8_t offset = reinterpret_cast(destination) & (PageSize - 1); size_t lengthThatFitsInPage = PageSize - offset; while (length > 0) { @@ -464,7 +485,12 @@ void __attribute__((noinline)) WriteMemory(uint8_t * destination, const uint8_t } send_command(Command::WriteEnable); wait(); - send_write_command(pageProgram, destination, source, lengthThatFitsInPage); + + /* Some chips implement 0x32 only, others 0x33 only, we call both. This does + * not seem to affect the writing. */ + send_write_command(Command::QuadPageProgramAT25F641, destination, source, lengthThatFitsInPage, sOperatingModes144); + send_write_command(Command::QuadPageProgramW25Q64JV, destination, source, lengthThatFitsInPage, sOperatingModes114); + length -= lengthThatFitsInPage; destination += lengthThatFitsInPage; source += lengthThatFitsInPage; diff --git a/ion/src/device/n0110/drivers/usb.cpp b/ion/src/device/n0110/drivers/usb.cpp new file mode 100644 index 000000000..d56cee475 --- /dev/null +++ b/ion/src/device/n0110/drivers/usb.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +namespace Ion { +namespace Device { + +using namespace Regs; + +namespace USB { + +bool useAlternateFunctionVbus() { + return Board::pcbVersion() == 0; +} + +void initVbus() { + if (useAlternateFunctionVbus()) { + Config::VbusPin.init(); + } else { + Config::VbusPin.group().MODER()->setMode(Config::VbusPin.pin(), GPIO::MODER::Mode::Input); + Config::VbusPin.group().PUPDR()->setPull(Config::VbusPin.pin(), GPIO::PUPDR::Pull::None); + } +} + +} +} +} diff --git a/ion/src/device/shared/drivers/Makefile b/ion/src/device/shared/drivers/Makefile index 847009957..2db043c5f 100644 --- a/ion/src/device/shared/drivers/Makefile +++ b/ion/src/device/shared/drivers/Makefile @@ -5,16 +5,18 @@ ion_device_src += $(addprefix ion/src/device/shared/drivers/, \ board.cpp \ clipboard.cpp \ console_uart.cpp:+consoleuart \ + console_display.cpp:+consoledisplay \ + console_dummy.cpp:-consoledisplay \ console_dummy.cpp:-consoleuart \ crc32.cpp \ display.cpp \ events_keyboard_platform.cpp \ exam_mode.cpp \ - external_flash.cpp \ flash.cpp \ internal_flash.cpp \ keyboard.cpp \ led.cpp \ + pcb_version.cpp \ power.cpp\ random.cpp\ reset.cpp \ diff --git a/ion/src/device/shared/drivers/board.cpp b/ion/src/device/shared/drivers/board.cpp index cb9490b0e..2164d0d6b 100644 --- a/ion/src/device/shared/drivers/board.cpp +++ b/ion/src/device/shared/drivers/board.cpp @@ -94,3 +94,26 @@ void setClockFrequency(Frequency f) { } } } + +namespace Ion { +namespace Board { + +using namespace Device::Board; + +void lockUnlockedPCBVersion() { + if (pcbVersionIsLocked()) { + return; + } + /* PCB version is unlocked : the device is a N0110 that has been + * produced prior to the pcb revision 3.43. */ + PCBVersion version = pcbVersion(); + if (version != 0) { + /* Some garbage has been written in OTP0. We overwrite it fully, which is + * interepreted as blank. */ + writePCBVersion(k_alternateBlankVersion); + } + lockPCBVersion(); +} + +} +} diff --git a/ion/src/device/shared/drivers/board.h b/ion/src/device/shared/drivers/board.h index a643c35b7..61b63b2b3 100644 --- a/ion/src/device/shared/drivers/board.h +++ b/ion/src/device/shared/drivers/board.h @@ -1,6 +1,8 @@ #ifndef ION_DEVICE_SHARED_DRIVERS_BOARD_H #define ION_DEVICE_SHARED_DRIVERS_BOARD_H +#include + namespace Ion { namespace Device { namespace Board { @@ -23,6 +25,20 @@ Frequency standardFrequency(); void setStandardFrequency(Frequency f); void setClockFrequency(Frequency f); +typedef uint32_t PCBVersion; +/* On N0110 released before the PCB revision, OTP0 is supposed to be blank and + * unlocked. However, such a device with something written in OTP0 will be + * unable to configure Vbus properly. In this case, and if OTP0 is still + * unlocked, we fully write OTP0 and treat it the same as fully blank. + * This way, pcbVersion will be 0 if OTP0 is either 0x00000000 or 0xFFFFFFFF.*/ +constexpr PCBVersion k_alternateBlankVersion = 0xFFFFFFFF; + +PCBVersion pcbVersion(); +PCBVersion readPCBVersionInMemory(); +void writePCBVersion(PCBVersion version); +void lockPCBVersion(); +bool pcbVersionIsLocked(); + } } } diff --git a/ion/src/device/shared/drivers/console_display.cpp b/ion/src/device/shared/drivers/console_display.cpp new file mode 100644 index 000000000..b85227bc7 --- /dev/null +++ b/ion/src/device/shared/drivers/console_display.cpp @@ -0,0 +1,39 @@ +#include "console.h" +#include +#include + +namespace Ion { +namespace Console { + +char readChar() { + return 0; +} + +void writeChar(char c) { + KDIonContext::putchar(c); +} + +bool transmissionDone() { + return true; +} + +} +} + +namespace Ion { +namespace Device { +namespace Console { + +void init() { +} + +void shutdown() { +} + +bool peerConnected() { + return false; +} + +} +} +} diff --git a/ion/src/device/shared/drivers/console_dummy.cpp b/ion/src/device/shared/drivers/console_dummy.cpp index 9ed1718f5..bc7eccd22 100644 --- a/ion/src/device/shared/drivers/console_dummy.cpp +++ b/ion/src/device/shared/drivers/console_dummy.cpp @@ -1,4 +1,22 @@ #include "console.h" +#include + +namespace Ion { +namespace Console { + +char readChar() { + return 0; +} + +void writeChar(char c) { +} + +bool transmissionDone() { + return true; +} + +} +} namespace Ion { namespace Device { diff --git a/ion/src/device/shared/drivers/pcb_version.cpp b/ion/src/device/shared/drivers/pcb_version.cpp new file mode 100644 index 000000000..8f0626609 --- /dev/null +++ b/ion/src/device/shared/drivers/pcb_version.cpp @@ -0,0 +1,25 @@ +#include +#include "board.h" + +namespace Ion { + +const char * pcbVersion() { + constexpr int pcbVersionLength = 5; // xx.yy + static char pcbVer[pcbVersionLength] = {'\0'}; + if (pcbVer[0] == '\0') { + Device::Board::PCBVersion ver = Device::Board::pcbVersion(); + /* As PCB version only uses 4 chars, value should be at most 9999. */ + assert(ver < 10000); + for (int i = pcbVersionLength - 1; i >= 0; i--) { + if (i == 2) { + pcbVer[i] = '.'; + } else { + pcbVer[i] = '0' + ver % 10; + ver /= 10; + } + } + } + return pcbVer; +} + +} diff --git a/ion/src/device/shared/drivers/serial_number.cpp b/ion/src/device/shared/drivers/serial_number.cpp index 4d61a6b4c..4330a97c2 100644 --- a/ion/src/device/shared/drivers/serial_number.cpp +++ b/ion/src/device/shared/drivers/serial_number.cpp @@ -1,4 +1,5 @@ #include "serial_number.h" +#include "board.h" #include #include "base64.h" diff --git a/ion/src/device/shared/drivers/usb.cpp b/ion/src/device/shared/drivers/usb.cpp index ec79da42a..c382b0b53 100644 --- a/ion/src/device/shared/drivers/usb.cpp +++ b/ion/src/device/shared/drivers/usb.cpp @@ -65,22 +65,17 @@ void initGPIO() { GPIOC.MODER()->setMode(11, GPIO::MODER::Mode::Output); GPIOC.ODR()->set(11, false); - /* Configure the GPIO - * The VBUS pin is connected to the USB VBUS port. To read if the USB is - * plugged, the pin must be pulled down. */ - // FIXME: Understand how the Vbus pin really works! -#if 0 - Config::VbusPin.group().MODER()->setMode(Config::VbusPin.pin(), GPIO::MODER::Mode::Input); - Config::VbusPin.group().PUPDR()->setPull(Config::VbusPin.pin(), GPIO::PUPDR::Pull::Down); -#else - Config::VbusPin.init(); -#endif + /* Configure the GPIO */ + /* The Vbus pin is connected to the USB Vbus port. Depending on the + * hardware, it should either be configured as an AlternateFunction, or as a + * floating Input. */ + initVbus(); Config::DmPin.init(); Config::DpPin.init(); } void shutdownGPIO() { - constexpr static AFGPIOPin Pins[] = {Config::DpPin, Config::DmPin, Config::VbusPin}; + constexpr static AFGPIOPin Pins[] = { Config::DpPin, Config::DmPin, Config::VbusPin }; for (const AFGPIOPin & p : Pins) { p.shutdown(); } diff --git a/ion/src/device/shared/drivers/usb.h b/ion/src/device/shared/drivers/usb.h index a53beb8a0..df27513e7 100644 --- a/ion/src/device/shared/drivers/usb.h +++ b/ion/src/device/shared/drivers/usb.h @@ -7,6 +7,7 @@ namespace USB { void init(); void shutdown(); +void initVbus(); void initGPIO(); void shutdownGPIO(); void initOTG(); diff --git a/ion/src/device/shared/usb/Makefile b/ion/src/device/shared/usb/Makefile index 3db2b7d51..a79991d74 100644 --- a/ion/src/device/shared/usb/Makefile +++ b/ion/src/device/shared/usb/Makefile @@ -43,7 +43,9 @@ ion_device_dfu_src += libaxx/src/cxxabi/pure_virtual.cpp ion_device_dfu_src += ion/src/device/shared/usb/boot.cpp ion_device_dfu_src += ion/src/device/$(MODEL)/drivers/board.cpp ion_device_dfu_src += ion/src/device/$(MODEL)/drivers/cache.cpp +ion_device_dfu_src += ion/src/device/$(MODEL)/drivers/external_flash.cpp ion_device_dfu_src += ion/src/device/$(MODEL)/drivers/reset.cpp +ion_device_dfu_src += ion/src/device/$(MODEL)/drivers/usb.cpp ion_device_dfu_src += $(addprefix ion/src/device/shared/drivers/, \ backlight.cpp \ battery.cpp \ @@ -53,7 +55,6 @@ ion_device_dfu_src += $(addprefix ion/src/device/shared/drivers/, \ crc32.cpp \ display.cpp \ events_keyboard_platform.cpp \ - external_flash.cpp \ flash.cpp \ internal_flash.cpp \ keyboard.cpp \ diff --git a/ion/src/shared/console_display.cpp b/ion/src/shared/console_display.cpp deleted file mode 100644 index 8be425eb9..000000000 --- a/ion/src/shared/console_display.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include -#include - -namespace Ion { -namespace Console { - -char readChar() { - return '\0'; -} - -static KDPoint cursor = KDPointZero; - -void writeChar(char c) { - char text[2] = {c, 0}; - KDContext * ctx = KDIonContext::sharedContext(); - cursor = ctx->drawString(text, cursor); - if (cursor.y() > Ion::Display::Height) { - cursor = KDPoint(cursor.x(), 0); - } -} - -bool transmissionDone() { - // Always true because we flush after each writeChar - return true; -} - -} -} diff --git a/ion/src/shared/dummy/backlight.cpp b/ion/src/shared/dummy/backlight.cpp new file mode 100644 index 000000000..651f3bf23 --- /dev/null +++ b/ion/src/shared/dummy/backlight.cpp @@ -0,0 +1,24 @@ +#include + +namespace Ion { +namespace Backlight { + +uint8_t brightness() { + return 0; +} + +void setBrightness(uint8_t b) { +} + +void init() { +} + +bool isInitialized() { + return true; +} + +void shutdown() { +} + +} +} diff --git a/ion/src/simulator/shared/dummy/battery.cpp b/ion/src/shared/dummy/battery.cpp similarity index 100% rename from ion/src/simulator/shared/dummy/battery.cpp rename to ion/src/shared/dummy/battery.cpp diff --git a/ion/src/shared/dummy/board.cpp b/ion/src/shared/dummy/board.cpp new file mode 100644 index 000000000..ad38e793a --- /dev/null +++ b/ion/src/shared/dummy/board.cpp @@ -0,0 +1,4 @@ +#include + +void Ion::Board::lockUnlockedPCBVersion() { +} diff --git a/ion/src/shared/dummy/display.cpp b/ion/src/shared/dummy/display.cpp new file mode 100644 index 000000000..a5b075ceb --- /dev/null +++ b/ion/src/shared/dummy/display.cpp @@ -0,0 +1,22 @@ +#include + +namespace Ion { +namespace Display { + +void POSTPushMulticolor(int rootNumberTiles, int tileSize) { +} + +int displayUniformTilingSize10(KDColor c) { + return 0; +} + +int displayColoredTilingSize10() { + return 0; +} + +bool waitForVBlank() { + return true; +} + +} +} diff --git a/ion/src/simulator/shared/dummy/exam_mode.cpp b/ion/src/shared/dummy/exam_mode.cpp similarity index 100% rename from ion/src/simulator/shared/dummy/exam_mode.cpp rename to ion/src/shared/dummy/exam_mode.cpp diff --git a/ion/src/simulator/shared/dummy/fcc_id.cpp b/ion/src/shared/dummy/fcc_id.cpp similarity index 75% rename from ion/src/simulator/shared/dummy/fcc_id.cpp rename to ion/src/shared/dummy/fcc_id.cpp index 4635993d0..daf908c81 100644 --- a/ion/src/simulator/shared/dummy/fcc_id.cpp +++ b/ion/src/shared/dummy/fcc_id.cpp @@ -1,5 +1,4 @@ #include -#include const char * Ion::fccId() { return "NA"; diff --git a/ion/src/simulator/shared/dummy/led.cpp b/ion/src/shared/dummy/led.cpp similarity index 67% rename from ion/src/simulator/shared/dummy/led.cpp rename to ion/src/shared/dummy/led.cpp index 91e0cb0b4..034d048a0 100644 --- a/ion/src/simulator/shared/dummy/led.cpp +++ b/ion/src/shared/dummy/led.cpp @@ -7,9 +7,11 @@ KDColor getColor() { return KDColorBlack; } -void setColor(KDColor c) {} +void setColor(KDColor c) { +} -void setBlinking(uint16_t period, float dutyCycle) {} +void setBlinking(uint16_t period, float dutyCycle) { +} KDColor updateColorWithPlugAndCharge() { return KDColorBlack; diff --git a/ion/src/shared/dummy/pcb_version.cpp b/ion/src/shared/dummy/pcb_version.cpp new file mode 100644 index 000000000..c96193940 --- /dev/null +++ b/ion/src/shared/dummy/pcb_version.cpp @@ -0,0 +1,5 @@ +#include + +const char * Ion::pcbVersion() { + return "00.00"; +} diff --git a/ion/src/shared/dummy/power.cpp b/ion/src/shared/dummy/power.cpp new file mode 100644 index 000000000..f95af846f --- /dev/null +++ b/ion/src/shared/dummy/power.cpp @@ -0,0 +1,13 @@ +#include + +namespace Ion { +namespace Power { + +void suspend(bool checkIfOnOffKeyReleased) { +} + +void standby() { +} + +} +} diff --git a/ion/src/simulator/shared/dummy/serial_number.cpp b/ion/src/shared/dummy/serial_number.cpp similarity index 100% rename from ion/src/simulator/shared/dummy/serial_number.cpp rename to ion/src/shared/dummy/serial_number.cpp diff --git a/ion/src/simulator/shared/dummy/stack.cpp b/ion/src/shared/dummy/stack.cpp similarity index 100% rename from ion/src/simulator/shared/dummy/stack.cpp rename to ion/src/shared/dummy/stack.cpp diff --git a/ion/src/shared/dummy/usb.cpp b/ion/src/shared/dummy/usb.cpp new file mode 100644 index 000000000..ce3c0fd2c --- /dev/null +++ b/ion/src/shared/dummy/usb.cpp @@ -0,0 +1,27 @@ +#include + +namespace Ion { +namespace USB { + +bool isPlugged() { + return false; +} + +bool isEnumerated() { + return false; +} + +void clearEnumerationInterrupt() { +} + +void DFU(bool) { +} + +void enable() { +} + +void disable() { +} + +} +} diff --git a/ion/src/shared/events_keyboard.cpp b/ion/src/shared/events_keyboard.cpp index 29939263d..72d6533ed 100644 --- a/ion/src/shared/events_keyboard.cpp +++ b/ion/src/shared/events_keyboard.cpp @@ -50,7 +50,11 @@ void resetLongRepetition() { ComputeAndSetRepetionFactor(sEventRepetitionCount); } -Event getEvent(int * timeout) { +static Keyboard::Key keyFromState(Keyboard::State state) { + return static_cast(63 - __builtin_clzll(state)); +} + +static inline Event innerGetEvent(int * timeout) { assert(*timeout > delayBeforeRepeat); assert(*timeout > delayBetweenRepeat); int time = 0; @@ -128,5 +132,36 @@ Event getEvent(int * timeout) { } } +#if ION_EVENTS_JOURNAL + +static Journal * sSourceJournal = nullptr; +static Journal * sDestinationJournal = nullptr; +void replayFrom(Journal * l) { sSourceJournal = l; } +void logTo(Journal * l) { sDestinationJournal = l; } + +Event getEvent(int * timeout) { + if (sSourceJournal != nullptr) { + if (sSourceJournal->isEmpty()) { + sSourceJournal = nullptr; + } else { + return sSourceJournal->popEvent(); + } + } + Event e = innerGetEvent(timeout); + if (sDestinationJournal != nullptr) { + sDestinationJournal->pushEvent(e); + } + return e; +} + +#else + +Event getEvent(int * timeout) { + return innerGetEvent(timeout); +} + +#endif + + } } diff --git a/ion/src/simulator/Makefile b/ion/src/simulator/Makefile index fbf8d2d4d..f52acde98 100644 --- a/ion/src/simulator/Makefile +++ b/ion/src/simulator/Makefile @@ -1,31 +1,34 @@ +ion_src += $(addprefix ion/src/shared/dummy/, \ + backlight.cpp \ + board.cpp \ + battery.cpp \ + display.cpp \ + exam_mode.cpp \ + fcc_id.cpp \ + led.cpp \ + pcb_version.cpp \ + power.cpp \ + serial_number.cpp \ + stack.cpp \ + usb.cpp \ +) + ion_src += $(addprefix ion/src/simulator/shared/, \ - dummy/backlight.cpp \ - dummy/battery.cpp \ - dummy/display.cpp \ - dummy/exam_mode.cpp \ - dummy/fcc_id.cpp \ - dummy/led.cpp \ - dummy/serial_number.cpp \ - dummy/stack.cpp \ - dummy/usb.cpp \ clipboard.cpp \ - console_stdio.cpp:-consoledisplay \ + console.cpp \ rtc.cpp \ crc32.cpp \ - display.cpp:-headless \ + display.cpp \ events.cpp \ - events_keyboard.cpp:-headless \ - events_stdin.cpp:+headless \ - framebuffer_base.cpp \ - framebuffer_png.cpp:+headless \ - keyboard_dummy.cpp:+headless \ - keyboard_sdl.cpp:-headless \ - layout.cpp:-headless \ - main_headless.cpp:+headless \ - main_sdl.cpp:-headless \ - power.cpp \ + events_platform.cpp \ + framebuffer.cpp \ + keyboard.cpp \ + layout.cpp \ + main.cpp \ random.cpp \ timing.cpp \ + window.cpp \ + store_script.cpp \ ) ion_simulator_assets = background.jpg horizontal_arrow.png vertical_arrow.png round.png small_squircle.png large_squircle.png @@ -33,3 +36,13 @@ ion_simulator_assets_paths = $(add_prefix ion/src/simulator/assets/,$(ion_simula include ion/src/simulator/$(TARGET)/Makefile include ion/src/simulator/external/Makefile + +SFLAGS += -DION_EVENTS_JOURNAL + +ifeq ($(ION_SIMULATOR_FILES),1) +ion_src += $(addprefix ion/src/simulator/shared/, \ + actions.cpp \ + state_file.cpp \ +) +SFLAGS += -DION_SIMULATOR_FILES=1 +endif diff --git a/ion/src/simulator/android/Makefile b/ion/src/simulator/android/Makefile index ca319a32b..f888d726b 100644 --- a/ion/src/simulator/android/Makefile +++ b/ion/src/simulator/android/Makefile @@ -1,11 +1,13 @@ ion_src += $(addprefix ion/src/simulator/android/src/cpp/, \ - images.cpp \ haptics_enabled.cpp \ + platform_images.cpp \ + platform_language.cpp \ ) ion_src += $(addprefix ion/src/simulator/shared/, \ - dummy/callback.cpp \ - dummy/language.cpp \ + dummy/journal.cpp \ + dummy/keyboard_callback.cpp \ + dummy/window_callback.cpp \ clipboard_helper.cpp \ haptics.cpp \ ) diff --git a/ion/src/simulator/android/src/cpp/haptics_enabled.cpp b/ion/src/simulator/android/src/cpp/haptics_enabled.cpp index 6802d3a45..52a8b5480 100644 --- a/ion/src/simulator/android/src/cpp/haptics_enabled.cpp +++ b/ion/src/simulator/android/src/cpp/haptics_enabled.cpp @@ -1,6 +1,7 @@ #include "../../../shared/haptics.h" #include #include +#include namespace Ion { namespace Simulator { @@ -11,8 +12,15 @@ bool isEnabled() { jobject activity = static_cast(SDL_AndroidGetActivity()); jclass j_class = env->FindClass("io/github/omega/simulator/OmegaActivity"); jmethodID j_methodId = env->GetMethodID(j_class,"hapticFeedbackIsEnabled", "()Z"); - - return env->CallObjectMethod(activity, j_methodId); + assert(j_methodId != 0); + bool result = (env->CallBooleanMethod(activity, j_methodId) != JNI_FALSE); + /* Local references are automatically deleted if a native function called from + * Java side returns. For SDL this native function is main() itself. Therefore + * references need to be manually deleted because otherwise the references + * will first be cleaned if main() returns (application exit). */ + env->DeleteLocalRef(j_class); + env->DeleteLocalRef(activity); + return result; } } diff --git a/ion/src/simulator/android/src/cpp/images.cpp b/ion/src/simulator/android/src/cpp/platform_images.cpp similarity index 77% rename from ion/src/simulator/android/src/cpp/images.cpp rename to ion/src/simulator/android/src/cpp/platform_images.cpp index b96a9647c..86ed18060 100644 --- a/ion/src/simulator/android/src/cpp/images.cpp +++ b/ion/src/simulator/android/src/cpp/platform_images.cpp @@ -2,8 +2,13 @@ #include #include #include +#include -SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identifier) { +namespace Ion { +namespace Simulator { +namespace Platform { + +SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { JNIEnv * env = static_cast(SDL_AndroidGetJNIEnv()); jobject activity = static_cast(SDL_AndroidGetActivity()); @@ -13,6 +18,7 @@ SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identi "retrieveBitmapAsset", "(Ljava/lang/String;)Landroid/graphics/Bitmap;" ); + assert(j_methodId != 0); jstring j_identifier = env->NewStringUTF(identifier); @@ -45,6 +51,14 @@ SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identi SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); AndroidBitmap_unlockPixels(env, j_bitmap); - + // See comment in haptics_enabled.cpp + env->DeleteLocalRef(j_bitmap); + env->DeleteLocalRef(j_identifier); + env->DeleteLocalRef(j_class); + env->DeleteLocalRef(activity); return texture; } + +} +} +} diff --git a/ion/src/simulator/android/src/cpp/platform_language.cpp b/ion/src/simulator/android/src/cpp/platform_language.cpp new file mode 100644 index 000000000..d626939c0 --- /dev/null +++ b/ion/src/simulator/android/src/cpp/platform_language.cpp @@ -0,0 +1,37 @@ +#include "../../../shared/platform.h" +#include +#include + +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * languageCode() { + static char buffer[4] = {0}; + if (buffer[0] == 0) { + JNIEnv * env = static_cast(SDL_AndroidGetJNIEnv()); + jobject activity = static_cast(SDL_AndroidGetActivity()); + + jclass j_class = env->FindClass("com/numworks/calculator/EpsilonActivity"); + jmethodID j_methodId = env->GetMethodID( + j_class, + "retrieveLanguage", + "()Ljava/lang/String;" + ); + assert(j_methodId != 0); + + jstring j_language = static_cast(env->CallObjectMethod(activity, j_methodId)); + const char * language = env->GetStringUTFChars(j_language, nullptr); + memcpy(buffer, language, 4); + buffer[3] = 0; + env->ReleaseStringUTFChars(j_language, language); + env->DeleteLocalRef(j_language); + env->DeleteLocalRef(j_class); + env->DeleteLocalRef(activity); + } + return buffer; +} + +} +} +} diff --git a/ion/src/simulator/android/src/cpp/telemetry.cpp b/ion/src/simulator/android/src/cpp/telemetry.cpp new file mode 100644 index 000000000..54c2fb1c8 --- /dev/null +++ b/ion/src/simulator/android/src/cpp/telemetry.cpp @@ -0,0 +1,66 @@ +#include "../../../shared/telemetry.h" +#include +#include +#include + +static inline JNIEnv * AndroidJNI() { + return static_cast(SDL_AndroidGetJNIEnv()); +} + +static inline jobject AndroidActivity() { + return static_cast(SDL_AndroidGetActivity()); +} + +static inline jstring JS(const char * s, JNIEnv * env) { + return env->NewStringUTF(s); +} + +namespace Ion { +namespace Simulator { +namespace Telemetry { + +void init() { + JNIEnv * env = AndroidJNI(); + + jclass j_class = env->FindClass("com/numworks/calculator/EpsilonActivity"); + jmethodID j_methodId = env->GetMethodID(j_class,"telemetryInit", "()V"); + assert(j_methodId != 0); + + env->CallVoidMethod(AndroidActivity(), j_methodId); + env->DeleteLocalRef(j_class); +} + +void shutdown() { +} + +} +} +} + +namespace Ion { +namespace Telemetry { + +void reportScreen(const char * screenName) { + JNIEnv * env = AndroidJNI(); + + jclass j_class = env->FindClass("com/numworks/calculator/EpsilonActivity"); + jmethodID j_methodId = env->GetMethodID(j_class, "telemetryScreen", "(Ljava/lang/String;)V"); + assert(j_methodId != 0); + + env->CallVoidMethod(AndroidActivity(), j_methodId, JS(screenName, env)); + env->DeleteLocalRef(j_class); +} + +void reportEvent(const char * category, const char * action, const char * label) { + JNIEnv * env = AndroidJNI(); + + jclass j_class = env->FindClass("com/numworks/calculator/EpsilonActivity"); + jmethodID j_methodId = env->GetMethodID(j_class, "telemetryEvent", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V"); + assert(j_methodId != 0); + + env->CallVoidMethod(AndroidActivity(), j_methodId, JS(category, env), JS(action, env), JS(label, env)); + env->DeleteLocalRef(j_class); +} + +} +} diff --git a/ion/src/simulator/android/src/java/io/github/omega/simulator/OmegaActivity.java b/ion/src/simulator/android/src/java/io/github/omega/simulator/OmegaActivity.java index 83b94a5c1..259922365 100644 --- a/ion/src/simulator/android/src/java/io/github/omega/simulator/OmegaActivity.java +++ b/ion/src/simulator/android/src/java/io/github/omega/simulator/OmegaActivity.java @@ -25,13 +25,6 @@ public class OmegaActivity extends SDLActivity { }; } - @Override - protected String[] getArguments() { - Locale currentLocale = getResources().getConfiguration().locale; - String[] arguments = {"--language", currentLocale.getLanguage()}; - return arguments; - } - public Bitmap retrieveBitmapAsset(String identifier) { Bitmap bitmap = null; try { @@ -44,6 +37,10 @@ public class OmegaActivity extends SDLActivity { return bitmap; } + public String retrieveLanguage() { + return getResources().getConfiguration().locale.getLanguage(); + } + public boolean hapticFeedbackIsEnabled() { ContentResolver contentResolver = SDL.getContext().getContentResolver(); int val = Settings.System.getInt(contentResolver, Settings.System.HAPTIC_FEEDBACK_ENABLED, 0); diff --git a/ion/src/simulator/android/src/java/org/libsdl/app/HIDDeviceUSB.java b/ion/src/simulator/android/src/java/org/libsdl/app/HIDDeviceUSB.java index c9fc58ece..ad9b06ff4 100644 --- a/ion/src/simulator/android/src/java/org/libsdl/app/HIDDeviceUSB.java +++ b/ion/src/simulator/android/src/java/org/libsdl/app/HIDDeviceUSB.java @@ -51,7 +51,12 @@ class HIDDeviceUSB implements HIDDevice { public String getSerialNumber() { String result = null; if (Build.VERSION.SDK_INT >= 21) { - result = mDevice.getSerialNumber(); + try { + result = mDevice.getSerialNumber(); + } + catch (SecurityException exception) { + //Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage()); + } } if (result == null) { result = ""; diff --git a/ion/src/simulator/ios/Makefile b/ion/src/simulator/ios/Makefile index a6b114ad2..0477807f0 100644 --- a/ion/src/simulator/ios/Makefile +++ b/ion/src/simulator/ios/Makefile @@ -1,11 +1,10 @@ -ion_src += $(addprefix ion/src/simulator/ios/, \ - images.m \ -) - ion_src += $(addprefix ion/src/simulator/shared/, \ - apple/language.m \ - dummy/callback.cpp \ + apple/platform_images.mm \ + apple/platform_language.mm \ dummy/haptics_enabled.cpp \ + dummy/journal.cpp \ + dummy/keyboard_callback.cpp \ + dummy/window_callback.cpp \ clipboard_helper.cpp \ haptics.cpp \ ) @@ -31,6 +30,9 @@ LDFLAGS += -framework CoreData LDFLAGS += -Lion/src/simulator/ios/GoogleAnalyticsServices endif +LDFLAGS += -framework MobileCoreServices +LDFLAGS += -framework ImageIO + ifndef ARCH # App resources diff --git a/ion/src/simulator/ios/images.m b/ion/src/simulator/ios/images.m deleted file mode 100644 index 6a68fe2c4..000000000 --- a/ion/src/simulator/ios/images.m +++ /dev/null @@ -1,55 +0,0 @@ -#include "../shared/platform.h" - -#include -#include - -SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identifier) { - CGImageRef cgImage = [[UIImage imageNamed:[NSString stringWithUTF8String:identifier]] CGImage]; - if (cgImage == NULL) { - return NULL; - } - size_t width = CGImageGetWidth(cgImage); - size_t height = CGImageGetHeight(cgImage); - - - size_t bytesPerPixel = 4; - size_t bytesPerRow = bytesPerPixel * width; - size_t bitsPerComponent = 8; - - size_t size = height * width * bytesPerPixel; - void * bitmapData = malloc(size); - memset(bitmapData, 0, size); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate( - bitmapData, width, height, - bitsPerComponent, bytesPerRow, colorSpace, - kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big - ); - - CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); - - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - - SDL_Texture * texture = SDL_CreateTexture( - renderer, - SDL_PIXELFORMAT_ABGR8888, - SDL_TEXTUREACCESS_STATIC, - width, - height - ); - - SDL_UpdateTexture( - texture, - NULL, - bitmapData, - bytesPerPixel * width - ); - - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); - - free(bitmapData); - - return texture; -} diff --git a/ion/src/simulator/linux/Makefile b/ion/src/simulator/linux/Makefile index 1ff0ebcff..b05bba115 100644 --- a/ion/src/simulator/linux/Makefile +++ b/ion/src/simulator/linux/Makefile @@ -1,5 +1,6 @@ -# The following lines allow us to use our own SDL_config.h +ION_SIMULATOR_FILES = 1 +# The following lines allow us to use our own SDL_config.h # First, make sure an error is raised if we ever use the standard SDL_config.h SFLAGS += -DUSING_GENERATED_CONFIG_H # Then use our very own include dir if either SDL.h or SDL_config.h are included @@ -8,18 +9,26 @@ SFLAGS += -DUSING_GENERATED_CONFIG_H SFLAGS += -Iion/src/simulator/linux/include ion_src += $(addprefix ion/src/simulator/linux/, \ - images.cpp \ - language.cpp \ assets.s \ + platform_files.cpp \ + platform_images.cpp \ + platform_language.cpp \ ) +SFLAGS += $(shell pkg-config libpng libjpeg --cflags) +LDFLAGS += $(shell pkg-config libpng libjpeg --libs) + ion_src += $(addprefix ion/src/simulator/shared/, \ - dummy/callback.cpp \ dummy/haptics_enabled.cpp \ + dummy/keyboard_callback.cpp \ + dummy/window_callback.cpp \ + actions.cpp \ clipboard_helper.cpp \ collect_registers_x86_64.s \ collect_registers.cpp \ haptics.cpp \ + journal.cpp \ + state_file.cpp \ ) ifeq ($(EPSILON_TELEMETRY),1) @@ -27,17 +36,15 @@ ion_src += ion/src/simulator/shared/dummy/telemetry_init.cpp ion_src += ion/src/shared/telemetry_console.cpp endif -LDFLAGS += -ljpeg - $(eval $(call rule_for, \ INCBIN, \ - ion/src/simulator/linux/assets.s ion/src/simulator/linux/images.h, \ + ion/src/simulator/linux/assets.s ion/src/simulator/linux/platform_images.h, \ $(ion_simulator_assets_paths), \ $$(PYTHON) ion/src/simulator/linux/incbin.py $(ion_simulator_assets) -o $$@, \ global \ )) -$(call object_for,ion/src/simulator/linux/images.cpp): $(BUILD_DIR)/ion/src/simulator/linux/images.h +$(call object_for,ion/src/simulator/linux/platform_images.cpp): $(BUILD_DIR)/ion/src/simulator/linux/platform_images.h -# The header is refered to as so make sure it's findable this way -SFLAGS += -I$(BUILD_DIR) +# The header is refered to as so make sure it's findable this way +$(call object_for,ion/src/simulator/linux/platform_images.cpp): SFLAGS += -I$(BUILD_DIR) diff --git a/ion/src/simulator/linux/platform_files.cpp b/ion/src/simulator/linux/platform_files.cpp new file mode 100644 index 000000000..b866fe05c --- /dev/null +++ b/ion/src/simulator/linux/platform_files.cpp @@ -0,0 +1,21 @@ +#include "../shared/platform.h" + +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * filePathForReading(const char * extension) { + return nullptr; +} + +const char * filePathForWriting(const char * extension) { + static char path[64]; + if (snprintf(path, sizeof(path), "/tmp/epsilon.%s", extension) < 0) { + return nullptr; + } + return path; +} + +} +} +} diff --git a/ion/src/simulator/linux/images.cpp b/ion/src/simulator/linux/platform_images.cpp similarity index 75% rename from ion/src/simulator/linux/images.cpp rename to ion/src/simulator/linux/platform_images.cpp index dbe94b123..0fb23867b 100644 --- a/ion/src/simulator/linux/images.cpp +++ b/ion/src/simulator/linux/platform_images.cpp @@ -1,11 +1,16 @@ #include "../shared/platform.h" -#include +#include #include #include -#include +#include +#include -#include +#include + +namespace Ion { +namespace Simulator { +namespace Platform { enum class AssetFormat { JPG, @@ -95,7 +100,7 @@ bool readJPG(const unsigned char * start, size_t size, unsigned char ** bitmapDa return true; } -SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identifier) { +SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { static constexpr const char * jpgExtension = ".jpg"; static constexpr const char * pngExtension = ".png"; @@ -161,3 +166,55 @@ SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identi return texture; } + +class RGB888Pixel { +public: + RGB888Pixel() {} + RGB888Pixel(KDColor c) : + m_red(c.red()), + m_green(c.green()), + m_blue(c.blue()) { + } +private: + uint8_t m_red; + uint8_t m_green; + uint8_t m_blue; +}; +static_assert(sizeof(RGB888Pixel) == 3, "RGB888Pixel shall be 3 bytes long"); + +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + FILE * file = fopen(path, "wb"); // Write in binary mode + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + png_infop info = png_create_info_struct(png); + png_init_io(png, file); + + png_set_IHDR(png, info, + width, height, + 8, // Number of bits per channel + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png, info); + + RGB888Pixel * row = new RGB888Pixel[3*width]; + for (int j=0;j(row)); + } + delete row; + + png_write_end(png, NULL); + + png_free_data(png, info, PNG_FREE_ALL, -1); // -1 = all items + png_destroy_write_struct(&png, nullptr); + fclose(file); +} + +} +} +} diff --git a/ion/src/simulator/linux/language.cpp b/ion/src/simulator/linux/platform_language.cpp similarity index 72% rename from ion/src/simulator/linux/language.cpp rename to ion/src/simulator/linux/platform_language.cpp index dd146e143..81a370de3 100644 --- a/ion/src/simulator/linux/language.cpp +++ b/ion/src/simulator/linux/platform_language.cpp @@ -1,8 +1,11 @@ #include "../shared/platform.h" - #include -char * IonSimulatorGetLanguageCode() { +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * languageCode() { static char buffer[3] = {0}; char * locale = setlocale(LC_ALL, ""); if (locale[2] == '_') { @@ -12,3 +15,7 @@ char * IonSimulatorGetLanguageCode() { } return nullptr; } + +} +} +} diff --git a/ion/src/simulator/macos/Info.plist b/ion/src/simulator/macos/Info.plist index fc8d2aab2..88da389fb 100644 --- a/ion/src/simulator/macos/Info.plist +++ b/ion/src/simulator/macos/Info.plist @@ -20,6 +20,41 @@ MacOSX + CFBundleDocumentTypes + + + CFBundleTypeName + Nacho MNDL file + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + com.numworks.savefile + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + com.numworks.savefile + UTTypeConformsTo + + public.data + + UTTypeDescription + NumWorks save file + UTTypeTagSpecification + + public.filename-extension + + nws + + + + NSHumanReadableCopyright Copyright © 2019 NumWorks. All rights reserved. NSPrincipalClass diff --git a/ion/src/simulator/macos/Makefile b/ion/src/simulator/macos/Makefile index a465b83c2..a640a6571 100644 --- a/ion/src/simulator/macos/Makefile +++ b/ion/src/simulator/macos/Makefile @@ -1,15 +1,20 @@ +ION_SIMULATOR_FILES = 1 + ion_src += $(addprefix ion/src/simulator/macos/, \ - images.m \ + platform_files.mm \ ) ion_src += $(addprefix ion/src/simulator/shared/, \ - apple/language.m \ - dummy/callback.cpp \ + apple/platform_images.mm \ + apple/platform_language.mm \ dummy/haptics_enabled.cpp \ + dummy/keyboard_callback.cpp \ + dummy/window_callback.cpp \ clipboard_helper.cpp \ collect_registers_x86_64.s \ collect_registers.cpp \ haptics.cpp \ + journal.cpp \ ) ifeq ($(EPSILON_TELEMETRY),1) diff --git a/ion/src/simulator/macos/images.m b/ion/src/simulator/macos/images.m deleted file mode 100644 index 03c82ab51..000000000 --- a/ion/src/simulator/macos/images.m +++ /dev/null @@ -1,57 +0,0 @@ -#include "../shared/platform.h" - -#include -#include - -SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identifier) { - NSImage * nsImage = [NSImage imageNamed:[NSString stringWithUTF8String:identifier]]; - CGImageRef cgImage = [nsImage CGImageForProposedRect:NULL - context:NULL - hints:0]; - if (cgImage == NULL) { - return NULL; - } - size_t width = CGImageGetWidth(cgImage); - size_t height = CGImageGetHeight(cgImage); - - size_t bytesPerPixel = 4; - size_t bytesPerRow = bytesPerPixel * width; - size_t bitsPerComponent = 8; - - size_t size = height * width * bytesPerPixel; - void * bitmapData = malloc(size); - memset(bitmapData, 0, size); - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef context = CGBitmapContextCreate( - bitmapData, width, height, - bitsPerComponent, bytesPerRow, colorSpace, - kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big - ); - - CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); - - CGContextRelease(context); - CGColorSpaceRelease(colorSpace); - - SDL_Texture * texture = SDL_CreateTexture( - renderer, - SDL_PIXELFORMAT_ABGR8888, - SDL_TEXTUREACCESS_STATIC, - width, - height - ); - - SDL_UpdateTexture( - texture, - NULL, - bitmapData, - bytesPerPixel * width - ); - - SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); - - free(bitmapData); - - return texture; -} diff --git a/ion/src/simulator/macos/platform_files.mm b/ion/src/simulator/macos/platform_files.mm new file mode 100644 index 000000000..990cf3a5f --- /dev/null +++ b/ion/src/simulator/macos/platform_files.mm @@ -0,0 +1,32 @@ +#include "../shared/platform.h" +#include + +namespace Ion { +namespace Simulator { +namespace Platform { + +static const char * getPath(NSSavePanel * panel, const char * extension) { + [panel setAllowedFileTypes:[NSArray arrayWithObject:[NSString stringWithUTF8String:extension]]]; + static NSString * path = nil; + [path release]; path = nil; // Discard previous path if any + NSWindow * mainWindow = [[NSApplication sharedApplication] keyWindow]; + if ([panel runModal] == NSFileHandlingPanelOKButton) { + path = [[[panel URL] path] retain]; + } + [mainWindow makeKeyAndOrderFront:nil]; + return [path UTF8String]; +} + +const char * filePathForReading(const char * extension) { + NSOpenPanel * panel = [NSOpenPanel openPanel]; + return getPath(panel, extension); +} + +const char * filePathForWriting(const char * extension) { + NSSavePanel * panel = [NSSavePanel savePanel]; + return getPath(panel, extension); +} + +} +} +} diff --git a/ion/src/simulator/shared/actions.cpp b/ion/src/simulator/shared/actions.cpp new file mode 100644 index 000000000..ee8395f3f --- /dev/null +++ b/ion/src/simulator/shared/actions.cpp @@ -0,0 +1,43 @@ +#include "actions.h" +#include +#include "framebuffer.h" +#include "platform.h" +#include "state_file.h" + +namespace Ion { +namespace Simulator { +namespace Actions { + +constexpr const char * kStateFileExtension = "nws"; + +void saveState() { + const char * path = Platform::filePathForWriting(kStateFileExtension); + if (path != nullptr) { + StateFile::save(path); + } +} + +#if 0 +void loadState() { +// We would need to be able to perform a reset for this to be callable anytime + const char * path = Platform::filePathForReading(kStateFileExtension); + if (path != nullptr) { + StateFile::load(path); + } +} +#endif + +void takeScreenshot() { + const char * path = Platform::filePathForWriting("png"); + if (path != nullptr) { + Platform::saveImage( + Framebuffer::address(), + Display::Width, Display::Height, + path + ); + } +} + +} +} +} diff --git a/ion/src/simulator/shared/actions.h b/ion/src/simulator/shared/actions.h new file mode 100644 index 000000000..ad5b1f3e2 --- /dev/null +++ b/ion/src/simulator/shared/actions.h @@ -0,0 +1,17 @@ +#ifndef ION_SIMULATOR_ACTIONS_H +#define ION_SIMULATOR_ACTIONS_H + +#include + +namespace Ion { +namespace Simulator { +namespace Actions { + +void saveState(); +void takeScreenshot(); + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/apple/platform_images.mm b/ion/src/simulator/shared/apple/platform_images.mm new file mode 100644 index 000000000..7e4cbecad --- /dev/null +++ b/ion/src/simulator/shared/apple/platform_images.mm @@ -0,0 +1,128 @@ +#include "../platform.h" +#include +#include +#if TARGET_OS_OSX +#include +#else +#include +#include +#endif + +namespace Ion { +namespace Simulator { +namespace Platform { + +static CGContextRef createABGR8888Context(size_t width, size_t height) { + size_t bytesPerPixel = 4; + size_t bytesPerRow = bytesPerPixel * width; + size_t bitsPerComponent = 8; + + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGContextRef context = CGBitmapContextCreate( + nullptr, // The context will allocate and take ownership of the bitmap buffer + width, height, + bitsPerComponent, bytesPerRow, colorSpace, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big + ); + if (colorSpace) { + CFRelease(colorSpace); + } + return context; +} + +SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier) { + CGImageRef image = nullptr; +#if TARGET_OS_OSX + //http://lists.libsdl.org/pipermail/commits-libsdl.org/2016-December/001235.html + [[[NSApp windows] firstObject] setColorSpace:[NSColorSpace sRGBColorSpace]]; + + NSImage * nsImage = [NSImage imageNamed:[NSString stringWithUTF8String:identifier]]; + image = [nsImage CGImageForProposedRect:NULL context:NULL hints:0]; +#else + image = [[UIImage imageNamed:[NSString stringWithUTF8String:identifier]] CGImage]; +#endif + if (image == nullptr) { + return nullptr; + } + + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + + CGContextRef context = createABGR8888Context(width, height); + if (context == nullptr) { + return nullptr; + } + + void * argb8888Pixels = CGBitmapContextGetData(context); + + CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); + + SDL_Texture * texture = SDL_CreateTexture( + renderer, + SDL_PIXELFORMAT_ABGR8888, + SDL_TEXTUREACCESS_STATIC, + width, + height + ); + + size_t bytesPerPixel = 4; + + SDL_UpdateTexture( + texture, + NULL, + argb8888Pixels, + bytesPerPixel * width + ); + + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + + CGContextRelease(context); + + return texture; +} + +class ABGR8888Pixel { +public: + ABGR8888Pixel(KDColor c) : + m_red(c.red()), + m_green(c.green()), + m_blue(c.blue()), + m_alpha(255) { + } +private: + uint8_t m_red; + uint8_t m_green; + uint8_t m_blue; + uint8_t m_alpha; +}; +static_assert(sizeof(ABGR8888Pixel) == 4, "ARGB8888Pixel shall be 4 bytes long"); + +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + CGContextRef context = createABGR8888Context(width, height); + if (context == nullptr) { + return; + } + ABGR8888Pixel * argb8888Pixels = static_cast(CGBitmapContextGetData(context)); + for (int i=0; i([NSURL fileURLWithPath:[NSString stringWithUTF8String:path]]); + + CGImageDestinationRef destination = CGImageDestinationCreateWithURL(url, kUTTypePNG, 1, NULL); + CGImageDestinationAddImage(destination, image, nil); + CGImageDestinationFinalize(destination); + + if (destination) { + CFRelease(destination); + } + CGImageRelease(image); + CGContextRelease(context); +} + + +} +} +} diff --git a/ion/src/simulator/shared/apple/language.m b/ion/src/simulator/shared/apple/platform_language.mm similarity index 83% rename from ion/src/simulator/shared/apple/language.m rename to ion/src/simulator/shared/apple/platform_language.mm index 1fcefe08b..e765a8af9 100644 --- a/ion/src/simulator/shared/apple/language.m +++ b/ion/src/simulator/shared/apple/platform_language.mm @@ -2,7 +2,11 @@ #include -char * IonSimulatorGetLanguageCode() { +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * languageCode() { static char buffer[4] = {0}; if (buffer[0] == 0) { NSString * preferredLanguage = [[NSLocale preferredLanguages] firstObject]; @@ -15,3 +19,7 @@ char * IonSimulatorGetLanguageCode() { } return buffer; } + +} +} +} diff --git a/ion/src/simulator/shared/console.cpp b/ion/src/simulator/shared/console.cpp new file mode 100644 index 000000000..36b4a8f98 --- /dev/null +++ b/ion/src/simulator/shared/console.cpp @@ -0,0 +1,31 @@ +#include +#include "window.h" +#include +#include + +namespace Ion { +namespace Console { + +char readChar() { + if (Simulator::Window::isHeadless()) { + return getchar(); + } else { + return 0; + } +} + +void writeChar(char c) { + if (Simulator::Window::isHeadless()) { + putchar(c); + fflush(stdout); + } else { + KDIonContext::putchar(c); + } +} + +bool transmissionDone() { + return true; +} + +} +} diff --git a/ion/src/simulator/shared/console_stdio.cpp b/ion/src/simulator/shared/console_stdio.cpp deleted file mode 100644 index e77659f71..000000000 --- a/ion/src/simulator/shared/console_stdio.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include -#include - -namespace Ion { -namespace Console { - -char readChar() { - return getchar(); -} - -void writeChar(char c) { - putchar(c); - fflush(stdout); -} - -bool transmissionDone() { - // Always true because we flush after each writeChar - return true; -} - -} -} diff --git a/ion/src/simulator/shared/crc32.cpp b/ion/src/simulator/shared/crc32.cpp index 0fdfd3f6a..04718ea71 100644 --- a/ion/src/simulator/shared/crc32.cpp +++ b/ion/src/simulator/shared/crc32.cpp @@ -1,28 +1,32 @@ #include -uint32_t crc32Helper(const uint8_t * data, size_t length, bool wordAccess) { +namespace Ion { + +static uint32_t crc32Helper(const uint8_t * data, size_t length, bool wordAccess) { size_t uint32ByteLength = sizeof(uint32_t)/sizeof(uint8_t); uint32_t crc = 0xFFFFFFFF; size_t byteLength = (wordAccess ? length * uint32ByteLength : length); size_t wordLength = byteLength / uint32ByteLength; - for (int i = 0; i < (int)wordLength; i++) { + for (int i = 0; i < wordLength; i++) { // FIXME: Assumes little-endian byte order! for (int j = uint32ByteLength-1; j >= 0; j--) { // scan byte by byte to avoid alignment issue when building for emscripten platform - crc = Ion::crc32EatByte(crc, data[i*uint32ByteLength+j]); + crc = crc32EatByte(crc, data[i*uint32ByteLength+j]); } } - for (int i = (int) wordLength * uint32ByteLength; i < byteLength; i++) { - crc = Ion::crc32EatByte(crc, data[i]); + for (int i = wordLength * uint32ByteLength; i < byteLength; i++) { + crc = crc32EatByte(crc, data[i]); } return crc; } -uint32_t Ion::crc32Word(const uint32_t * data, size_t length) { - return crc32Helper((const uint8_t *) data, length, true); +uint32_t crc32Word(const uint32_t * data, size_t length) { + return crc32Helper(reinterpret_cast(data), length, true); } -uint32_t Ion::crc32Byte(const uint8_t * data, size_t length) { +uint32_t crc32Byte(const uint8_t * data, size_t length) { return crc32Helper(data, length, false); } + +} diff --git a/ion/src/simulator/shared/display.cpp b/ion/src/simulator/shared/display.cpp index 28994c8ab..798a68f0b 100644 --- a/ion/src/simulator/shared/display.cpp +++ b/ion/src/simulator/shared/display.cpp @@ -12,6 +12,7 @@ namespace Display { static SDL_Texture * sFramebufferTexture = nullptr; void init(SDL_Renderer * renderer) { + Framebuffer::setActive(true); Uint32 texturePixelFormat = SDL_PIXELFORMAT_RGB565; assert(sizeof(KDColor) == SDL_BYTESPERPIXEL(texturePixelFormat)); sFramebufferTexture = SDL_CreateTexture( @@ -23,7 +24,7 @@ void init(SDL_Renderer * renderer) { ); } -void quit() { +void shutdown() { SDL_DestroyTexture(sFramebufferTexture); sFramebufferTexture = nullptr; } diff --git a/ion/src/simulator/shared/display.h b/ion/src/simulator/shared/display.h index 5b1b98170..1f8adbd80 100644 --- a/ion/src/simulator/shared/display.h +++ b/ion/src/simulator/shared/display.h @@ -9,7 +9,7 @@ namespace Simulator { namespace Display { void init(SDL_Renderer * renderer); -void quit(); +void shutdown(); void draw(SDL_Renderer * renderer, SDL_Rect * rect); diff --git a/ion/src/simulator/shared/dummy/backlight.cpp b/ion/src/simulator/shared/dummy/backlight.cpp deleted file mode 100644 index 657122482..000000000 --- a/ion/src/simulator/shared/dummy/backlight.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include - -uint8_t Ion::Backlight::brightness() { - return 0; -} - -void Ion::Backlight::setBrightness(uint8_t b) {} - -void Ion::Backlight::init() {} - -bool Ion::Backlight::isInitialized() { return true; } - -void Ion::Backlight::shutdown() {} diff --git a/ion/src/simulator/shared/dummy/callback.cpp b/ion/src/simulator/shared/dummy/callback.cpp deleted file mode 100644 index d5851e650..000000000 --- a/ion/src/simulator/shared/dummy/callback.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "../platform.h" - -void IonSimulatorCallbackDidRefresh() { -} - -void IonSimulatorCallbackDidScanKeyboard() { -} diff --git a/ion/src/simulator/shared/dummy/display.cpp b/ion/src/simulator/shared/dummy/display.cpp deleted file mode 100644 index de76e0d91..000000000 --- a/ion/src/simulator/shared/dummy/display.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include - -void Ion::Display::POSTPushMulticolor(int rootNumberTiles, int tileSize) { -} - -int Ion::Display::displayUniformTilingSize10(KDColor c) { - return 0; -} - -int Ion::Display::displayColoredTilingSize10() { - return 0; -} - -bool Ion::Display::waitForVBlank() { - return true; -} diff --git a/ion/src/simulator/shared/dummy/journal.cpp b/ion/src/simulator/shared/dummy/journal.cpp new file mode 100644 index 000000000..4ff02f61a --- /dev/null +++ b/ion/src/simulator/shared/dummy/journal.cpp @@ -0,0 +1,20 @@ +#include "../journal.h" + +namespace Ion { +namespace Simulator { +namespace Journal { + +void init() { +} + +Ion::Events::Journal * replayJournal() { + return nullptr; +} + +Ion::Events::Journal * logJournal() { + return nullptr; +} + +} +} +} diff --git a/ion/src/simulator/shared/dummy/keyboard_callback.cpp b/ion/src/simulator/shared/dummy/keyboard_callback.cpp new file mode 100644 index 000000000..621cbd7ec --- /dev/null +++ b/ion/src/simulator/shared/dummy/keyboard_callback.cpp @@ -0,0 +1,12 @@ +#include "../keyboard.h" + +namespace Ion { +namespace Simulator { +namespace Keyboard { + +void didScan() { +} + +} +} +} diff --git a/ion/src/simulator/shared/dummy/language.cpp b/ion/src/simulator/shared/dummy/language.cpp index ac672ab47..18aedef3e 100644 --- a/ion/src/simulator/shared/dummy/language.cpp +++ b/ion/src/simulator/shared/dummy/language.cpp @@ -1,5 +1,13 @@ #include "../platform.h" -char * IonSimulatorGetLanguageCode() { +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * languageCode() { return nullptr; } + +} +} +} diff --git a/ion/src/simulator/shared/dummy/usb.cpp b/ion/src/simulator/shared/dummy/usb.cpp deleted file mode 100644 index b118ae964..000000000 --- a/ion/src/simulator/shared/dummy/usb.cpp +++ /dev/null @@ -1,21 +0,0 @@ -#include - -bool Ion::USB::isPlugged() { - return false; -} - -bool Ion::USB::isEnumerated() { - return false; -} - -void Ion::USB::clearEnumerationInterrupt() { -} - -void Ion::USB::DFU(bool) { -} - -void Ion::USB::enable() { -} - -void Ion::USB::disable() { -} diff --git a/ion/src/simulator/shared/dummy/window_callback.cpp b/ion/src/simulator/shared/dummy/window_callback.cpp new file mode 100644 index 000000000..908d2027d --- /dev/null +++ b/ion/src/simulator/shared/dummy/window_callback.cpp @@ -0,0 +1,12 @@ +#include "../window.h" + +namespace Ion { +namespace Simulator { +namespace Window { + +void didRefresh() { +} + +} +} +} diff --git a/ion/src/simulator/shared/events.h b/ion/src/simulator/shared/events.h index 1770c40ba..131df3807 100644 --- a/ion/src/simulator/shared/events.h +++ b/ion/src/simulator/shared/events.h @@ -5,15 +5,6 @@ #include namespace Ion { -namespace Simulator { -namespace Events { - -void dumpEventCount(int i); -void logAfter(int numberOfEvents); - -} -} - namespace Events { static constexpr size_t sharedExternalTextBufferSize = sizeof(SDL_TextInputEvent::text); diff --git a/ion/src/simulator/shared/events_keyboard.cpp b/ion/src/simulator/shared/events_platform.cpp similarity index 75% rename from ion/src/simulator/shared/events_keyboard.cpp rename to ion/src/simulator/shared/events_platform.cpp index 1087528f1..8eb5b894a 100644 --- a/ion/src/simulator/shared/events_keyboard.cpp +++ b/ion/src/simulator/shared/events_platform.cpp @@ -1,7 +1,10 @@ -#include "main.h" -#include "platform.h" -#include "layout.h" +#include "actions.h" #include "events.h" +#include "journal.h" +#include "keyboard.h" +#include "layout.h" +#include "state_file.h" +#include "window.h" #include #include @@ -9,71 +12,17 @@ #include #include -#if EPSILON_SDL_SCREEN_ONLY - -template -class Queue { -public: - Queue() : m_first(&m_elements[0]), m_last(&m_elements[0]) {} - int size() { - if (m_last >= m_first) { - return m_last - m_first; - } else { - return m_last - (m_first - N); - } - } - - void enqueue(T element) { - if (size() > N) { - // Queue is full - return; - } - *m_last = element; - m_last = next(m_last); - } - - T dequeue() { - if (size() <= 0) { - // Dequeueing an empty queue - return T(); - } - T e = *m_first; - m_first = next(m_first); - return e; - } - -private: - T * next(T * p) { - if (p >= m_elements + N) { - return m_elements; - } else { - return p + 1; - } - } - T * m_first; - T * m_last; - T m_elements[N]; -}; - -static Queue sEventQueue; - -void IonSimulatorEventsPushEvent(int eventNumber) { - sEventQueue.enqueue(Ion::Events::Event(eventNumber)); -} - -#endif - namespace Ion { namespace Events { -static Event eventFromSDLKeyboardEvent(SDL_KeyboardEvent event) { +static inline Event eventFromSDLKeyboardEvent(SDL_KeyboardEvent event) { /* If an event is detected, we want to remove the Shift modifier to mimic the * device behaviour. If no event was detected, we restore the previous * ShiftAlphaStatus. */ - Ion::Events::ShiftAlphaStatus previousShiftAlphaStatus = Ion::Events::shiftAlphaStatus(); - Ion::Events::removeShift(); + ShiftAlphaStatus previousShiftAlphaStatus = shiftAlphaStatus(); + removeShift(); - if (event.keysym.mod & KMOD_CTRL) { + if (event.keysym.mod & (KMOD_CTRL|KMOD_GUI)) { switch (event.keysym.sym) { case SDLK_x: return Cut; @@ -81,6 +30,14 @@ static Event eventFromSDLKeyboardEvent(SDL_KeyboardEvent event) { return Copy; case SDLK_v: return Paste; +#if ION_SIMULATOR_FILES + case SDLK_s: + Simulator::Actions::saveState(); + return None; + case SDLK_p: + Simulator::Actions::takeScreenshot(); + return None; +#endif } } if (event.keysym.mod & KMOD_ALT) { @@ -130,7 +87,7 @@ static Event eventFromSDLKeyboardEvent(SDL_KeyboardEvent event) { return Termination; } // No event was detected, restore the previous ShiftAlphaStatus. - Ion::Events::setShiftAlphaStatus(previousShiftAlphaStatus); + setShiftAlphaStatus(previousShiftAlphaStatus); return None; } @@ -159,7 +116,7 @@ static Event eventFromSDLTextInputEvent(SDL_TextInputEvent event) { * then we press "&", transformed by eventFromSDLTextInputEvent into the * text "1". If we do not remove the Shift here, it would still be * pressed afterwards. */ - Ion::Events::removeShift(); + Events::removeShift(); Event res = sEventForASCIICharAbove32[character-32]; if (res != None) { return res; @@ -172,18 +129,11 @@ static Event eventFromSDLTextInputEvent(SDL_TextInputEvent event) { } Event getPlatformEvent() { -#if EPSILON_SDL_SCREEN_ONLY - if (sEventQueue.size() > 0) { - Event event = sEventQueue.dequeue(); - return event; - } -#endif SDL_Event event; Event result = None; - while (SDL_PollEvent(&event)) { - // The while is important: it'll do a fast-pass over all useless SDL events + while (SDL_PollEvent(&event)) { // That "while" is important: it'll do a fast-pass over all useless SDL events if (event.type == SDL_WINDOWEVENT) { - Ion::Simulator::Main::relayout(); + Simulator::Window::relayout(); break; } if (event.type == SDL_QUIT) { @@ -191,7 +141,7 @@ Event getPlatformEvent() { break; } if (event.type == SDL_KEYDOWN) { - if (IonSimulatorSDLKeyDetectedByScan(event.key.keysym.scancode)) { + if (Simulator::Keyboard::scanHandlesSDLKey(event.key.keysym.scancode)) { continue; } result = eventFromSDLKeyboardEvent(event.key); @@ -201,6 +151,12 @@ Event getPlatformEvent() { result = eventFromSDLTextInputEvent(event.text); break; } +#if ION_SIMULATOR_FILES + if (event.type == SDL_DROPFILE) { + Simulator::StateFile::load(event.drop.file); + break; + } +#endif #if !EPSILON_SDL_SCREEN_ONLY if (event.type == SDL_MOUSEMOTION) { SDL_Point p; @@ -227,6 +183,13 @@ Event getPlatformEvent() { * the subsequent ones. */ SDL_FlushEvents(0, UINT32_MAX); } + if (Simulator::Window::isHeadless()) { + if (Simulator::Journal::replayJournal() == nullptr || Simulator::Journal::replayJournal()->isEmpty()) { + /* We don't want to keep the simulator process alive if there's no chance + * we're ever going to provide it with new events to process. */ + return Termination; + } + } return result; } diff --git a/ion/src/simulator/shared/events_stdin.cpp b/ion/src/simulator/shared/events_stdin.cpp deleted file mode 100644 index 053118b23..000000000 --- a/ion/src/simulator/shared/events_stdin.cpp +++ /dev/null @@ -1,62 +0,0 @@ -#include "main.h" -#include "platform.h" -#include "framebuffer.h" -#include "events.h" - -#include -#include - -#include -#include - -void IonSimulatorEventsPushEvent(int eventNumber) { -} - -static int sLogAfterNumberOfEvents = -1; -static int sEventCount = 0; - -namespace Ion { -namespace Events { - -Event getPlatformEvent() { - Ion::Events::Event event = Ion::Events::None; - while (!(event.isDefined() && event.isKeyboardEvent())) { - int c = getchar(); - if (c == EOF) { - printf("Finished processing %d events\n", sEventCount); - event = Ion::Events::Termination; - break; - } - event = Ion::Events::Event(c); - } -#if EPSILON_SIMULATOR_HAS_LIBPNG - if (sEventCount++ > sLogAfterNumberOfEvents && sLogAfterNumberOfEvents >= 0) { - char filename[32]; - sprintf(filename, "event%d.png", sEventCount); - Ion::Simulator::Framebuffer::writeToFile(filename); -#ifndef NDEBUG - printf("Event %d is %s\n", sEventCount, event.name()); -#endif - } -#endif - return event; -} - -} -} - -namespace Ion { -namespace Simulator { -namespace Events { - -void dumpEventCount(int i) { - printf("Current event index: %d\n", sEventCount); -} - -void logAfter(int numberOfEvents) { - sLogAfterNumberOfEvents = numberOfEvents; -} - -} -} -} diff --git a/ion/src/simulator/shared/framebuffer_base.cpp b/ion/src/simulator/shared/framebuffer.cpp similarity index 88% rename from ion/src/simulator/shared/framebuffer_base.cpp rename to ion/src/simulator/shared/framebuffer.cpp index 0eb597a5e..eb5a25fff 100644 --- a/ion/src/simulator/shared/framebuffer_base.cpp +++ b/ion/src/simulator/shared/framebuffer.cpp @@ -1,6 +1,6 @@ #include "framebuffer.h" +#include "window.h" #include -#include "main.h" /* Drawing on an SDL texture * In SDL2, drawing bitmap data happens through textures, whose data lives in @@ -14,23 +14,23 @@ * framebuffer to a PNG file. */ static KDColor sPixels[Ion::Display::Width * Ion::Display::Height]; -static bool sFrameBufferActive = true; +static bool sFrameBufferActive = false; namespace Ion { namespace Display { -static KDFrameBuffer sFrameBuffer = KDFrameBuffer(sPixels, KDSize(Ion::Display::Width, Ion::Display::Height)); +static KDFrameBuffer sFrameBuffer = KDFrameBuffer(sPixels, KDSize(Width, Height)); void pushRect(KDRect r, const KDColor * pixels) { if (sFrameBufferActive) { - Simulator::Main::setNeedsRefresh(); + Simulator::Window::setNeedsRefresh(); sFrameBuffer.pushRect(r, pixels); } } void pushRectUniform(KDRect r, KDColor c) { if (sFrameBufferActive) { - Simulator::Main::setNeedsRefresh(); + Simulator::Window::setNeedsRefresh(); sFrameBuffer.pushRectUniform(r, c); } } diff --git a/ion/src/simulator/shared/framebuffer.h b/ion/src/simulator/shared/framebuffer.h index e9eed19e2..9ae419e8e 100644 --- a/ion/src/simulator/shared/framebuffer.h +++ b/ion/src/simulator/shared/framebuffer.h @@ -9,7 +9,6 @@ namespace Framebuffer { const KDColor * address(); void setActive(bool enabled); -void writeToFile(const char * filename); } } diff --git a/ion/src/simulator/shared/framebuffer_png.cpp b/ion/src/simulator/shared/framebuffer_png.cpp deleted file mode 100644 index 9582e48e1..000000000 --- a/ion/src/simulator/shared/framebuffer_png.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#if EPSILON_SIMULATOR_HAS_LIBPNG - -#include "framebuffer.h" -#include -#include -#include - -typedef struct { - uint8_t red; - uint8_t green; - uint8_t blue; -} pixel_t; - -void Ion::Simulator::Framebuffer::writeToFile(const char * filename) { - FILE * file = fopen(filename, "wb"); // Write in binary mode - //ENSURE(file != NULL, "Opening file %s", filename); - - png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - //ENSURE(png != NULL, "Allocating PNG write structure"); - - png_infop info = png_create_info_struct(png); - //ENSURE(info != NULL, "Allocating info structure"); - - png_init_io(png, file); - - png_set_IHDR(png, info, - Ion::Display::Width, Ion::Display::Height, - 8, // Number of bits per channel - PNG_COLOR_TYPE_RGB, - PNG_INTERLACE_NONE, - PNG_COMPRESSION_TYPE_DEFAULT, - PNG_FILTER_TYPE_DEFAULT); - - png_write_info(png, info); - - static_assert(sizeof(pixel_t) == 3, "pixel_t shall be 3 bytes long (RGB888 format)"); - pixel_t * row = (pixel_t *)malloc(3*Ion::Display::Width); - const KDColor * pixels = address(); - for (int j=0;j + +namespace Ion { +namespace Simulator { +namespace Journal { + +void init() { + Events::logTo(logJournal()); +} + +Events::Journal * replayJournal() { + static QueueJournal journal; + return &journal; +} + +Events::Journal * logJournal() { + static QueueJournal journal; + return &journal; +} + +} +} +} diff --git a/ion/src/simulator/shared/journal.h b/ion/src/simulator/shared/journal.h new file mode 100644 index 000000000..8134a49e6 --- /dev/null +++ b/ion/src/simulator/shared/journal.h @@ -0,0 +1,19 @@ +#ifndef ION_SIMULATOR_JOURNAL_H +#define ION_SIMULATOR_JOURNAL_H + +#include + +namespace Ion { +namespace Simulator { +namespace Journal { + +void init(); + +Ion::Events::Journal * replayJournal(); +Ion::Events::Journal * logJournal(); + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/journal/queue_journal.h b/ion/src/simulator/shared/journal/queue_journal.h new file mode 100644 index 000000000..71d62cc79 --- /dev/null +++ b/ion/src/simulator/shared/journal/queue_journal.h @@ -0,0 +1,37 @@ +#ifndef ION_SIMULATOR_JOURNAL_QUEUE_JOURNAL_H +#define ION_SIMULATOR_JOURNAL_QUEUE_JOURNAL_H + +#include +#include + +namespace Ion { +namespace Simulator { +namespace Journal { + +class QueueJournal : public Ion::Events::Journal { +public: + void pushEvent(Ion::Events::Event e) override { + if (e != Ion::Events::None) { + m_eventStorage.push(e); + } + } + virtual Ion::Events::Event popEvent() override { + if (isEmpty()) { + return Ion::Events::None; + } + Ion::Events::Event e = m_eventStorage.front(); + m_eventStorage.pop(); + return e; + } + virtual bool isEmpty() override { + return m_eventStorage.empty(); + } +private: + std::queue m_eventStorage; +}; + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/keyboard.cpp b/ion/src/simulator/shared/keyboard.cpp new file mode 100644 index 000000000..9113c8b90 --- /dev/null +++ b/ion/src/simulator/shared/keyboard.cpp @@ -0,0 +1,104 @@ +#include "keyboard.h" +#include "layout.h" +#include "window.h" + +#include +#include + +using namespace Ion::Keyboard; + +static State sKeyboardState; + +class KeySDLKeyPair { +public: + constexpr KeySDLKeyPair(Key key, SDL_Scancode SDLKey) : + m_key(key), + m_SDLKey(SDLKey) + {} + Key key() const { return m_key; } + SDL_Scancode SDLKey() const { return m_SDLKey; } +private: + Key m_key; + SDL_Scancode m_SDLKey; +}; + +constexpr static KeySDLKeyPair sKeyPairs[] = { + KeySDLKeyPair(Key::Down, SDL_SCANCODE_DOWN), + KeySDLKeyPair(Key::Up, SDL_SCANCODE_UP), + KeySDLKeyPair(Key::Left, SDL_SCANCODE_LEFT), + KeySDLKeyPair(Key::Right, SDL_SCANCODE_RIGHT), + KeySDLKeyPair(Key::Shift, SDL_SCANCODE_LSHIFT), + KeySDLKeyPair(Key::Shift, SDL_SCANCODE_RSHIFT), + KeySDLKeyPair(Key::EXE, SDL_SCANCODE_RETURN), + KeySDLKeyPair(Key::Back, SDL_SCANCODE_ESCAPE), + KeySDLKeyPair(Key::Toolbox, SDL_SCANCODE_TAB), + KeySDLKeyPair(Key::Backspace, SDL_SCANCODE_BACKSPACE) +}; + +constexpr int sNumberOfKeyPairs = sizeof(sKeyPairs)/sizeof(KeySDLKeyPair); + +namespace Ion { +namespace Keyboard { + +State scan() { + State state = sKeyboardState; + + if (Simulator::Window::isHeadless()) { + return state; + } + // We need to tell SDL to get new state from the host OS + SDL_PumpEvents(); + + // Notify callbacks in case we need to do something + Simulator::Keyboard::didScan(); + + // Grab this opportunity to refresh the display if needed + Simulator::Window::refresh(); + +#if !EPSILON_SDL_SCREEN_ONLY + // Register a key for the mouse, if any + Key k = Simulator::Layout::getHighlightedKey(); + if (SDL_GetMouseState(nullptr, nullptr) && SDL_BUTTON(SDL_BUTTON_LEFT)) { + state.setKey(k); + } +#endif + + // Catch the physical keyboard events + const uint8_t * SDLstate = SDL_GetKeyboardState(NULL); + for (int i = 0; i < sNumberOfKeyPairs; i++) { + KeySDLKeyPair pair = sKeyPairs[i]; + if (SDLstate[pair.SDLKey()]) { + state.setKey(pair.key()); + } + } + + return state; +} + +} +} + +namespace Ion { +namespace Simulator { +namespace Keyboard { + +void keyDown(Ion::Keyboard::Key k) { + sKeyboardState.setKey(k); +} + +void keyUp(Ion::Keyboard::Key k) { + sKeyboardState.clearKey(k); +} + +bool scanHandlesSDLKey(SDL_Scancode key) { + for (int i = 0; i < sNumberOfKeyPairs; i++) { + if (key == sKeyPairs[i].SDLKey()) { + return true; + } + } + return false; +} + +} +} +} diff --git a/ion/src/simulator/shared/keyboard.h b/ion/src/simulator/shared/keyboard.h new file mode 100644 index 000000000..c4368282c --- /dev/null +++ b/ion/src/simulator/shared/keyboard.h @@ -0,0 +1,20 @@ +#ifndef ION_SIMULATOR_KEYBOARD_H +#define ION_SIMULATOR_KEYBOARD_H + +#include +#include + +namespace Ion { +namespace Simulator { +namespace Keyboard { + +void keyDown(Ion::Keyboard::Key k); +void keyUp(Ion::Keyboard::Key k); +bool scanHandlesSDLKey(SDL_Scancode key); +void didScan(); + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/keyboard_dummy.cpp b/ion/src/simulator/shared/keyboard_dummy.cpp deleted file mode 100644 index 2e4b56fe6..000000000 --- a/ion/src/simulator/shared/keyboard_dummy.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include -#include "platform.h" - -void IonSimulatorKeyboardKeyDown(int keyNumber) { -} - -void IonSimulatorKeyboardKeyUp(int keyNumber) { -} - -namespace Ion { -namespace Keyboard { - -State scan() { - return 0; -} - -} -} diff --git a/ion/src/simulator/shared/keyboard_sdl.cpp b/ion/src/simulator/shared/keyboard_sdl.cpp deleted file mode 100644 index 64e659df6..000000000 --- a/ion/src/simulator/shared/keyboard_sdl.cpp +++ /dev/null @@ -1,97 +0,0 @@ -#include "main.h" -#include "platform.h" -#include "layout.h" - -#include -#include - -#if EPSILON_SDL_SCREEN_ONLY -static Ion::Keyboard::State sKeyboardState; - -void IonSimulatorKeyboardKeyDown(int keyNumber) { - Ion::Keyboard::Key key = static_cast(keyNumber); - sKeyboardState.setKey(key); -} - -void IonSimulatorKeyboardKeyUp(int keyNumber) { - Ion::Keyboard::Key key = static_cast(keyNumber); - sKeyboardState.clearKey(key); -} -#endif - -namespace Ion { -namespace Keyboard { - -class KeySDLKeyPair { -public: - constexpr KeySDLKeyPair(Ion::Keyboard::Key key, SDL_Scancode SDLKey) : - m_key(key), - m_SDLKey(SDLKey) - {} - Ion::Keyboard::Key key() const { return m_key; } - SDL_Scancode SDLKey() const { return m_SDLKey; } -private: - Ion::Keyboard::Key m_key; - SDL_Scancode m_SDLKey; -}; - -constexpr static KeySDLKeyPair sKeyPairs[] = { - KeySDLKeyPair(Ion::Keyboard::Key::Down, SDL_SCANCODE_DOWN), - KeySDLKeyPair(Ion::Keyboard::Key::Up, SDL_SCANCODE_UP), - KeySDLKeyPair(Ion::Keyboard::Key::Left, SDL_SCANCODE_LEFT), - KeySDLKeyPair(Ion::Keyboard::Key::Right, SDL_SCANCODE_RIGHT), - KeySDLKeyPair(Ion::Keyboard::Key::Shift, SDL_SCANCODE_LSHIFT), - KeySDLKeyPair(Ion::Keyboard::Key::Shift, SDL_SCANCODE_RSHIFT), - KeySDLKeyPair(Ion::Keyboard::Key::EXE, SDL_SCANCODE_RETURN), - KeySDLKeyPair(Ion::Keyboard::Key::Back, SDL_SCANCODE_ESCAPE), - KeySDLKeyPair(Ion::Keyboard::Key::Toolbox, SDL_SCANCODE_TAB), - KeySDLKeyPair(Ion::Keyboard::Key::Backspace, SDL_SCANCODE_BACKSPACE) -}; - -constexpr int sNumberOfKeyPairs = sizeof(sKeyPairs)/sizeof(KeySDLKeyPair); - -State scan() { - // We need to tell SDL to get new state from the host OS - SDL_PumpEvents(); - - // Notify callbacks in case we need to do something - IonSimulatorCallbackDidScanKeyboard(); - - // Grab this opportunity to refresh the display if needed - Simulator::Main::refresh(); - - // Start with a "clean" state - State state; -#if EPSILON_SDL_SCREEN_ONLY - // In this case, keyboard states are sent over another channel. - state = sKeyboardState; -#else - // Register a key for the mouse, if any - Key k = Simulator::Layout::getHighlightedKey(); - if (SDL_GetMouseState(nullptr, nullptr) && SDL_BUTTON(SDL_BUTTON_LEFT)) { - state.setKey(k); - } -#endif - - // Catch the physical keyboard events - const uint8_t * SDLstate = SDL_GetKeyboardState(NULL); - for (int i = 0; i < sNumberOfKeyPairs; i++) { - KeySDLKeyPair pair = sKeyPairs[i]; - if (SDLstate[pair.SDLKey()]) { - state.setKey(pair.key()); - } - } - return state; -} - -} -} - -bool IonSimulatorSDLKeyDetectedByScan(SDL_Scancode key) { - for (int i = 0; i < Ion::Keyboard::sNumberOfKeyPairs; i++) { - if (key == Ion::Keyboard::sKeyPairs[i].SDLKey()) { - return true; - } - } - return false; -} diff --git a/ion/src/simulator/shared/layout.cpp b/ion/src/simulator/shared/layout.cpp index ef07451f8..f3dffd900 100644 --- a/ion/src/simulator/shared/layout.cpp +++ b/ion/src/simulator/shared/layout.cpp @@ -1,5 +1,5 @@ #include "layout.h" -#include "main.h" +#include "window.h" #include "platform.h" #include #include @@ -202,9 +202,9 @@ static SDL_Texture * sBackgroundTexture = nullptr; static SDL_Texture * sKeyLayoutTextures[KeyLayout::NumberOfShapes]; void init(SDL_Renderer * renderer) { - sBackgroundTexture = IonSimulatorLoadImage(renderer, "background.jpg"); + sBackgroundTexture = Platform::loadImage(renderer, "background.jpg"); for (size_t i = 0; i < KeyLayout::NumberOfShapes; i++) { - sKeyLayoutTextures[i] = IonSimulatorLoadImage(renderer, KeyLayout::assetName[i]); + sKeyLayoutTextures[i] = Platform::loadImage(renderer, KeyLayout::assetName[i]); } } @@ -239,13 +239,13 @@ void highlightKeyAt(SDL_Point * p) { } if (newHighlightedKeyIndex != sHighlightedKeyIndex) { sHighlightedKeyIndex = newHighlightedKeyIndex; - Main::setNeedsRefresh(); + Window::setNeedsRefresh(); } } void unhighlightKey() { sHighlightedKeyIndex = -1; - Main::setNeedsRefresh(); + Window::setNeedsRefresh(); } void drawHighlightedKey(SDL_Renderer * renderer) { diff --git a/ion/src/simulator/shared/layout.json b/ion/src/simulator/shared/layout.json new file mode 100644 index 000000000..1ec23b553 --- /dev/null +++ b/ion/src/simulator/shared/layout.json @@ -0,0 +1,61 @@ +{ + "background": [0, 0, 1160, 2220], + "screen": [192, 191, 776, 582], + "keys": { + "0": [133, 983, 140, 87], + "1": [231, 886, 87, 140], + "2": [231, 1026, 87, 140], + "3": [275, 983, 140, 87], + + "4": [745, 963, 129, 129], + "5": [898, 963, 129, 129], + + "6": [502, 908, 156, 101], + "7": [502, 1044, 156, 101], + + "12": [133, 1210, 129, 87], + "13": [286, 1210, 129, 87], + "14": [439, 1210, 129, 87], + "15": [592, 1210, 129, 87], + "16": [745, 1210, 129, 87], + "17": [898, 1210, 129, 87], + + "18": [133, 1332, 129, 87], + "19": [286, 1332, 129, 87], + "20": [439, 1332, 129, 87], + "21": [592, 1332, 129, 87], + "22": [745, 1332, 129, 87], + "23": [898, 1332, 129, 87], + + "24": [133, 1455, 129, 87], + "25": [286, 1455, 129, 87], + "26": [439, 1455, 129, 87], + "27": [592, 1455, 129, 87], + "28": [745, 1455, 129, 87], + "29": [898, 1455, 129, 87], + + "30": [133, 1578, 156, 101], + "31": [317, 1578, 156, 101], + "32": [502, 1578, 156, 101], + "33": [686, 1578, 156, 101], + "34": [871, 1578, 156, 101], + + "36": [133, 1715, 156, 101], + "37": [317, 1715, 156, 101], + "38": [502, 1715, 156, 101], + "39": [686, 1715, 156, 101], + "40": [871, 1715, 156, 101], + + "42": [133, 1851, 156, 101], + "43": [317, 1851, 156, 101], + "44": [502, 1851, 156, 101], + "45": [686, 1851, 156, 101], + "46": [871, 1851, 156, 101], + + "48": [133, 1988, 156, 101], + "49": [317, 1988, 156, 101], + "50": [502, 1988, 156, 101], + "51": [686, 1988, 156, 101], + "52": [871, 1988, 156, 101] + } +} diff --git a/ion/src/simulator/shared/main.cpp b/ion/src/simulator/shared/main.cpp new file mode 100644 index 000000000..a8deed32e --- /dev/null +++ b/ion/src/simulator/shared/main.cpp @@ -0,0 +1,126 @@ +#include "haptics.h" +#include "journal.h" +#include "platform.h" +#include "random.h" +#include "state_file.h" +#include "telemetry.h" +#include "window.h" +#include +#include +#ifndef __WIN32__ +#include +#include +#endif +#include "store_script.h" + +/* The Args class allows parsing and editing command-line arguments + * The editing part allows us to add/remove arguments before forwarding them to + * ion_main. */ + +bool Args::has(const char * name) const { + return find(name) != m_arguments.end(); +} + +const char * Args::pop(const char * argument) { + auto nameIt = find(argument); + if (nameIt != m_arguments.end()) { + auto valueIt = std::next(nameIt); + if (valueIt != m_arguments.end()) { + const char * value = *valueIt; + m_arguments.erase(valueIt); + m_arguments.erase(nameIt); + return value; + } + m_arguments.erase(nameIt); + } + return nullptr; +} + +bool Args::popFlag(const char * argument) { + auto flagIt = find(argument); + if (flagIt != m_arguments.end()) { + m_arguments.erase(flagIt); + return true; + } + return false; +} + +std::vector::const_iterator Args::find(const char * name) const { + return std::find_if( + m_arguments.begin(), m_arguments.end(), + [name](const char * s) { return strcmp(s, name) == 0; } + ); +} + +using namespace Ion::Simulator; + +int main(int argc, char * argv[]) { + Args args(argc, argv); + + if (!args.has("--language")) { + args.push("--language", Platform::languageCode()); + } + +#ifndef __WIN32__ + if (args.popFlag("--limit-stack-usage")) { + // Limit stack usage + /* TODO : Reduce stack memory cost in prime factorization to allow running + * tests with the actual stack size */ + constexpr int kStackSize = 32768*10; + struct rlimit stackLimits = {kStackSize, kStackSize}; + setrlimit(RLIMIT_STACK, &stackLimits); + } +#endif + + bool help = args.popFlag("--help") || args.popFlag("-h"); + bool headless = args.popFlag("--headless"); + bool volatile_storage = args.popFlag("--volatile") || args.popFlag("-v"); + +#if ION_SIMULATOR_FILES + const char * stateFile = args.pop("--load-state-file"); + if (stateFile) { + StateFile::load(stateFile); + } +#endif + + if (help) { + printf("Usage: %s [options]\n", argv[0]); + printf("Options:\n"); + printf(" -f, --fullscreen Starts the emulator in fullscreen\n"); + printf(" -s, --screen-only Disable the keyboard.\n"); + printf(" -v, --volatile Disable saving and loading python scripts from file.\n"); + printf(" -u, --unresizable Disable resizing the window.\n"); + printf(" -h, --help Show this help menu.\n"); + return 0; + } + + Random::init(); + if (!headless) { + bool screen_only = args.popFlag("--screen-only") || args.popFlag("-s"); + bool fullscreen = args.popFlag("--fullscreen") || args.popFlag("-f"); + bool unresizable = args.popFlag("--unresizable") || args.popFlag("-u"); + Journal::init(); +#if EPSILON_TELEMETRY + Telemetry::init(); +#endif + Window::init(screen_only, fullscreen, unresizable); + Haptics::init(); + } + if (!volatile_storage) { + Ion::Simulator::StoreScript::loadPython(&args); + } + ion_main(args.argc(), args.argv()); + if (!headless) { + Haptics::shutdown(); + Window::quit(); +#if EPSILON_TELEMETRY + Telemetry::shutdown(); +#endif + } + + if (!volatile_storage) { + Ion::Simulator::StoreScript::savePython(); + } + + return 0; +} diff --git a/ion/src/simulator/shared/main.h b/ion/src/simulator/shared/main.h index 544f008f4..46164e971 100644 --- a/ion/src/simulator/shared/main.h +++ b/ion/src/simulator/shared/main.h @@ -1,19 +1,26 @@ #ifndef ION_SIMULATOR_MAIN_H #define ION_SIMULATOR_MAIN_H -namespace Ion { -namespace Simulator { -namespace Main { +#include -void init(); -void quit(); - -void setNeedsRefresh(); -void refresh(); -void relayout(); - -} -} -} +class Args { +public: + Args(int argc, char * argv[]) : m_arguments(argv, argv+argc) {} + bool has(const char * key) const; + const char * pop(const char * key); + bool popFlag(const char * flag); + void push(const char * key, const char * value) { + if (key != nullptr && value != nullptr) { + m_arguments.push_back(key); + m_arguments.push_back(value); + } + } + void push(const char * argument) { m_arguments.push_back(argument); } + int argc() const { return m_arguments.size(); } + const char * const * argv() const { return &m_arguments[0]; } +private: + std::vector::const_iterator find(const char * name) const; + std::vector m_arguments; +}; #endif diff --git a/ion/src/simulator/shared/main_headless.cpp b/ion/src/simulator/shared/main_headless.cpp deleted file mode 100644 index 8b8fec39c..000000000 --- a/ion/src/simulator/shared/main_headless.cpp +++ /dev/null @@ -1,74 +0,0 @@ -#include "main.h" -#include "display.h" -#include "platform.h" -#include "framebuffer.h" -#include "events.h" - -#include -#include -#include -#include -#ifndef __WIN32__ -#include -#include -#endif - -constexpr int kHeapSize = 131072; -#ifdef NDEBUG -/* TODO : Reduce stack memory cost in prime factorization to allow running - * tests with the actual stack size */ -constexpr int kStackSize = 32768*10; -#else -constexpr int kStackSize = 32768*10; // In DEBUG mode, we increase the stack to be able to pass the tests -#endif - -char heap[kHeapSize]; -extern "C" { - char * _heap_start = (char *)heap; - char * _heap_end = _heap_start+kHeapSize; -} - -void Ion::Timing::msleep(uint32_t ms) { -} - -int main(int argc, char * argv[]) { - Ion::Simulator::Framebuffer::setActive(false); - // Parse command-line arguments - for (int i=1; i i+1) { - Ion::Simulator::Framebuffer::setActive(true); - Ion::Simulator::Events::logAfter(atoi(argv[i+1])); - } - } - -#ifndef __WIN32__ - // Handle signals - signal(SIGABRT, Ion::Simulator::Events::dumpEventCount); - - // Limit stack usage - struct rlimit stackLimits = {kStackSize, kStackSize}; - setrlimit(RLIMIT_STACK, &stackLimits); -#endif - - ion_main(argc, argv); - return 0; -} - -namespace Ion { -namespace Simulator { -namespace Main { - -void init() { -} - -void setNeedsRefresh() { -} - -void relayout() { -} - -void refresh() { -} -} -} -} diff --git a/ion/src/simulator/shared/main_sdl.cpp b/ion/src/simulator/shared/main_sdl.cpp deleted file mode 100644 index 5cea5207b..000000000 --- a/ion/src/simulator/shared/main_sdl.cpp +++ /dev/null @@ -1,347 +0,0 @@ -#include "main.h" -#include "display.h" -#include "haptics.h" -#include "layout.h" -#include "platform.h" -#include "telemetry.h" -#include "random.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static bool argument_screen_only = false; -static bool argument_fullscreen = false; -static bool argument_unresizable = false; -static bool argument_volatile = false; -static char* pref_path = nullptr; -static char* file_buffer = nullptr; - -static void loadPython(std::vector* arguments); -static void savePython(); - -void Ion::Timing::msleep(uint32_t ms) { - SDL_Delay(ms); -} - -void print_help(char * program_name) { - printf("Usage: %s [options]\n", program_name); - printf("Options:\n"); - printf(" -f, --fullscreen Starts the emulator in fullscreen\n"); - printf(" -s, --screen-only Disable the keyboard.\n"); - printf(" -v, --volatile Disable saving and loading python scripts from file.\n"); - printf(" -u, --unresizable Disable resizing the window.\n"); - printf(" -h, --help Show this help menu.\n"); -} - -int event_filter(void* userdata, SDL_Event* e) { - if (e->type == SDL_APP_TERMINATING || e->type == SDL_APP_WILLENTERBACKGROUND) { - savePython(); - } - - return 1; -} - - - -int main(int argc, char * argv[]) { - std::vector arguments(argv, argv + argc); - - for(int i = 1; i < argc; i++) { - if(strcmp(argv[i], "-h")==0 || strcmp(argv[i], "--help")==0) { - print_help(argv[0]); - return 0; - } else if(strcmp(argv[i], "-s")==0 || strcmp(argv[i], "--screen-only")==0) { - argument_screen_only = true; - } else if(strcmp(argv[i], "-f")==0 || strcmp(argv[i], "--fullscreen")==0) { - argument_fullscreen = true; - } else if(strcmp(argv[i], "-u")==0 || strcmp(argv[i], "--unresizable")==0) { - argument_unresizable = true; - } else if(strcmp(argv[i], "-v")==0 || strcmp(argv[i], "--volatile")==0) { - argument_volatile = true; - } - } - -#if EPSILON_SDL_SCREEN_ONLY - // Still allow the use of EPSILON_SDL_SCREEN_ONLY. - argument_screen_only = true; -#endif - - char * language = IonSimulatorGetLanguageCode(); - if (language != nullptr) { - arguments.push_back("--language"); - arguments.push_back(language); - } - -#ifndef __EMSCRIPTEN__ - if (!argument_volatile) { - loadPython(&arguments); - SDL_SetEventFilter(event_filter, NULL); - } -#endif - - Ion::Simulator::Main::init(); - Ion::Simulator::Haptics::init(); - - ion_main(arguments.size(), &arguments[0]); - -#ifndef __EMSCRIPTEN__ - if (!argument_volatile) { - savePython(); - } -#endif - - // Shutdown - Ion::Simulator::Haptics::shutdown(); - Ion::Simulator::Main::quit(); - - if (file_buffer != nullptr) - SDL_free(file_buffer); - if (pref_path != nullptr) - SDL_free(pref_path); - - return 0; -} - -static void loadPython(std::vector* arguments) { - pref_path = SDL_GetPrefPath("io.github.omega", "omega-simulator"); - std::string path(pref_path); - printf("Loading from %s\n", (path + "python.dat").c_str()); - - SDL_RWops* save_file = SDL_RWFromFile((path + "python.dat").c_str(), "rb"); - - if (save_file != NULL) { - // Calculate checksum - uint64_t checksum = 0; - uint64_t calc_checksum = 0; - - SDL_RWread(save_file, &checksum, sizeof(uint64_t), 1); - - uint8_t curr_check = 0; - - while(SDL_RWread(save_file, &curr_check, sizeof(uint8_t), 1)) { - calc_checksum += curr_check; - } - - if (checksum == calc_checksum) { - arguments->push_back("--code-wipe"); - arguments->push_back("true"); - - uint64_t length = SDL_RWseek(save_file, 0, RW_SEEK_END) - sizeof(uint64_t); - - SDL_RWseek(save_file, sizeof(uint64_t), RW_SEEK_SET); - - file_buffer = (char*) SDL_malloc(length); - SDL_RWread(save_file, file_buffer, length, 1); - - // printf("Length: %ld\n", length); - size_t i = 0; - while(i < length) { - uint16_t size = *(uint16_t*)(file_buffer + i); - arguments->push_back("--code-script"); - arguments->push_back((char*)(file_buffer + i + sizeof(uint16_t))); - // printf("Loaded size=%d i=%ld, %s\n", size, i+size, (char*)(file_buffer + i + sizeof(uint16_t))); - i += size + sizeof(uint16_t); - } - } - } -} - -static void savePython() { - std::string path(pref_path); - - printf("Saving to %s\n", (path + "python.dat").c_str()); - - SDL_RWops* save_file = SDL_RWFromFile((path + "python.dat").c_str(), "wb+"); - - if (save_file != NULL) { - - // Placeholder for checksum - uint64_t checksum = 0; - SDL_RWwrite(save_file, &checksum, sizeof(uint64_t), 1); - - uint16_t num = (uint16_t) Ion::Storage::sharedStorage()->numberOfRecordsWithExtension("py"); - - // Write all checksums - for(uint16_t i = 0; i < num; i++) { - Ion::Storage::Record record = Ion::Storage::sharedStorage()->recordWithExtensionAtIndex("py", i); - - const char* record_name = record.fullName(); - uint16_t record_name_len = strlen(record_name); - - Ion::Storage::Record::Data record_data = record.value(); - - uint16_t total_length = record_name_len + record_data.size; - - SDL_RWwrite(save_file, &total_length, sizeof(uint16_t), 1); - - SDL_RWwrite(save_file, record_name, record_name_len, 1); - SDL_RWwrite(save_file, ":", 1, 1); - // Remove import status, keep trailing \x00 - SDL_RWwrite(save_file, ((char*)record_data.buffer + 1), record_data.size - 1, 1); - } - - // Compute and write checksum - - SDL_RWseek(save_file, sizeof(uint64_t), RW_SEEK_SET); - uint8_t curr_check = 0; - - while(SDL_RWread(save_file, &curr_check, sizeof(uint8_t), 1)) { - checksum += curr_check; - } - - SDL_RWseek(save_file, 0, RW_SEEK_SET); - SDL_RWwrite(save_file, &checksum, sizeof(uint64_t), 1); - - SDL_RWclose(save_file); - } -} - -namespace Ion { -namespace Simulator { -namespace Main { - -static SDL_Window * sWindow = nullptr; -static SDL_Renderer * sRenderer = nullptr; -static bool sNeedsRefresh = false; -static SDL_Rect sScreenRect; - -void init() { - if (SDL_Init(SDL_INIT_VIDEO) != 0) { - SDL_Log("Could not init video"); - return; - } - - Random::init(); - - uint32_t sdl_window_args = SDL_WINDOW_ALLOW_HIGHDPI | (argument_unresizable ? 0 : SDL_WINDOW_RESIZABLE); - - if (argument_fullscreen) { - sdl_window_args = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_FULLSCREEN; - } - - if (argument_screen_only) { - sWindow = SDL_CreateWindow( - "Omega", - SDL_WINDOWPOS_CENTERED, - SDL_WINDOWPOS_CENTERED, - Ion::Display::Width, Ion::Display::Height, -#if EPSILON_SDL_SCREEN_ONLY - 0 -#else - sdl_window_args -#endif - ); - } else { - sWindow = SDL_CreateWindow( - "Omega", - SDL_WINDOWPOS_CENTERED, - SDL_WINDOWPOS_CENTERED, - 458, 888, - sdl_window_args - ); - } - - SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); - - // Try creating a hardware-accelerated renderer. - sRenderer = SDL_CreateRenderer(sWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); - if (!sRenderer) { - // Try creating a software renderer. - sRenderer = SDL_CreateRenderer(sWindow, -1, 0); - } - assert(sRenderer); - - Display::init(sRenderer); - -#if !EPSILON_SDL_SCREEN_ONLY - Layout::init(sRenderer); -#endif - - relayout(); -} - -void relayout() { - int windowWidth = 0; - int windowHeight = 0; - SDL_GetWindowSize(sWindow, &windowWidth, &windowHeight); - SDL_RenderSetLogicalSize(sRenderer, windowWidth, windowHeight); - - #if !EPSILON_SDL_SCREEN_ONLY - if (argument_screen_only) { - // Keep original aspect ration in screen_only mode. - float scale = (float)(Ion::Display::Width) / (float)(Ion::Display::Height); - if ((float)(windowHeight) * scale > float(windowWidth)) { - sScreenRect.w = windowWidth; - sScreenRect.h = (int)((float)(windowWidth) / scale); - } else { - sScreenRect.w = (int)((float)(windowHeight) * scale); - sScreenRect.h = windowHeight; - } - - sScreenRect.x = (windowWidth - sScreenRect.w) / 2; - sScreenRect.y = (windowHeight - sScreenRect.h) / 2; - } else { - Layout::recompute(windowWidth, windowHeight); - } - #else - sScreenRect.x = 0; - sScreenRect.y = 0; - sScreenRect.w = windowWidth; - sScreenRect.h = windowHeight; - #endif - - setNeedsRefresh(); -} - -void setNeedsRefresh() { - sNeedsRefresh = true; -} - -void refresh() { - if (!sNeedsRefresh) { - return; - } - sNeedsRefresh = false; - - #if EPSILON_SDL_SCREEN_ONLY - Display::draw(sRenderer, &sScreenRect); - #else - if (argument_screen_only) { - Display::draw(sRenderer, &sScreenRect); - } else { - SDL_Rect screenRect; - Layout::getScreenRect(&screenRect); - - SDL_SetRenderDrawColor(sRenderer, 194, 194, 194, 255); - SDL_RenderClear(sRenderer); - // Can change sNeedsRefresh state if a key is highlighted and needs to be reset - Layout::draw(sRenderer); - Display::draw(sRenderer, &screenRect); - } - #endif - - SDL_RenderPresent(sRenderer); - - IonSimulatorCallbackDidRefresh(); -} - -void quit() { -#if !EPSILON_SDL_SCREEN_ONLY - Layout::quit(); -#endif - Display::quit(); - SDL_DestroyWindow(sWindow); - SDL_Quit(); -} - -} -} -} diff --git a/ion/src/simulator/shared/platform.h b/ion/src/simulator/shared/platform.h index 494c83c23..3d950e255 100644 --- a/ion/src/simulator/shared/platform.h +++ b/ion/src/simulator/shared/platform.h @@ -2,30 +2,22 @@ #define ION_SIMULATOR_PLATFORM_H #include -#include +#include -#ifdef __cplusplus -extern "C" { +namespace Ion { +namespace Simulator { +namespace Platform { + +SDL_Texture * loadImage(SDL_Renderer * renderer, const char * identifier); +const char * languageCode(); +#if ION_SIMULATOR_FILES +const char * filePathForReading(const char * extension); +const char * filePathForWriting(const char * extension); +void saveImage(const KDColor * pixels, int width, int height, const char * path); #endif -/* Those functions should be implemented per-platform. - * They are defined as C function for easier interop. */ - -SDL_Texture * IonSimulatorLoadImage(SDL_Renderer * renderer, const char * identifier); -char * IonSimulatorGetLanguageCode(); - -#if EPSILON_SDL_SCREEN_ONLY -void IonSimulatorKeyboardKeyDown(int keyNumber); -void IonSimulatorKeyboardKeyUp(int keyNumber); -void IonSimulatorEventsPushEvent(int eventNumber); -#endif - -bool IonSimulatorSDLKeyDetectedByScan(SDL_Scancode key); -void IonSimulatorCallbackDidRefresh(); -void IonSimulatorCallbackDidScanKeyboard(); - -#ifdef __cplusplus } -#endif +} +} #endif diff --git a/ion/src/simulator/shared/power.cpp b/ion/src/simulator/shared/power.cpp deleted file mode 100644 index be918d1df..000000000 --- a/ion/src/simulator/shared/power.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include - -void Ion::Power::suspend(bool checkIfOnOffKeyReleased) { -} - -void Ion::Power::standby() { -} diff --git a/ion/src/simulator/shared/state_file.cpp b/ion/src/simulator/shared/state_file.cpp new file mode 100644 index 000000000..4b6e681ae --- /dev/null +++ b/ion/src/simulator/shared/state_file.cpp @@ -0,0 +1,100 @@ +#include +#include +#include +#include +#include "journal.h" + +namespace Ion { +namespace Simulator { +namespace StateFile { + +static constexpr const char * sHeader = "NWSF"; +static constexpr int sHeaderLength = 4; +static constexpr int sVersionLength = 8; +static constexpr const char * sWildcardVersion = "**.**.**"; + +/* File format: * "NWSF" + "XXXXXXXX" (version) + EVENTS... */ + +static inline bool load(FILE * f) { + char buffer[sVersionLength+1]; + + // Header + buffer[sHeaderLength] = 0; + if (fread(buffer, sHeaderLength, 1, f) != 1) { + return false; + } + if (strcmp(buffer, sHeader) != 0) { + return false; + } + + // Software version + buffer[sVersionLength] = 0; + if (fread(buffer, sVersionLength, 1, f) != 1) { + return false; + } + if (strcmp(buffer, softwareVersion()) != 0 && strcmp(buffer, sWildcardVersion) != 0) { + return false; + } + + // Events + Ion::Events::Journal * journal = Journal::replayJournal(); + int c = 0; + while ((c = getc(f)) != EOF) { + Ion::Events::Event e = Ion::Events::Event(c); + if (e.isDefined() && e.isKeyboardEvent()) { + // Avoid pushing invalid events - useful when fuzzing + journal->pushEvent(e); + } + } + Ion::Events::replayFrom(journal); + + return true; +} + +void load(const char * filename) { + FILE * f = nullptr; + if (strcmp(filename, "-") == 0) { + f = stdin; + } else { + f = fopen(filename, "rb"); + } + if (f == nullptr) { + return; + } + load(f); + if (f != stdin) { + fclose(f); + } +} + +static inline bool save(FILE * f) { + if (fwrite(sHeader, sHeaderLength, 1, f) != 1) { + return false; + } + if (fwrite(softwareVersion(), sVersionLength, 1, f) != 1) { + return false; + } + Ion::Events::Journal * journal = Journal::logJournal(); + Ion::Events::Event e; + while (!journal->isEmpty()) { + Ion::Events::Event e = journal->popEvent(); + uint8_t code = static_cast(e); + if (fwrite(&code, 1, 1, f) != 1) { + return false; + } + } + return true; +} + +void save(const char * filename) { + FILE * f = fopen(filename, "w"); + if (f == nullptr) { + return; + } + save(f); + fclose(f); +} + +} +} +} diff --git a/ion/src/simulator/shared/state_file.h b/ion/src/simulator/shared/state_file.h new file mode 100644 index 000000000..2d1686cd0 --- /dev/null +++ b/ion/src/simulator/shared/state_file.h @@ -0,0 +1,15 @@ +#ifndef ION_SIMULATOR_STATE_FILE_H +#define ION_SIMULATOR_STATE_FILE_H + +namespace Ion { +namespace Simulator { +namespace StateFile { + +void load(const char * filename); +void save(const char * filename); + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/store_script.cpp b/ion/src/simulator/shared/store_script.cpp new file mode 100644 index 000000000..8a5cea839 --- /dev/null +++ b/ion/src/simulator/shared/store_script.cpp @@ -0,0 +1,112 @@ +#include "store_script.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Ion { +namespace Simulator { +namespace StoreScript { + +static char* pref_path = nullptr; +static char* file_buffer = nullptr; + +void loadPython(Args * arguments) { + pref_path = SDL_GetPrefPath("io.github.omega", "omega-simulator"); + std::string path(pref_path); + printf("Loading from %s\n", (path + "python.dat").c_str()); + + SDL_RWops* save_file = SDL_RWFromFile((path + "python.dat").c_str(), "rb"); + + if (save_file != NULL) { + // Calculate checksum + uint64_t checksum = 0; + uint64_t calc_checksum = 0; + + SDL_RWread(save_file, &checksum, sizeof(uint64_t), 1); + + uint8_t curr_check = 0; + + while(SDL_RWread(save_file, &curr_check, sizeof(uint8_t), 1)) { + calc_checksum += curr_check; + } + + if (checksum == calc_checksum) { + arguments->push("--code-wipe", "true"); + + uint64_t length = SDL_RWseek(save_file, 0, RW_SEEK_END) - sizeof(uint64_t); + + SDL_RWseek(save_file, sizeof(uint64_t), RW_SEEK_SET); + + file_buffer = (char*) SDL_malloc(length); + SDL_RWread(save_file, file_buffer, length, 1); + + // printf("Length: %ld\n", length); + size_t i = 0; + while(i < length) { + uint16_t size = *(uint16_t*)(file_buffer + i); + arguments->push("--code-script", (char*)(file_buffer + i + sizeof(uint16_t))); + // printf("Loaded size=%d i=%ld, %s\n", size, i+size, (char*)(file_buffer + i + sizeof(uint16_t))); + i += size + sizeof(uint16_t); + } + } + } +} + +void savePython() { + std::string path(pref_path); + + printf("Saving to %s\n", (path + "python.dat").c_str()); + + SDL_RWops* save_file = SDL_RWFromFile((path + "python.dat").c_str(), "wb+"); + + if (save_file != NULL) { + + // Placeholder for checksum + uint64_t checksum = 0; + SDL_RWwrite(save_file, &checksum, sizeof(uint64_t), 1); + + uint16_t num = (uint16_t) Ion::Storage::sharedStorage()->numberOfRecordsWithExtension("py"); + + // Write all checksums + for(uint16_t i = 0; i < num; i++) { + Ion::Storage::Record record = Ion::Storage::sharedStorage()->recordWithExtensionAtIndex("py", i); + + const char* record_name = record.fullName(); + uint16_t record_name_len = strlen(record_name); + + Ion::Storage::Record::Data record_data = record.value(); + + uint16_t total_length = record_name_len + record_data.size; + + SDL_RWwrite(save_file, &total_length, sizeof(uint16_t), 1); + + SDL_RWwrite(save_file, record_name, record_name_len, 1); + SDL_RWwrite(save_file, ":", 1, 1); + // Remove import status, keep trailing \x00 + SDL_RWwrite(save_file, ((char*)record_data.buffer + 1), record_data.size - 1, 1); + } + + // Compute and write checksum + + SDL_RWseek(save_file, sizeof(uint64_t), RW_SEEK_SET); + uint8_t curr_check = 0; + + while(SDL_RWread(save_file, &curr_check, sizeof(uint8_t), 1)) { + checksum += curr_check; + } + + SDL_RWseek(save_file, 0, RW_SEEK_SET); + SDL_RWwrite(save_file, &checksum, sizeof(uint64_t), 1); + + SDL_RWclose(save_file); + } +} + +} +} +} diff --git a/ion/src/simulator/shared/store_script.h b/ion/src/simulator/shared/store_script.h new file mode 100644 index 000000000..6f6aa55e7 --- /dev/null +++ b/ion/src/simulator/shared/store_script.h @@ -0,0 +1,18 @@ +#ifndef ION_SIMULATOR_STORE_SCRIPT_H +#define ION_SIMULATOR_STORE_SCRIPT_H + +#include +#include "main.h" + +namespace Ion { +namespace Simulator { +namespace StoreScript { + +void loadPython(Args * arguments); +void savePython(); + +} +} +} + +#endif diff --git a/ion/src/simulator/shared/timing.cpp b/ion/src/simulator/shared/timing.cpp index a9bf44567..9997bbcf4 100644 --- a/ion/src/simulator/shared/timing.cpp +++ b/ion/src/simulator/shared/timing.cpp @@ -1,9 +1,24 @@ -#include +#include +#include "window.h" #include +#include static auto start = std::chrono::steady_clock::now(); -uint64_t Ion::Timing::millis() { +namespace Ion { +namespace Timing { + +uint64_t millis() { auto elapsed = std::chrono::steady_clock::now() - start; return std::chrono::duration_cast(elapsed).count(); } + +void msleep(uint32_t ms) { + if (Simulator::Window::isHeadless()) { + return; + } + SDL_Delay(ms); +} + +} +} diff --git a/ion/src/simulator/shared/window.cpp b/ion/src/simulator/shared/window.cpp new file mode 100644 index 000000000..e9b04bdaf --- /dev/null +++ b/ion/src/simulator/shared/window.cpp @@ -0,0 +1,161 @@ +#include "window.h" +#include "display.h" +#include "layout.h" +#include "platform.h" + +#include +#include +#include +#include + +namespace Ion { +namespace Simulator { +namespace Window { + +static SDL_Window * sWindow = nullptr; +static SDL_Renderer * sRenderer = nullptr; +static bool sNeedsRefresh = false; +static SDL_Rect sScreenRect; +static bool sScreenOnly = false; + +bool isHeadless() { + return sWindow == nullptr; +} + +void init(bool screen_only, bool fullscreen, bool unresizable) { + if (SDL_Init(SDL_INIT_VIDEO) != 0) { + SDL_Log("Could not init video"); + return; + } + + uint32_t sdl_window_args = SDL_WINDOW_ALLOW_HIGHDPI | (unresizable ? 0 : SDL_WINDOW_RESIZABLE); + if (fullscreen) { + sdl_window_args = SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_FULLSCREEN; + } + + if (screen_only) { + sScreenOnly = true; + sWindow = SDL_CreateWindow( + "Omega", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + Ion::Display::Width, Ion::Display::Height, +#if EPSILON_SDL_SCREEN_ONLY + 0 +#else + sdl_window_args +#endif + ); + } else { + sWindow = SDL_CreateWindow( + "Omega", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + 458, 888, + sdl_window_args + ); + } + + SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); + + // Try creating a hardware-accelerated renderer. + sRenderer = SDL_CreateRenderer(sWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); + if (!sRenderer) { + // Try creating a software renderer. + sRenderer = SDL_CreateRenderer(sWindow, -1, 0); + } + assert(sRenderer); + + Display::init(sRenderer); + +#if !EPSILON_SDL_SCREEN_ONLY + Layout::init(sRenderer); +#endif + + relayout(); +} + +void relayout() { + if (isHeadless()) { + return; + } + int windowWidth = 0; + int windowHeight = 0; + SDL_GetWindowSize(sWindow, &windowWidth, &windowHeight); + SDL_RenderSetLogicalSize(sRenderer, windowWidth, windowHeight); + + #if !EPSILON_SDL_SCREEN_ONLY + if (sScreenOnly) { + // Keep original aspect ration in screen_only mode. + float scale = (float)(Ion::Display::Width) / (float)(Ion::Display::Height); + if ((float)(windowHeight) * scale > float(windowWidth)) { + sScreenRect.w = windowWidth; + sScreenRect.h = (int)((float)(windowWidth) / scale); + } else { + sScreenRect.w = (int)((float)(windowHeight) * scale); + sScreenRect.h = windowHeight; + } + + sScreenRect.x = (windowWidth - sScreenRect.w) / 2; + sScreenRect.y = (windowHeight - sScreenRect.h) / 2; + } else { + Layout::recompute(windowWidth, windowHeight); + } + #else + sScreenRect.x = 0; + sScreenRect.y = 0; + sScreenRect.w = windowWidth; + sScreenRect.h = windowHeight; + #endif + + setNeedsRefresh(); +} + +void setNeedsRefresh() { + sNeedsRefresh = true; +} + +void refresh() { + if (!sNeedsRefresh || isHeadless()) { + return; + } + sNeedsRefresh = false; + + #if EPSILON_SDL_SCREEN_ONLY + Display::draw(sRenderer, &sScreenRect); + #else + if (sScreenOnly) { + Display::draw(sRenderer, &sScreenRect); + } else { + SDL_Rect screenRect; + Layout::getScreenRect(&screenRect); + + SDL_SetRenderDrawColor(sRenderer, 194, 194, 194, 255); + SDL_RenderClear(sRenderer); + // Can change sNeedsRefresh state if a key is highlighted and needs to be reset + Layout::draw(sRenderer); + Display::draw(sRenderer, &screenRect); + } + #endif + + SDL_RenderPresent(sRenderer); + + didRefresh(); +} + +void quit() { + if (isHeadless()) { + return; + } +#if !EPSILON_SDL_SCREEN_ONLY + Layout::quit(); +#endif + Display::shutdown(); + SDL_DestroyWindow(sWindow); + sWindow = nullptr; + SDL_Quit(); +} + +} +} +} diff --git a/ion/src/simulator/shared/window.h b/ion/src/simulator/shared/window.h new file mode 100644 index 000000000..1ab4031d5 --- /dev/null +++ b/ion/src/simulator/shared/window.h @@ -0,0 +1,23 @@ +#ifndef ION_SIMULATOR_WINDOW_H +#define ION_SIMULATOR_WINDOW_H + +namespace Ion { +namespace Simulator { +namespace Window { + +void init(bool screen_only, bool fullscreen, bool unresizable); +void quit(); + +bool isHeadless(); + +void setNeedsRefresh(); +void refresh(); +void relayout(); + +void didRefresh(); + +} +} +} + +#endif diff --git a/ion/src/simulator/web/Makefile b/ion/src/simulator/web/Makefile index 4032ef4df..3b60f0f8e 100644 --- a/ion/src/simulator/web/Makefile +++ b/ion/src/simulator/web/Makefile @@ -14,9 +14,11 @@ SFLAGS += -DEPSILON_SDL_SCREEN_ONLY=1 LDFLAGS += --pre-js ion/src/simulator/web/preamble_env.js ion_src += $(addprefix ion/src/simulator/web/, \ - callback.cpp \ clipboard_helper.cpp \ - helpers.cpp \ + exports.cpp \ + journal.cpp \ + keyboard_callback.cpp \ + window_callback.cpp \ ) ion_src += $(addprefix ion/src/simulator/shared/, \ @@ -34,6 +36,12 @@ endif DEFAULT = epsilon.zip +$(addprefix $(BUILD_DIR)/ion/src/simulator/web/,calculator.html calculator.css): ion/src/simulator/shared/layout.json ion/src/simulator/web/layout.py | $$(@D)/. + $(call rule_label,LAYOUT) + $(Q) $(PYTHON) ion/src/simulator/web/layout.py --html $(BUILD_DIR)/ion/src/simulator/web/calculator.html --css $(BUILD_DIR)/ion/src/simulator/web/calculator.css $< + +$(BUILD_DIR)/ion/src/simulator/web/simulator.html: $(addprefix $(BUILD_DIR)/ion/src/simulator/web/,calculator.html calculator.css) + $(BUILD_DIR)/epsilon%zip: $(BUILD_DIR)/epsilon%js $(BUILD_DIR)/ion/src/simulator/web/simulator.html @rm -rf $(basename $@) @mkdir -p $(basename $@) diff --git a/ion/src/simulator/web/calculator.js b/ion/src/simulator/web/calculator.js new file mode 100644 index 000000000..2234f1fa6 --- /dev/null +++ b/ion/src/simulator/web/calculator.js @@ -0,0 +1,93 @@ +function Calculator(emModule) { + /* emModule is an optional parameter. + * In addition to the ones supported by Emscripten, here are the values that + * this object can have: + * - element: a DOM element containing a copy of 'calculator.html' as + * generated by 'layout.py' + * - mirrorCanvas: a DOM element where the main canvas will be mirrored + */ + + // Configure emModule + var emModule = (typeof emModule === 'undefined') ? {} : emModule; + var calculatorElement = emModule.element || document.querySelector('.calculator'); + var mainCanvas = calculatorElement.querySelector("canvas"); + if (typeof emModule.mirrorCanvas === 'undefined') { + /* If emModule.mirrorCanvas is defined as null, don't do anything */ + emModule.mirrorCanvas = document.querySelector('.calculator-mirror canvas'); + } + var mirrorCanvasContext = emModule.mirrorCanvas ? emModule.mirrorCanvas.getContext('2d') : null; + var defaultModule = { + canvas: mainCanvas, + arguments: [ + '--language', + document.documentElement.lang || window.navigator.language.split('-')[0] + ], + onEpsilonIdle: function() { + calculatorElement.classList.remove('loading'); + }, + downloadScreenshot: function() { + // toDataURL needs the canvas to be refreshed + this._IonDisplayForceRefresh(); + var link = document.createElement('a'); + link.download = 'screenshot.png'; + link.href = mainCanvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'); + link.click(); + } + }; + if (mirrorCanvasContext) { + defaultModule.onDisplayRefresh = function() { + mirrorCanvasContext.drawImage(mainCanvas, 0, 0); + } + } + for (var attrname in defaultModule) { + if (!emModule.hasOwnProperty(attrname)) { + emModule[attrname] = defaultModule[attrname]; + } + } + + // Load and run Epsilon + Epsilon(emModule); + + /* Install event handlers + * This needs to be done after loading Epsilon, otherwise the _IonSimulator* + * functions haven't been defined just yet. */ + function eventHandler(keyHandler) { + return function(ev) { + var key = this.getAttribute('data-key'); + keyHandler(key); + /* Always prevent default action of event. + * First, this will prevent the browser from delaying that event. Indeed + * the browser would otherwise try to see if that event could have any + * other meaning (e.g. a click) and might delay it as a result. + * Second, this prevents touch events to be handled twice. Indeed, for + * backward compatibility reasons, mobile browsers usually create a fake + * mouse event after each real touch event. This allows desktop websites + * to work unmodified on mobile devices. But here we are explicitly + * handling both touch and mouse events. We therefore need to disable + * the default action of touch events, otherwise the handler would get + * called twice. */ + ev.preventDefault(); + }; + } + var downHandler = eventHandler(emModule._IonSimulatorKeyboardKeyDown); + var upHandler = eventHandler(emModule._IonSimulatorKeyboardKeyUp); + + calculatorElement.querySelectorAll('span').forEach(function(span){ + /* We decide to hook both to touch and mouse events + * On most mobile browsers, mouse events are generated if addition to touch + * events, so this could seem pointless. But those mouse events are not + * generated in real time: instead, they are buffered and eventually fired + * in a very rapid sequence. This prevents Epsilon from generating an event + * since this quick sequence will trigger the debouncer. */ + ['touchstart', 'mousedown'].forEach(function(type){ + span.addEventListener(type, downHandler); + }); + ['touchend', 'mouseup'].forEach(function(type){ + span.addEventListener(type, upHandler); + }); + }); +} + +if (typeof exports === 'object' && typeof module === 'object') { + module.exports = Calculator; +} diff --git a/ion/src/simulator/web/exports.cpp b/ion/src/simulator/web/exports.cpp new file mode 100644 index 000000000..e4f32ce2b --- /dev/null +++ b/ion/src/simulator/web/exports.cpp @@ -0,0 +1,37 @@ +#include "exports.h" +#include "../shared/journal.h" +#include "../shared/keyboard.h" +#include "../shared/window.h" +#include + +const char * IonSoftwareVersion() { + return Ion::softwareVersion(); +} + +const char * IonPatchLevel() { + return Ion::patchLevel(); +} + +void IonDisplayForceRefresh() { + Ion::Simulator::Window::setNeedsRefresh(); + Ion::Simulator::Window::refresh(); +} + +void IonSimulatorKeyboardKeyDown(int keyNumber) { + Ion::Keyboard::Key key = static_cast(keyNumber); + Ion::Simulator::Keyboard::keyDown(key); +} + +void IonSimulatorKeyboardKeyUp(int keyNumber) { + Ion::Keyboard::Key key = static_cast(keyNumber); + Ion::Simulator::Keyboard::keyUp(key); +} + +void IonSimulatorEventsPushEvent(int eventNumber) { + Ion::Events::Journal * j = Ion::Simulator::Journal::replayJournal(); + if (j != nullptr) { + Ion::Events::Event event(eventNumber); + j->pushEvent(event); + Ion::Events::replayFrom(j); + } +} diff --git a/ion/src/simulator/web/exports.h b/ion/src/simulator/web/exports.h new file mode 100644 index 000000000..ee0d47e08 --- /dev/null +++ b/ion/src/simulator/web/exports.h @@ -0,0 +1,19 @@ +#ifndef ION_SIMULATOR_WEB_EXPORTS_H +#define ION_SIMULATOR_WEB_EXPORTS_H + +#ifdef __cplusplus +extern "C" { +#endif + +const char * IonSoftwareVersion(); +const char * IonPatchLevel(); +void IonDisplayForceRefresh(); +void IonSimulatorKeyboardKeyDown(int keyNumber); +void IonSimulatorKeyboardKeyUp(int keyNumber); +void IonSimulatorEventsPushEvent(int eventNumber); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ion/src/simulator/web/helpers.cpp b/ion/src/simulator/web/helpers.cpp deleted file mode 100644 index b3ced2b17..000000000 --- a/ion/src/simulator/web/helpers.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include -#include "../shared/main.h" - -extern "C" const char * IonSoftwareVersion() { - return Ion::softwareVersion(); -} - -extern "C" const char * IonPatchLevel() { - return Ion::patchLevel(); -} - -extern "C" void IonDisplayForceRefresh() { - Ion::Simulator::Main::setNeedsRefresh(); - Ion::Simulator::Main::refresh(); -} diff --git a/ion/src/simulator/web/journal.cpp b/ion/src/simulator/web/journal.cpp new file mode 100644 index 000000000..694ca4cd4 --- /dev/null +++ b/ion/src/simulator/web/journal.cpp @@ -0,0 +1,58 @@ +#include "../shared/journal.h" +#include "../shared/journal/queue_journal.h" +#include + +namespace Ion { +namespace Simulator { +namespace Journal { + +using Ion::Events::Event; +using Ion::Events::None; + +class LogJournal : public Ion::Events::Journal { +public: + void pushEvent(Event e) override { + static bool lastEventWasNone = false; + if (e != None) { + EM_ASM({ + if (typeof Module.onIonEvent === "function") { + Module.onIonEvent($0); + } + }, static_cast(e)); + lastEventWasNone = false; + } else { + if (!lastEventWasNone) { + EM_ASM({ + if (typeof Module.onEpsilonIdle === "function") { + Module.onEpsilonIdle(); + } + }); + lastEventWasNone = true; + } + } + } + Event popEvent() override { + return None; + } + bool isEmpty() override { + return true; + } +}; + +void init() { + Events::logTo(logJournal()); +} + +Events::Journal * replayJournal() { + static QueueJournal journal; + return &journal; +} + +Ion::Events::Journal * logJournal() { + static LogJournal journal; + return &journal; +} + +} +} +} diff --git a/ion/src/simulator/web/callback.cpp b/ion/src/simulator/web/keyboard_callback.cpp similarity index 64% rename from ion/src/simulator/web/callback.cpp rename to ion/src/simulator/web/keyboard_callback.cpp index 6c5254a8d..7efdc746f 100644 --- a/ion/src/simulator/web/callback.cpp +++ b/ion/src/simulator/web/keyboard_callback.cpp @@ -1,13 +1,11 @@ -#include "../shared/platform.h" +#include "../shared/keyboard.h" #include -void IonSimulatorCallbackDidRefresh() { - /* Notify JS that the display has been refreshed. - * This gives us a chance to mirror the display in fullscreen mode. */ - EM_ASM(if (typeof Module.onDisplayRefresh === "function") { Module.onDisplayRefresh(); }); -} +namespace Ion { +namespace Simulator { +namespace Keyboard { -void IonSimulatorCallbackDidScanKeyboard() { +void didScan() { /* The following call to emscripten_sleep gives the JS VM a chance to do a run * loop iteration. This in turns gives the browser an opportunity to call the * IonEventsEmscriptenPushKey function, therefore modifying the sKeyboardState @@ -17,3 +15,7 @@ void IonSimulatorCallbackDidScanKeyboard() { * puts the callback at the end of the queue of callbacks to be processed. */ emscripten_sleep(0); } + +} +} +} diff --git a/ion/src/simulator/web/layout.py b/ion/src/simulator/web/layout.py new file mode 100644 index 000000000..cac9fffd6 --- /dev/null +++ b/ion/src/simulator/web/layout.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 + +import argparse +import json + +def css_rule(selector, *declarations): + css = '' + css += selector + ' {\n' + for decl in declarations: + css += decl + css += '}\n\n' + return css + +def css_declaration(prop, value): + return " %s: %s;\n" % (prop, value) + +def css_percentage_declaration(prop, ratio): + return css_declaration(prop, "%.2f%%" % (100*ratio)) + +def css_rect_declarations(rect, ref): + css = '' + css += css_percentage_declaration("left", rect[0]/ref[2]) + css += css_percentage_declaration("top", rect[1]/ref[3]) + css += css_percentage_declaration("width", rect[2]/ref[2]) + css += css_percentage_declaration("height", rect[3]/ref[3]) + return css + +def css(layout): + background = layout["background"] + css = '' + css += css_rule( + '.calculator', + css_declaration('position', 'relative'), + ) + css += css_rule( + '.calculator-aspect-ratio', + css_percentage_declaration('padding-bottom', background[3]/background[2]) + ) + css += css_rule( + '.calculator canvas, .calculator .loader', + css_declaration('position', 'absolute'), + css_rect_declarations(layout['screen'], background) + ) + css += css_rule( + '.calculator span', + css_declaration('position', 'absolute'), + css_declaration('display', 'block'), + css_declaration('border-radius', '999px'), + css_declaration('cursor', 'pointer'), + ) + css += css_rule( + '.calculator span:hover', + css_declaration('background-color', 'rgba(0, 0, 0, 0.1)') + ) + css += css_rule( + '.calculator span:active', + css_declaration('background-color', 'rgba(0, 0, 0, 0.2)') + ) + for key,rect in layout["keys"].items(): + css += css_rule( + '.calculator span[data-key="%s"]' % key, + css_rect_declarations(rect, background) + ) + css += css_rule( + '.calculator-mirror canvas', + css_declaration('image-rendering', '-moz-crisp-edges'), + css_declaration('image-rendering', 'pixelated'), + css_declaration('-ms-interpolation-mode', 'nearest-neighbor') + ) + css += css_rule( + '.calculator.loading canvas', + css_declaration('display', 'none') + ) + css += css_rule( + '.calculator .loader', + css_declaration('display', 'none') + ) + css += css_rule( + '.calculator.loading .loader', + css_declaration('display', 'block'), + css_declaration('background-color', '#000'), + css_declaration('position', 'absolute') + ) + css += css_rule( + '.calculator .loader span', + css_declaration('display', 'inline-block'), + css_declaration('width', '80px'), + css_declaration('height', '80px'), + css_declaration('top', '50%'), + css_declaration('margin-top', '-40px'), + css_declaration('left', '50%'), + css_declaration('margin-left', '-40px') + ) + css += '@keyframes calculator-loader-rotation { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }\n' + css += css_rule('.calculator .loader span:after', + css_declaration('content', '" "'), + css_declaration('display', 'block'), + css_declaration('width', '64px'), + css_declaration('height', '64px'), + css_declaration('margin', '8px'), + css_declaration('border-radius', '50%'), + css_declaration('border', '6px solid #fff'), + css_declaration('border-color', '#fff transparent #fff transparent'), + css_declaration('animation', 'calculator-loader-rotation 1.2s linear infinite') + ) + return css + +def html(layout): + screen = layout["screen"] + background = layout["background"] + html = '' + html += '
\n' + html += '
\n' + html += ' \n' + for key in layout["keys"]: + html += ' \n' % key + html += '
' + return html + +# Argument parsing + +parser = argparse.ArgumentParser(description='Generate HTML and CSS files from JSON layout') +parser.add_argument('file', type=str, help='a JSON layout file') +parser.add_argument('--css', type=str, help='an output HTML file') +parser.add_argument('--html', type=str, help='an output CSS file') +args = parser.parse_args() + +with open(args.file) as f: + layout = json.load(f) + if args.css: + with open(args.css, 'w') as o: + o.write(css(layout)) + if args.html: + with open(args.html, 'w') as o: + o.write(html(layout)) diff --git a/ion/src/simulator/web/preamble_env.js b/ion/src/simulator/web/preamble_env.js index 42fc03f95..3400727ff 100644 --- a/ion/src/simulator/web/preamble_env.js +++ b/ion/src/simulator/web/preamble_env.js @@ -18,4 +18,13 @@ Module["preRun"] = function () { ENV[key] = Module['env'][key]; } } + /* When figuring up in which DOM element to draw, SDL2/emscripten looks for + * an element with an id of "canvas". That's pretty annoying because it pretty + * much prevents us from running multiple instances of Epsilon in a single web + * page even if Epsilon is built using MODULARIZE=1. + * Enters specialHTMLTargets. This is a nifty hack that allows one to override + * DOM node lookups made by Emscripten! Using this feature, we can instruct + * emscripten to use a custom element when looking for "#canvas". + * See https://github.com/emscripten-core/emscripten/pull/10659 */ + specialHTMLTargets["#canvas"] = Module.canvas; } diff --git a/ion/src/simulator/web/simulator-keyboard.html b/ion/src/simulator/web/simulator-keyboard.html deleted file mode 100644 index 86425fc37..000000000 --- a/ion/src/simulator/web/simulator-keyboard.html +++ /dev/null @@ -1,54 +0,0 @@ -
- -
- - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - - - - - - - - -
-
diff --git a/ion/src/simulator/web/simulator-setup.js b/ion/src/simulator/web/simulator-setup.js deleted file mode 100644 index d4eb18b97..000000000 --- a/ion/src/simulator/web/simulator-setup.js +++ /dev/null @@ -1,65 +0,0 @@ -/* To use this file you should adhere to the following conventions: - * - Main canvas has id #canvas - * - Secondary canvas (fullscreen on the side) has id #secondary-canvas - * - Calculator keyboard has id #keyboard */ - -var Module; - -(function () { - var mainCanvas = document.getElementById('canvas'); - var secondaryCanvasContext = document.getElementById('secondary-canvas').getContext('2d'); - var epsilonLanguage = document.documentElement.lang || window.navigator.language.split('-')[0]; - Module = { - canvas: mainCanvas, - arguments: ['--language', epsilonLanguage], - onDisplayRefresh: function() { - secondaryCanvasContext.drawImage(mainCanvas, 0, 0); - } - } - - Epsilon(Module); - - document.querySelectorAll('#keyboard span').forEach(function(span){ - function eventHandler(keyHandler) { - return function(ev) { - var key = this.getAttribute('data-key'); - keyHandler(key); - /* Always prevent default action of event. - * First, this will prevent the browser from delaying that event. Indeed - * the browser would otherwise try to see if that event could have any - * other meaning (e.g. a click) and might delay it as a result. - * Second, this prevents touch events to be handled twice. Indeed, for - * backward compatibility reasons, mobile browsers usually create a fake - * mouse event after each real touch event. This allows desktop websites - * to work unmodified on mobile devices. But here we are explicitly - * handling both touch and mouse events. We therefore need to disable - * the default action of touch events, otherwise the handler would get - * called twice. */ - ev.preventDefault(); - }; - } - /* We decide to hook both to touch and mouse events - * On most mobile browsers, mouse events are generated if addition to touch - * events, so this could seem pointless. But those mouse events are not - * generated in real time: instead, they are buffered and eventually fired - * in a very rapid sequence. This prevents Epsilon from generating an event - * since this quick sequence will trigger the debouncer. */ - ['touchstart', 'mousedown'].forEach(function(type){ - span.addEventListener(type, eventHandler(Module._IonSimulatorKeyboardKeyDown)); - }); - ['touchend', 'mouseup'].forEach(function(type){ - span.addEventListener(type, eventHandler(Module._IonSimulatorKeyboardKeyUp)); - }); - }); -}()); - -function screenshot() { - // toDataURL needs the canvas to be refreshed - Module._IonDisplayForceRefresh(); - - var canvas = document.getElementById('canvas'); - var link = document.createElement('a'); - link.download = 'screenshot.png'; - link.href = canvas.toDataURL('image/png').replace('image/png', 'image/octet-stream'); - link.click(); -} diff --git a/ion/src/simulator/web/simulator.css b/ion/src/simulator/web/simulator.css new file mode 100644 index 000000000..996203724 --- /dev/null +++ b/ion/src/simulator/web/simulator.css @@ -0,0 +1,80 @@ +body { + background: #C2C2C2; + margin: 0; + padding: 0; + text-align: center; +} + +.col-fullscreen { + display: none; + width: 60%; + float: left; +} + +.col-fullscreen canvas { + margin-top: 7%; + width: 100%; +} + +body.fullscreen .col-fullscreen { + display: block; +} + +body.fullscreen .col-calculator { + width: 35%; + float: left; +} + +a.action { + display: block; + width: 4%; + padding: 1.5% 3%; + border: 1px solid black; + border-radius: 100px; + cursor: pointer; + opacity: 0.65; +} + +a.action:hover { + opacity: 1.0; + background-color: rgba(0,0,0,0.125); +} + +a.action svg { + display: block; +} + +.calculator-container { + margin: 0 auto; + position: relative; + display: inline-block; + text-align: left; +} +.calculator-container > img { + /* Rely on the img content to set the dimensions */ + max-height: 92.0vh; + max-width: 100%; + display: block; +} + +.calculator { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.calculator-container .actions { + position: absolute; + top: 94.0vh; + text-align: center; + left: 0; + right: 0; + margin-left: 20px; /* Center the buttons - compensate the 10px margin below */ +} + +.calculator-container .actions a { + display: inline-block; + margin-right: 10px; +} diff --git a/ion/src/simulator/web/simulator.html.inc b/ion/src/simulator/web/simulator.html.inc index ddb6452fb..dbfd95c89 100644 --- a/ion/src/simulator/web/simulator.html.inc +++ b/ion/src/simulator/web/simulator.html.inc @@ -1,5 +1,3 @@ -#define KEYBOARD #keyboard - @@ -7,178 +5,17 @@ Omega - +
-
+
NumWorks Calculator - -#include "simulator-keyboard.html" +#include "calculator.html"
-
- +
+
diff --git a/ion/src/simulator/web/window_callback.cpp b/ion/src/simulator/web/window_callback.cpp new file mode 100644 index 000000000..0688b5e78 --- /dev/null +++ b/ion/src/simulator/web/window_callback.cpp @@ -0,0 +1,16 @@ +#include "../shared/window.h" +#include + +namespace Ion { +namespace Simulator { +namespace Window { + +void didRefresh() { + /* Notify JS that the display has been refreshed. + * This gives us a chance to mirror the display in fullscreen mode. */ + EM_ASM(if (typeof Module.onDisplayRefresh === "function") { Module.onDisplayRefresh(); }); +} + +} +} +} diff --git a/ion/src/simulator/windows/Makefile b/ion/src/simulator/windows/Makefile index 70f6dc4ca..b44499e02 100644 --- a/ion/src/simulator/windows/Makefile +++ b/ion/src/simulator/windows/Makefile @@ -1,14 +1,22 @@ +ION_SIMULATOR_FILES = 1 + ion_src += $(addprefix ion/src/simulator/windows/, \ - images.cpp \ - language.cpp \ + platform_files.cpp \ + platform_images.cpp \ + platform_language.cpp \ resources.rc \ ) ion_src += $(addprefix ion/src/simulator/shared/, \ - dummy/callback.cpp \ dummy/haptics_enabled.cpp \ + dummy/keyboard_callback.cpp \ + dummy/window_callback.cpp \ + actions.cpp \ clipboard_helper.cpp \ haptics.cpp \ + journal.cpp \ + platform_action_modifier_ctrl.cpp \ + state_file.cpp \ ) ion_src += ion/src/shared/collect_registers.cpp @@ -20,11 +28,14 @@ endif # RC file dependencies $(call object_for,ion/src/simulator/windows/resources.rc): WRFLAGS += -I $(BUILD_DIR) +$(call object_for,ion/src/simulator/windows/resources.rc): $(addprefix $(BUILD_DIR)/,logo.ico) -# The header of images is refered to as so make sure it's findable this way -SFLAGS += -I$(BUILD_DIR) +$(addprefix $(BUILD_DIR)/,logo.ico): ion/src/simulator/assets/logo.svg | $$(@D)/. + $(call rule_label,CONVERT) + $(Q) convert -background "#FFB734" -resize 256x256 $< $@ -LDFLAGS += -lgdiplus +# Linker flags +LDFLAGS += -lgdiplus -lcomdlg32 $(eval $(call rule_for, \ RESGEN, \ @@ -34,6 +45,5 @@ $(eval $(call rule_for, \ global \ )) -$(call object_for,ion/src/simulator/windows/images.cpp): $(BUILD_DIR)/ion/src/simulator/windows/images.h +$(call object_for,ion/src/simulator/windows/platform_images.cpp): $(BUILD_DIR)/ion/src/simulator/windows/images.h $(call object_for,ion/src/simulator/windows/resources.rc): $(BUILD_DIR)/ion/src/simulator/windows/resources_gen.rc - diff --git a/ion/src/simulator/windows/platform_files.cpp b/ion/src/simulator/windows/platform_files.cpp new file mode 100644 index 000000000..865f47179 --- /dev/null +++ b/ion/src/simulator/windows/platform_files.cpp @@ -0,0 +1,51 @@ +#include "../shared/platform.h" +#include +#include + +namespace Ion { +namespace Simulator { +namespace Platform { + +static OPENFILENAME * getOFN(const char * extension) { + static OPENFILENAME ofn; + memset(&ofn, 0, sizeof(ofn)); // Zero-out ofn + + static char path[PATH_MAX]; + path[0] = 0; // Reset the path + + static char filter[32]; + if (snprintf(filter, sizeof(filter), "%s%c*.%s%c%c", extension, 0, extension, 0, 0) < 0) { + /* Note: We cannot use litteral \0 in the format string, otherwise snprintf + * will think the format string is finished... */ + return nullptr; + } + + ofn.lStructSize = sizeof(ofn); + ofn.lpstrFile = path; + ofn.nMaxFile = sizeof(path); + ofn.lpstrFilter = filter; + ofn.nFilterIndex = 1; + ofn.lpstrDefExt = extension; + + return &ofn; +} + +const char * filePathForReading(const char * extension) { + OPENFILENAME * ofn = getOFN(extension); + if (ofn != nullptr && GetOpenFileName(ofn)) { + return ofn->lpstrFile; + } + return nullptr; +} + +const char * filePathForWriting(const char * extension) { + OPENFILENAME * ofn = getOFN(extension); + if (ofn != nullptr && GetSaveFileName(ofn)) { + return ofn->lpstrFile; + } + return nullptr; +} + +} +} +} diff --git a/ion/src/simulator/windows/platform_images.cpp b/ion/src/simulator/windows/platform_images.cpp new file mode 100644 index 000000000..436cabc04 --- /dev/null +++ b/ion/src/simulator/windows/platform_images.cpp @@ -0,0 +1,159 @@ +#include "../shared/platform.h" +#include + +#include +#include +#include +#include +#include + +/* Loading images using GDI+ + * On Windows, we manipulate images using GDI+ which is widely available. + * Note that this adds an extra runtime dependency (as compared to just SDL), + * but this should not be an issue. */ + +static inline HRESULT CreateStreamOnResource(const char * name, LPSTREAM * stream) { + HINSTANCE hInstance = GetModuleHandle(0); + *stream = nullptr; + HRSRC hC = FindResource(hInstance, name, RT_RCDATA); + if (!hC) { + SDL_Log("Could not find resource %s", name); + return E_INVALIDARG; + } + // This is not really a HGLOBAL http://msdn.microsoft.com/en-us/library/windows/desktop/ms648046(v=vs.85).aspx + HGLOBAL hG = LoadResource(hInstance, hC); + if (!hG) { + SDL_Log("Could not load resource %s", name); + return E_INVALIDARG; + } + void * bytes = LockResource(hG); + ULONG size = SizeofResource(hInstance, hC); + HRESULT hr = CreateStreamOnHGlobal(NULL, true, stream); + if (SUCCEEDED(hr)) { + ULONG written; + hr = (*stream)->Write(bytes, size, &written); + } + return hr; +} + +// Helper class to init/shutdown Gdiplus using RAII +class GdiplusSession { +public: + GdiplusSession() { + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + Gdiplus::GdiplusStartup(&m_gdiplusToken, &gdiplusStartupInput, nullptr); + } + ~GdiplusSession() { + Gdiplus::GdiplusShutdown(m_gdiplusToken); + } +private: + ULONG_PTR m_gdiplusToken; +}; + +// Helper function from MSDN +// https://docs.microsoft.com/en-us/windows/win32/gdiplus/-gdiplus-retrieving-the-class-identifier-for-an-encoder-use +int GetEncoderClsid(const WCHAR * format, CLSID * pClsid) { + UINT num = 0; // number of image encoders + UINT size = 0; // size of the image encoder array in bytes + + Gdiplus::ImageCodecInfo * pImageCodecInfo = nullptr; + Gdiplus::GetImageEncodersSize(&num, &size); + if (size == 0) { + return -1; + } + pImageCodecInfo = static_cast(malloc(size)); + if (pImageCodecInfo == nullptr) { + return -1; + } + + Gdiplus::GetImageEncoders(num, size, pImageCodecInfo); + + for (UINT i=0; i= 0); + const char * resname = MAKEINTRESOURCE(resourceID); + CreateStreamOnResource(resname, &stream); + + Gdiplus::Bitmap * image = Gdiplus::Bitmap::FromStream(stream); + + int width = (int)image->GetWidth(); + int height = (int)image->GetHeight(); + Gdiplus::Rect rc(0, 0, width, height); + + Gdiplus::BitmapData * bitmapData = new Gdiplus::BitmapData; + image->LockBits(&rc, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, bitmapData); + + size_t bytesPerPixel = 4; + + SDL_Texture * texture = SDL_CreateTexture( + renderer, + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STATIC, + width, + height + ); + + SDL_UpdateTexture( + texture, + NULL, + bitmapData->Scan0, + bytesPerPixel * width + ); + + SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND); + + image->UnlockBits(bitmapData); + delete bitmapData; + delete image; + + return texture; +} + +void saveImage(const KDColor * pixels, int width, int height, const char * path) { + static_assert(sizeof(KDColor) == 2, "KDColor expected to be RGB565"); + GdiplusSession session; + + Gdiplus::Bitmap bitmap(width, height, 2*width, PixelFormat16bppRGB565, reinterpret_cast(const_cast(pixels))); + + CLSID pngClsid; + if (GetEncoderClsid(L"image/png", &pngClsid) > 0) { + wchar_t * widePath = createWideCharArray(path); + bitmap.Save(widePath, &pngClsid, nullptr); + delete[] widePath; + } +} + +} +} +} diff --git a/ion/src/simulator/windows/language.cpp b/ion/src/simulator/windows/platform_language.cpp similarity index 84% rename from ion/src/simulator/windows/language.cpp rename to ion/src/simulator/windows/platform_language.cpp index 22d7909f8..5cc652a48 100644 --- a/ion/src/simulator/windows/language.cpp +++ b/ion/src/simulator/windows/platform_language.cpp @@ -1,8 +1,11 @@ #include "../shared/platform.h" - #include -char * IonSimulatorGetLanguageCode() { +namespace Ion { +namespace Simulator { +namespace Platform { + +const char * languageCode() { /* Per documentation, the maximum number of characters allowed for the * language string is nine, including a terminating null character. */ static char buffer[9] = {0}; @@ -19,3 +22,7 @@ char * IonSimulatorGetLanguageCode() { } return buffer; } + +} +} +} diff --git a/kandinsky/include/kandinsky/ion_context.h b/kandinsky/include/kandinsky/ion_context.h index 5cfd6cd9e..30807b7b1 100644 --- a/kandinsky/include/kandinsky/ion_context.h +++ b/kandinsky/include/kandinsky/ion_context.h @@ -27,6 +27,7 @@ public: bool zoomInhibit; bool gammaEnabled; int zoomPosition; + static void putchar(char c); private: KDIonContext(); void pushRect(KDRect rect, const KDColor * pixels) override; diff --git a/kandinsky/src/ion_context.cpp b/kandinsky/src/ion_context.cpp index ee4e1edd4..71efb8490 100644 --- a/kandinsky/src/ion_context.cpp +++ b/kandinsky/src/ion_context.cpp @@ -1,5 +1,5 @@ #include -#include +#include KDRealIonContext::KDRealIonContext() : KDContext(KDPointZero, KDRect(0, 0, Ion::Display::Width, Ion::Display::Height)) {} void KDRealIonContext::pushRect(KDRect rect, const KDColor * pixels) { @@ -61,3 +61,12 @@ void KDIonContext::pullRect(KDRect rect, KDColor * pixels) { } rootContext->pullRect(rect, pixels); } + +void KDIonContext::putchar(char c) { + static KDPoint cursor = KDPointZero; + char text[2] = {c, 0}; + cursor = sharedContext()->drawString(text, cursor); + if (cursor.y() > Ion::Display::Height) { + cursor = KDPoint(cursor.x(), 0); + } +}