diff --git a/.gitignore b/.gitignore index d6ed578..74d74f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,18 @@ /.vscode/ /SDL2/ -*.bat + +clean.bat +Run.bat +run_test.bat +compile.bat + *.o *.exe *.dll /games/ output +log*.txt /Release/ build out @@ -16,7 +22,72 @@ out .vscode *.pdb /src/unitTests/TestRoms/*.nes + +### Qt Generated Ignores ### +*~ +*.autosave +*.a +*.core +*.moc +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* +CMakeLists.txt.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release Tetroid *.nes Emulator_tests -Logo.png \ No newline at end of file +Logo.png + +logs/ +save/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index ff41e26..73638d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,100 +1,123 @@ cmake_minimum_required(VERSION 3.16) -project(CMakeSFMLProject LANGUAGES CXX) - -set(CMAKE_EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/..) -set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/..) -set(EXECUTABLE_NAME Tetroid) -set(UNIT_TEST_EXE Emulator_tests) -# string(APPEND CMAKE_CXX_FLAGS "-Wall") -string(APPEND CMAKE_CXX_FLAGS " -g") -# set(CMAKE_BUILD_TYPE Debug) -include(FetchContent) -FetchContent_Declare( - googletest - URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip + +project(tetroidnes VERSION 1.0.0 LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(EXECUTABLE_NAME TetroidNES) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(WIN32) +if(CMAKE_BUILD_TYPE STREQUAL "Debug") +set(DLL_DEPENDENCIES + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-graphics-d-2.dll + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-system-d-2.dll + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-window-d-2.dll +) +else() +set(DLL_DEPENDENCIES + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-graphics-2.dll + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-system-2.dll + ${CMAKE_CURRENT_BINARY_DIR}/_deps/sfml-build/lib/sfml-window-2.dll +) +endif() +endif() + +if(CMAKE_BUILD_TYPE STREQUAL "Debug") +set(VERSION_DEF "#define VERSION \"${PROJECT_VERSION}-Debug\"") +else() +set(VERSION_DEF "#define VERSION \"${PROJECT_VERSION}\"") +endif() + +set(QT_REQUIRED_COMPONENTS Widgets Core Gui LinguistTools) + +file(GLOB QT_TRANSLATION_FILES "src/gui/qt_tetroidnes/translations/tetroidnes_*.ts") +file(GLOB EMULATOR_HEADERS "src/include/Emulator/*.h") +file(GLOB EMULATOR_SOURCES "*src/emulator/*.cpp") +file(GLOB QT_PROJECT_SOURCES "src/gui/qt_tetroidnes/*.cpp") +file(GLOB QT_RCC_FILES "src/gui/qt_tetroidnes/resources/*.qrc") +file(GLOB QT_HEADERS "src/include/Qt/*.h") +file(GLOB QT_UI_FILES "src/gui/qt_tetroidnes/*.ui") + +find_package(OpenGL REQUIRED COMPONENTS OpenGL) + +find_package(Qt6 REQUIRED COMPONENTS + ${QT_REQUIRED_COMPONENTS} ) -# For Windows: Prevent overriding the parent project's compiler/linker settings -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) -option(BUILD_SHARED_LIBS "Build shared libraries" OFF) include(FetchContent) FetchContent_Declare(SFML GIT_REPOSITORY https://github.com/SFML/SFML.git GIT_TAG 2.6.x) FetchContent_MakeAvailable(SFML) +include_directories(${EXECUTABLE_NAME} PRIVATE src/include/Qt) +qt_add_executable(${EXECUTABLE_NAME} + MANUAL_FINALIZATION + ${QT_RCC_FILES} + ${QT_PROJECT_SOURCES} + ${EMULATOR_SOURCES} + ${QT_HEADERS} -if(DEFINED UNITTESTS AND UNITTESTS STREQUAL "ON") - enable_testing() - add_executable( - ${UNIT_TEST_EXE} - src/unitTests/RunUnitTests.cc - src/unitTests/emulator_test.cc - src/SetupSFML.cpp - src/emulator/APU.cpp - src/emulator/AddressMode.cpp - src/emulator/BitOperations.cpp - src/emulator/Bus.cpp - src/emulator/Computer.cpp - src/emulator/Instructions.cpp - src/emulator/LoadRom.cpp - src/emulator/PPU.cpp - src/emulator/StatusRegister.cpp - src/emulator/NESError.cpp - src/emulator/InstructionMap.cpp + ${QT_UI_FILES} ) -target_link_libraries( - ${UNIT_TEST_EXE} - GTest::gtest_main -) -target_include_directories(${UNIT_TEST_EXE} PRIVATE ${CMAKE_SOURCE_DIR}/src/include) +qt_add_translations(${EXECUTABLE_NAME} TS_FILES ${QT_TRANSLATION_FILES}) -target_link_libraries( - ${UNIT_TEST_EXE} - sfml-graphics -) -include(GoogleTest) -gtest_discover_tests( ${UNIT_TEST_EXE}) -target_compile_features( ${UNIT_TEST_EXE} PRIVATE cxx_std_20) +if(ANDROID) + set_property(TARGET ${EXECUTABLE_NAME} APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/android) +endif() +include_directories(${EXECUTABLE_NAME} PRIVATE src/include/) -install(TARGETS ${UNIT_TEST_EXE} -) -else() -add_executable(${EXECUTABLE_NAME} src/main.cpp - src/SetupSFML.cpp - src/emulator/APU.cpp - src/emulator/AddressMode.cpp - src/emulator/BitOperations.cpp - src/emulator/Bus.cpp - src/emulator/Computer.cpp - src/emulator/Instructions.cpp - src/emulator/LoadRom.cpp - src/emulator/PPU.cpp - src/emulator/StatusRegister.cpp - src/emulator/NESError.cpp - src/emulator/InstructionMap.cpp -) +target_link_libraries(${EXECUTABLE_NAME} PRIVATE Qt6::Widgets Qt6::Gui Qt6::Core) +target_link_libraries(${EXECUTABLE_NAME} PRIVATE sfml-system sfml-graphics sfml-window) -target_include_directories(${EXECUTABLE_NAME} PRIVATE ${CMAKE_SOURCE_DIR}/src/include) - -target_link_libraries(${EXECUTABLE_NAME} PRIVATE sfml-graphics) -target_compile_features(${EXECUTABLE_NAME} PRIVATE cxx_std_20) -install(TARGETS ${EXECUTABLE_NAME}) +#target_link_libraries(${EXECUTABLE_NAME} PRIVATE OpenGL::GLES2) +set_target_properties(${EXECUTABLE_NAME} PROPERTIES + ${BUNDLE_ID_OPTION} + MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} + MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} + MACOSX_BUNDLE TRUE + WIN32_EXECUTABLE TRUE +) +include(GNUInstallDirs) +install(TARGETS ${EXECUTABLE_NAME} + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) +install(CODE + file(GET_RUNTIME_DEPENDENCIES DIRECTORY ${QT_REQUIRED_COMPONENTS}) +) +if(WIN32) +# Copy DLL Depencencies +add_custom_command(TARGET ${EXECUTABLE_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${DLL_DEPENDENCIES} $ +) endif() +# Update version macro if needed +file(READ src/gui/qt_tetroidnes/Main.cpp FILE_CONTENT) +string(REGEX MATCH "#define VERSION \"[^\"]*\"" FIRST_LINE "${FILE_CONTENT}") +#message(STATUS "First line: ${FIRST_LINE} Replacing to: ${VERSION_DEF}") +string(REPLACE "${FIRST_LINE}" "${VERSION_DEF}" NEW_CONTENT "${FILE_CONTENT}") +#message(STATUS "New Content:\n${NEW_CONTENT}") +if(NOT NEW_CONTENT STREQUAL FILE_CONTENT) + file(WRITE src/gui/qt_tetroidnes/Main.cpp + "${NEW_CONTENT}" + ) -# if(WIN32) -# add_custom_command( -# TARGET ${EXECUTABLE_NAME} -# COMMENT "Copy OpenAL DLL" -# PRE_BUILD COMMAND ${CMAKE_COMMAND} -E copy ${SFML_SOURCE_DIR}/extlibs/bin/$,x64,x86>/openal32.dll $ -# VERBATIM) -# endif() -# uncomment if you are running this in Visual Studio or if you get an error on windows with the current setup +else() + message(STATUS "Version line not found or is the same") +endif() +qt_finalize_executable(${EXECUTABLE_NAME}) diff --git a/LICENSE b/LICENSE index 88549b1..f288702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,674 @@ -MIT License - -Copyright (c) 2024 Lillith - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index b85a99f..97a9e28 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,45 @@ # TetroidNES - ![ci build][build] ![license][license] TetroidNES is an open source multi platform NES emulator. -It currently has support for both windows and debian based linux distros +It currently has support for both Windows and debian based linux distros ## Build Instructions glance at the Prerequisites page in the wiki to see what you need to download and compiler -[Prerequisites] - -```SH -# build Cmake you can set the -DUNITTESTS flag to build unit tests or the relase - -# this builds the project -cmake -S . -B build -DUNITTESTS=OFF - -# this builds the unit tests -cmake -S . -B build -DUNITTESTS=ON -# build amd run project -cmake --build build --config Release - -./Release/Tetroid.exe ${path to .nes rom file} # windws -./Tetroid {path to .nes rom file} # linux +[Prerequisites] -# TODO install cc65 assembler and add it to your PATH/bin directory -# if you want buildTestRoms.ps1 to work -# downlaode page: -# make sure TestRoms are up to date +once you do that run this -# builds the Test Roms -. .\buildTestRoms.ps1 -build +```SH +cmake -DCMAKE_PREFIX_PATH=~/path/to/Qt/6.8.0/gcc_64/ -DCMAKE_CXX_FLAGS="-std=c++17" -S . -B build # this builds the CMake +cmake --build build --config Release # builds project -./Emulator_tests # linux -./Debug/Emulator_tests.exe # windows +./build.sh # build script if this is too muc +./build/TetroidNES # runs project ``` +check out the ./log/ directory to see logs + ## Documentation [documentation] a special thanks to all who helped me make this +## contributors + + + including wolfymyth. helped alot +

and wolfymyth who helped alot with the UI

+
+ [documentation]: [build]: [license]: diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..5f95f6b --- /dev/null +++ b/build.bat @@ -0,0 +1,5 @@ +@echo off +cmake -G "MinGW Makefiles" -DCMAKE_PREFIX_PATH=C:\Qt\6.8.0\mingw_64\ -DCMAKE_CXX_FLAGS="-std=c++17" -S . -B build +cmake --build build --config Release +cd build +TetroidNES.exe \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..a39bc53 --- /dev/null +++ b/build.sh @@ -0,0 +1,10 @@ +!#/bin/bash/ +if cmake -DCMAKE_PREFIX_PATH=~/Qt/6.8.0/gcc_64/ -DCMAKE_CXX_FLAGS="-std=c++17" -S . -B build; then + if cmake --build build --config Release; then + ./build/TetroidNES + else + echo "compiler error" + fi +else + echo "cmake error" +fi \ No newline at end of file diff --git a/buildTestRoms.ps1 b/buildTestRoms.ps1 deleted file mode 100644 index b6cd0b2..0000000 --- a/buildTestRoms.ps1 +++ /dev/null @@ -1,24 +0,0 @@ -function build { - $RomList = "nya~", - "./src/unitTests/TestRomsSource/LDAtest.s -o ./src/unitTests/TestRoms/LDATest.nes", - "./src/unitTests/TestRomsSource/JMPTest.s -o ./src/unitTests/TestRoms/JMPTest.nes", - "./src/unitTests/TestRomsSource/TestAsl.s -o ./src/unitTests/TestRoms/TestAsl.nes" - - Write-Output "" - - foreach ($p in $RomList) { - $args = @($p -split "\s") - & "cl65" "--target" "nes" @args - if ($?) { - Write-Output ("Built {0} successfully" -f $args[2]) - - } - else { - Write-Output ("Built {0} unsuccessfully" -f $args[2]) - - } - Write-Output "" - - } - -} \ No newline at end of file diff --git a/buildTestRoms.sh b/buildTestRoms.sh index 6e8d0ad..3145db5 100755 --- a/buildTestRoms.sh +++ b/buildTestRoms.sh @@ -1,15 +1,13 @@ #!/bin/bash build() { - # Define the list of ROM build commands - cmake -S . -B build -DUNITTESTS=ON - mkdir -p ./src/unitTests/TestRoms/ + # mkdir -p ./src/unitTests/TestRoms/ + mkdir -p ./games/ RomList=( "nya~" - "./src/unitTests/TestRomsSource/LDATest.s -o ./src/unitTests/TestRoms/LDATest.nes" - "./src/unitTests/TestRomsSource/JMPTest.s -o ./src/unitTests/TestRoms/JMPTest.nes" - "./src/unitTests/TestRomsSource/TestASL.s -o ./src/unitTests/TestRoms/TestAsl.nes" + "./src/TestRomSrc/WhiteBG.s -o ./games/WhiteBG.nes" + ) echo "" diff --git a/src/TestRomSrc/Skeleton.s b/src/TestRomSrc/Skeleton.s new file mode 100644 index 0000000..4f80191 --- /dev/null +++ b/src/TestRomSrc/Skeleton.s @@ -0,0 +1,112 @@ +; sekelton if you want to add on to the tests + + +.segment "HEADER" + ; .byte "NES", $1A ; iNES header identifier + .byte $4E, $45, $53, $1A + .byte 2 ; 2x 16KB PRG code + .byte 1 ; 1x 8KB CHR data + .byte $01, $00 ; mapper 0, vertical mirroring +.segment "VECTORS" + ;; When an NMI happens (once per frame if enabled) the label nmi: + .addr nmi + ;; When the processor first turns on or is reset, it will jump to the label reset: + .addr reset ; reset vector +.segment "STARTUP" + +reset: + sei + cld + + ; reset sound + ldx #%10000000 + stx $4017 + ldx #$00 + stx $4010 + + ; init stack + ldx #$FF + TXS + + ; clear PPU registers + LDX $00 + STX $2000 + STX $2001 + + ;wait for vblank + ; jsr wait_for_vblank + vblank_loop: + bit $2002 + bpl vblank_loop + txa + clear_mem: + sta $0000, X + sta $0100, X + lda $FF + sta $0200, X + lda $00 + sta $0300, X + sta $0400, X + sta $0500, X + sta $0600, X + sta $0700, X + inx + cpx #$00 + bne clear_mem + ; jsr wait_for_vblank + vblank_loop2: + bit $2002 + bpl vblank_loop2 + + lda #$02 + sta $4014 + nop + + ; store 3f00 in ppu + lda #$3f + sta $2006 + lda #$00 + sta $2006 + + load_pallete: + LDA palletes,X + sta $2007 + inx + cpx #$20 + bne load_pallete + + ldx #$00 + load_sprites: + lda sprite_data, X + sta $0200, X + inx + cpx #$10 + bne load_sprites + cli + lda #%10000000 ; vblank status + sta $2000 + lda #%00010000 ; vblank status + sta $2001 + + loop: + JMP loop +nmi: + lda #$02 + sta $4014 + rti +palletes: + .byte $00, $0F, $00, $10, $00, $0A, $15, $01, $00, $29, $28, $27, $00, $34, $24, $14 ;background palettes + .byte $31, $0F, $15, $30, $00, $0F, $11, $30, $00, $0F, $30, $27, $00, $3C, $2C, $1C ;sprite palettes + + +sprite_data: +;Y, SPRITE NUM, attributes, X +;76543210 +;|||||||| +;||||||++- Palette (4 to 7) of sprite +;|||+++--- Unimplemented +;||+------ Priority (0: in front of background; 1: behind background) +;|+------- Flip sprite horizontally +;+-------- Flip sprite vertically + .byte $24, $00, $00, $40 +.segment "CHARS" ; for graphics diff --git a/src/TestRomSrc/WhiteBG.s b/src/TestRomSrc/WhiteBG.s new file mode 100644 index 0000000..09d99e2 --- /dev/null +++ b/src/TestRomSrc/WhiteBG.s @@ -0,0 +1,115 @@ +; sekelton if you want to add on to the tests + +; for testing the CRT affect shader +.segment "HEADER" + ; .byte "NES", $1A ; iNES header identifier + .byte $4E, $45, $53, $1A + .byte 2 ; 2x 16KB PRG code + .byte 1 ; 1x 8KB CHR data + .byte $01, $00 ; mapper 0, vertical mirroring +.segment "VECTORS" + ;; When an NMI happens (once per frame if enabled) the label nmi: + .addr nmi + ;; When the processor first turns on or is reset, it will jump to the label reset: + .addr reset ; reset vector +.segment "STARTUP" +reset: + sei + cld + + ; reset sound + ldx #%10000000 + stx $4017 + ldx #$00 + stx $4010 + + ; init stack + ldx #$FF + TXS + + ; clear PPU registers + LDX $00 + STX $2000 + STX $2001 + + ;wait for vblank + ; jsr wait_for_vblank + ; vblank_loop: + ; bit $2002 + ; bpl vblank_loop + txa + clear_mem: + sta $0000, X + sta $0100, X + lda $FF + sta $0200, X + lda $00 + sta $0300, X + sta $0400, X + sta $0500, X + sta $0600, X + sta $0700, X + inx + cpx #$00 + bne clear_mem + ; jsr wait_for_vblank + vblank_loop2: + bit $2002 + bpl vblank_loop2 + + lda #$02 + sta $4014 + nop + + ; store 3f00 in ppu + lda #$3f + sta $2006 + lda #$00 + sta $2006 + + load_pallete: + LDA palletes,X + sta $2007 + inx + cpx #$20 + bne load_pallete + + ldx #$00 + load_sprites: + lda sprite_data, X + sta $0200, X + inx + cpx #$10 + bne load_sprites + cli + lda #%10010000 ; vblank status + sta $2000 + lda #%00011110 ; vblank status + sta $2001 + + loop: + JMP loop +nmi: + lda #$02 + sta $4014 + rti +palletes: + .byte $00, $0F, $00, $10, $00, $0A, $15, $01, $00, $29, $28, $27, $00, $34, $24, $14 ;background palettes + .byte $20, $0F, $15, $30, $20, $0F, $11, $30, $20, $0F, $30, $27, $20, $3C, $2C, $1C ;sprite palettes + + +sprite_data: +;Y, SPRITE NUM, attributes, X +;76543210 +;|||||||| +;||||||++- Palette (4 to 7) of sprite +;|||+++--- Unimplemented +;||+------ Priority (0: in front of background; 1: behind background) +;|+------- Flip sprite horizontally +;+-------- Flip sprite vertically + .byte $24, $00, $00, $40 + ; .byte $40, $01, $00, $48 + ; .byte $48, $10, $00, $40 + ; .byte $48, $11, $00, $48 +.segment "CHARS" ; for graphics +; .incbin "rom.chr" \ No newline at end of file diff --git a/src/depreciated/Computer.cpp b/src/depreciated/Computer.cpp new file mode 100644 index 0000000..ebed8b8 --- /dev/null +++ b/src/depreciated/Computer.cpp @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include +#include +#include "./NESError.h" +// #include +#include + +#include +// #include "Bus.h +#include +#include +#include +#include +#include +namespace fs = std::filesystem; + +#define PC_RESET 0x8000 +#define PC_END 0xffff + +uint8_t current_instruction = 0; + +CPU run(CPU cpu, std::string file_name); + +CPU init(std::string file_name) +{ + // initializeInstructionMap(); + // std::vector v = file_tobyte_vector(file_name); + // Bus bus(load_rom(v), 0x8000); + CPU cpu; + // bus.fill(bus.read_16bit(0xfffc)); + // cpu.A_Reg = 0; + // cpu.status.val = 0; + // cpu.X_Reg = 0; + // cpu.Y_Reg = 0; + // cpu.bus = bus; + // cpu.bus.clock_cycles = 0; + // std::string window_name = fs::path(file_name) + // .filename() + // .replace_extension() + // .string(); + // return run(cpu, window_name); + return cpu; +} +void printCPU_stats(CPU cpu) +{ + + cpu.bus.print_ppu(); + printf("\n===== CPU ON EXIT =========== \n"); + printf("\n"); + printf("Accumaltor: decimal: %d hexa: 0x%x \n", cpu.A_Reg, cpu.A_Reg); + printf("X Register: decimal: %d hexa: 0x%x \n", cpu.X_Reg, cpu.X_Reg); + printf("Y Register: decimal: %d hexa: 0x%x \n", cpu.Y_Reg, cpu.Y_Reg); + printf("Program Counter: 0x%X \n", cpu.bus.get_PC()); + cpu.bus.print_stack(); + std::bitset<7> status(cpu.status.val); + cpu.bus.print_clock(); + std::cout << "cpu status register: 0b" << status << std::endl; + printf("\n============================= \n"); + printf("\n"); +} + +void HandleNMIInterrupts(CPU &cpu) +{ + cpu.bus.push_stack8(cpu.status.val); + + cpu.bus.push_stack16(cpu.bus.get_PC() - 1); + cpu.bus.fetch_next(); + set_interrupt_disabled(1, cpu); + // std::cout << "test" << std::endl; + + cpu.bus.fill(cpu.bus.read_16bit(0xfffa)); + // printf("%x \n", cpu.bus.get_PC()); +} + +/** + * Executes actual code + */ +CPU run(CPU cpu, std::string window_name) +{ + + sf::RenderWindow window(sf::VideoMode(800, 600), window_name); + + window.setFramerateLimit(144); + sf::Texture texture; + texture.create(256, 240); + float scaleX = window.getSize().x / (float)(texture.getSize().x); + float scaleY = window.getSize().y / (float)(texture.getSize().y); + sf::Sprite sprite(texture); + sprite.setScale(scaleX, scaleY); + // uint8_t arr[257]; + // uint8_t data[] + + while (cpu.bus.get_PC() < PC_END && window.isOpen()) + { + for (auto event = sf::Event{}; window.pollEvent(event);) // checks if window is closed or event going + { + if (event.type == sf::Event::Closed) + { + window.close(); + program_success(cpu); + cpu.error_code = EXIT_SUCCESS; + return cpu; + } + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) + { + cpu.bus.write_controller1(Controller::UP, 1); + } + else + { + cpu.bus.write_controller1(Controller::UP, 0); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) + { + cpu.bus.write_controller1(Controller::DOWN, 1); + } + else + { + cpu.bus.write_controller1(Controller::DOWN, 0); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) + { + cpu.bus.write_controller1(Controller::LEFT, 1); + } + else + { + cpu.bus.write_controller1(Controller::LEFT, 0); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) + { + cpu.bus.write_controller1(Controller::RIGHT, 1); + } + else + { + cpu.bus.write_controller1(Controller::RIGHT, 0); + } + + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) + { + cpu.bus.write_controller1(Controller::A, 1); + } + else + { + cpu.bus.write_controller1(Controller::A, 0); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LShift)) + { + cpu.bus.write_controller1(Controller::B, 1); + } + else + { + cpu.bus.write_controller1(Controller::B, 0); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::Enter)) + { + cpu.bus.write_controller1(Controller::START, 1); + } + else + { + cpu.bus.write_controller1(Controller::START, 0); + } + if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) + { + cpu.bus.write_controller1(Controller::SELECT, 1); + } + else + { + cpu.bus.write_controller1(Controller::SELECT, 0); + } + + cpu.bus.write_8bit(0xfe, ((uint8_t)rand() % 16 + 1)); + + cpu.bus.tick(); + + // NMI interrupts + if (cpu.bus.NMI_interrupt()) + { + HandleNMIInterrupts(cpu); + // printf("%x \n", cpu.bus.get_PC()); + } + current_instruction = cpu.bus.fetch_next(); + // printf("%x \n", cpu.bus.get_PC()); + + if (InstructionValid(current_instruction)) + { + Instruction a = GetInstruction(current_instruction); + a.i(a.addressmode, cpu); + std::vector rgb_data_vector = cpu.bus.render_texture({NES_RES_L, NES_RES_W}); + uint8_t rgb_data[NES_RES_A * 4]; + std::copy(rgb_data_vector.begin(), rgb_data_vector.end(), rgb_data); + // nes_cpu. + // for (int i = 0; i < nes_cpu.size(); i++) + // { + // rgb_da + // } + // cpu.bus.render_texture(rgb_data); + window.clear(); // Change this to the desired color + + texture.update(rgb_data); + // // printf("pc: 0x%x current instrcution 0x%x \n", cpu.bus.get_PC(), current_instruction); + // // cpu.bus.render(texture, 0, 0); + // // cpu.bus.render() + window.draw(sprite); + window.display(); + } + else + { + printf("instruction opcode 0x%x is unrecongnized \n", current_instruction); + program_failure("Unrecognized instruction encountered", cpu, 1); + cpu.error_code = EXIT_FAILURE; + return cpu; + } + } + return cpu; +} diff --git a/src/emulator/NESError.h b/src/depreciated/NESError.h similarity index 100% rename from src/emulator/NESError.h rename to src/depreciated/NESError.h diff --git a/src/emulator/NESError.cpp b/src/depreciated/NES_Error.cpp similarity index 86% rename from src/emulator/NESError.cpp rename to src/depreciated/NES_Error.cpp index 640ecda..14dc9bf 100644 --- a/src/emulator/NESError.cpp +++ b/src/depreciated/NES_Error.cpp @@ -1,4 +1,4 @@ -#include +#include #include void cpu_to_log(std::string path) @@ -7,7 +7,7 @@ void cpu_to_log(std::string path) } void program_success(CPU cpu) { - printCPU_stats(cpu); + // printCPU_stats(cpu); std::cout << "" << std::endl; std::cout << "\033[92mProgram has successfully exited" << std::endl; std::cout << "exit code 0 \033[0m" << std::endl; @@ -16,7 +16,7 @@ void program_success(CPU cpu) void program_failure(std::string reason, CPU cpu, int exit_code) { std::cout << reason << std::endl; - printCPU_stats(cpu); + // printCPU_stats(cpu); printf("\n"); std::cout << "\033[91mProgram unsuccessfully exited" << std::endl; std::cout << "exit code 1\033[0m" << std::endl; diff --git a/src/SetupSFML.cpp b/src/depreciated/SetupSFML.cpp similarity index 100% rename from src/SetupSFML.cpp rename to src/depreciated/SetupSFML.cpp diff --git a/src/SetupSFML.h b/src/depreciated/SetupSFML.h similarity index 100% rename from src/SetupSFML.h rename to src/depreciated/SetupSFML.h diff --git a/src/main.cpp b/src/depreciated/main.cpp similarity index 100% rename from src/main.cpp rename to src/depreciated/main.cpp diff --git a/src/unitTests/RunUnitTests.cc b/src/depreciated/unitTests/RunUnitTests.cc similarity index 100% rename from src/unitTests/RunUnitTests.cc rename to src/depreciated/unitTests/RunUnitTests.cc diff --git a/src/unitTests/TestRomsSource/JMPTest.s b/src/depreciated/unitTests/TestRomsSource/JMPTest.s similarity index 100% rename from src/unitTests/TestRomsSource/JMPTest.s rename to src/depreciated/unitTests/TestRomsSource/JMPTest.s diff --git a/src/unitTests/TestRomsSource/LDATest.s b/src/depreciated/unitTests/TestRomsSource/LDATest.s similarity index 100% rename from src/unitTests/TestRomsSource/LDATest.s rename to src/depreciated/unitTests/TestRomsSource/LDATest.s diff --git a/src/unitTests/TestRomsSource/NESAssemblyFileSkeleton.s b/src/depreciated/unitTests/TestRomsSource/NESAssemblyFileSkeleton.s similarity index 100% rename from src/unitTests/TestRomsSource/NESAssemblyFileSkeleton.s rename to src/depreciated/unitTests/TestRomsSource/NESAssemblyFileSkeleton.s diff --git a/src/unitTests/TestRomsSource/TestASL.s b/src/depreciated/unitTests/TestRomsSource/TestASL.s similarity index 100% rename from src/unitTests/TestRomsSource/TestASL.s rename to src/depreciated/unitTests/TestRomsSource/TestASL.s diff --git a/src/unitTests/emulator_test.cc b/src/depreciated/unitTests/emulator_test.cc similarity index 100% rename from src/unitTests/emulator_test.cc rename to src/depreciated/unitTests/emulator_test.cc diff --git a/src/emulator/AddressMode.cpp b/src/emulator/Address_Mode.cpp similarity index 98% rename from src/emulator/AddressMode.cpp rename to src/emulator/Address_Mode.cpp index 97bfe90..9147747 100644 --- a/src/emulator/AddressMode.cpp +++ b/src/emulator/Address_Mode.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include /** * @brief get PC because this is the address bus this. does the immediate address or diff --git a/src/emulator/BitOperations.cpp b/src/emulator/Bit_Operations.cpp similarity index 98% rename from src/emulator/BitOperations.cpp rename to src/emulator/Bit_Operations.cpp index e3871db..2da4083 100644 --- a/src/emulator/BitOperations.cpp +++ b/src/emulator/Bit_Operations.cpp @@ -1,7 +1,7 @@ #include #include #include -#include +#include uint8_t add(uint8_t a, uint8_t b, CPU &cpu, uint8_t &carry) { uint8_t c_in = 0; diff --git a/src/emulator/Bus.cpp b/src/emulator/Bus.cpp index f6b07c2..73c2021 100644 --- a/src/emulator/Bus.cpp +++ b/src/emulator/Bus.cpp @@ -7,7 +7,9 @@ #include #include #include -// #include "Bus.h> +#include +#include + #define TOP_STACK 0x1ff #define BOTTOM_STACK 0x100 #define STACK_RESET 0xfd @@ -32,11 +34,13 @@ Bus::Bus(Rom rom, uint16_t pc_start) this->ppu = nes_ppu; // test // this->ppu.chr_rom = rom.CHR; // this->ppu.mirrorType = rom.mirror; + this->err_string = std::nullopt; APU APU(); this->apu = apu; // test this->stack_pointer = STACK_RESET; this->stack = BOTTOM_STACK + stack_pointer; + this->clock_cycles_instr = 0; } uint16_t Bus::get_PC() { @@ -69,6 +73,8 @@ uint8_t Bus::fetch_next() stored_instructions[1] = stored_instructions[0]; // printf(" fetch: current_instrcution: 0x%x pc: 0x%x \n", current_instruction, this->program_counter); program_counter++; + this->tick(); + this->tick(); stored_instructions[0] = rom.PRG[this->program_counter - reset_vector]; return current_instruction; @@ -76,9 +82,17 @@ uint8_t Bus::fetch_next() void Bus::fill(uint16_t pc) { + if (pc < rom.PRG.size()) + { + err_string = "ROM size is to small"; + return; + } + stored_instructions[0] = rom.PRG[(pc + 1) - reset_vector]; stored_instructions[1] = rom.PRG[(pc - reset_vector)]; - clock_cycles += 2; + // clock_cycles += 2; + this->tick(); + this->tick(); this->program_counter = pc; // printf("current_instrcution: 0x%x pc: 0x%x \n", current_instruction, this->program_counter); // printf(" fill: proram counter: 0x%x current: 0x%x \n", pc, stored_instructions[1]); @@ -88,7 +102,9 @@ void Bus::fill(uint16_t pc) uint8_t Bus::read_8bit(uint16_t address) { - this->clock_cycles++; + // this->clock_cycles++; + // this->ppu.tick(3); + this->tick(); if (address < 0x1FFF) { uint16_t mirror_address = address & 0x7ff; @@ -103,7 +119,9 @@ uint8_t Bus::read_8bit(uint16_t address) if (address == 0x2007) return this->ppu.read_PPU_data(); else if (address == 0x2002) + { return this->ppu.read_status(); + } else if (address == 0x2004) { return this->ppu.read_OAM_data(); @@ -129,11 +147,11 @@ uint8_t Bus::read_8bit(uint16_t address) } else if (address == 0x4016) { - return read_joypad(); + return read_joypad1(); } else if (address == 0x4017) { - return joy_pad_byte2; + return read_joypad2(); } else if (address >= 0x8000 && address <= 0xFFFF) { @@ -144,7 +162,10 @@ uint8_t Bus::read_8bit(uint16_t address) void Bus::write_8bit(uint16_t address, uint8_t value) { - this->clock_cycles++; + // this->clock_cycles++; + // this->ppu.tick(3); + this->tick(); + // this->tick(); if (address <= 0x1FFF) { uint16_t mirror_address = address & 0x7ff; @@ -152,15 +173,13 @@ void Bus::write_8bit(uint16_t address, uint8_t value) } else if (address >= 0x2000 && address <= 0x3FFF) { - // std::cout << "ppu write" << address << std::endl; - // printf("%x \n", address); - if (address == 0x2000) { this->ppu.write_PPU_ctrl(value); } else if (address == 0x2001) { + this->ppu.write_PPU_mask(value); } else if (address == 0x2003) @@ -173,41 +192,50 @@ void Bus::write_8bit(uint16_t address, uint8_t value) } else if (address == 0x2005) { + // this->ppu.write_OAM_data(value); } else if (address == 0x2006) { + std::cout << "write to addr" << std::endl; + qInfo() << num_to_hexa(this->get_PC() - reset_vector); + std::cout << rom.PRG.size() << std::endl; + std::cout << rom.CHR.size() << std::endl; + this->ppu.write_PPU_address(value); } else if (address == 0x2007) { auto b = this->ppu.write_PPU_data(value); - if (b == std::nullopt) - { - this->stored_instructions[1] = 0x82; - } + + // if (b == std::nullopt) + // { + // this->stored_instructions[1] = 0x82; + // } // if () } else if (address >= 0x2008) this->write_8bit(address & 0x2007, value); else { - this->stored_instructions[1] = 0x82; - std::cout << "\033[91mforbidden access to PPU read only address\033[0m" << std::endl; - printf("address 0x%x \n", address); - std::cout << "" << std::endl; + // this->stored_instructions[1] = 0x82; + // std::cout << "\033[91mforbidden access to PPU read only address\033[0m" << std::endl; + // printf("address 0x%x \n", address); + // this->err_string = {"\033[91mforbidden access to PPU read only address\033[0m"}; + // std::cout << "" << std::endl; + this->err_string = "Address 0x" + num_to_hexa(address) + " is a PPU read only address"; } } else if (address == 0x4014) { - uint8_t startaddr = value << 8; - for (int i = 0; i < 256; i++) - this->ppu.write_OAM_data(this->read_8bit(startaddr | i)); + this->ppu.write_OAM_data(value); + qInfo() << "writting to OAM"; } else if (address == 0x4016) { strobe = (bool)value; - button_idx = 0; + joypad1_idx = 0; + joypad2_idx = 0; // std::cout << // joy_pad_byte1 = 0; // joy_pad_byte1 = value & 0b00000001; @@ -219,11 +247,11 @@ void Bus::write_8bit(uint16_t address, uint8_t value) // } else if (address >= 0x8000 && address <= 0xFFFF) { - this->stored_instructions[1] = 0x82; - - std::cout << "\033[91mAttempt to write into READ_ONLY_MEM\033[0m" << std::endl; - printf("address 0x%x \n", address); - std::cout << "" << std::endl; + // this->stored_instructions[1] = 0x82; + this->err_string = "Address 0x" + num_to_hexa(address) + " is `_ONLY on this emulator"; + // std::cout << "\033[91mAttempt to write into READ_ONLY_MEM\033[0m" << std::endl; + // printf("address 0x%x \n", address); + // std::cout << "" << std::endl; // exit(EXIT_FAILURE); } @@ -236,13 +264,14 @@ void Bus::write_8bit(uint16_t address, uint8_t value) uint16_t Bus::read_16bit(uint16_t address) { - clock_cycles += 2; + // clock_cycles += 2; if (address < 0x1FFF) { - uint16_t mirror_address = address & 0x7ff; - uint16_t value = (uint16_t)(v_memory[mirror_address + 1] << 8) | v_memory[mirror_address]; - return value; + // uint16_t mirror_address = address & 0x7ff; + // uint16_t value = (uint16_t)(v_memory[mirror_address + 1] << 8) | v_memory[mirror_address]; + return read_8bit(address + 1) << 8 | read_8bit(address); + // return value; } else if (address >= 0x2000 && address <= 0x3FFF) { @@ -251,24 +280,27 @@ uint16_t Bus::read_16bit(uint16_t address) else if (address >= 0x8000 && address <= 0xFFFF) { - uint8_t lsb = rom.PRG[address - reset_vector]; - uint8_t msb = rom.PRG[(address + 1) - reset_vector]; - return (uint16_t)(msb << 8) | lsb; + // uint8_t lsb = rom.PRG[address - reset_vector]; + // uint8_t msb = rom.PRG[(address + 1) - reset_vector]; + // return (uint16_t)(msb << 8) | lsb; + return read_8bit(address + 1) << 8 | read_8bit(address); } return 0; } void Bus::write_16bit(uint16_t address, uint16_t value) { - clock_cycles += 2; + // clock_cycles += 2; if (address < 0x1FFF) { - uint16_t mirror_address = address & 0x7ff; - uint8_t msb = (uint8_t)(value >> 8); - uint8_t lsb = (uint8_t)(value & 0xFF); - v_memory[mirror_address] = lsb; - v_memory[mirror_address + 1] = msb; + // uint16_t mirror_address = address & 0x7ff; + // uint8_t msb = (uint8_t)(value >> 8); + // uint8_t lsb = (uint8_t)(value & 0xFF); + // v_memory[mirror_address] = lsb; + // v_memory[mirror_address + 1] = msb; + write_8bit(address + 1, value >> 8); + write_8bit(address, value); } else if (address >= 0x2000 && address <= 0x3FFF) { @@ -279,9 +311,12 @@ void Bus::write_16bit(uint16_t address, uint16_t value) else if (address >= 0x8000 && address <= 0xFFFF) { - this->stored_instructions[1] = 0x82; + // this->stored_instructions[1] = 0x82; + // write_8bit(address + 1, value >> 8); + // write_8bit(address, value); + this->err_string = "Address 0x" + num_to_hexa(address) + " is READ_ONLY on this emulator"; - std::cout << "Attempt to write into READ_ONLY_MEM" << std::endl; + // std::cout << "Attempt to write into READ_ONLY_MEM" << std::endl; } } @@ -335,10 +370,20 @@ void Bus::print_stack() void Bus::tick() { - this->ppu.tick(this->clock_cycles * 3); + this->clock_cycles++; + this->ppu.tick(3); + this->clock_cycles_instr++; + // std::cout << "clock cycles: " << this->clock_cycles << std::endl; + // this->ppu.tick(this->clock_cycles * 3); + // qInfo() << "cpu clock cyles * 3: " << clock_cycles * 3; } -// void Bus::render(sf::Texture &texture, int bank, int tile) +int Bus::reset_clock() +{ + int ret = this->clock_cycles_instr; + this->clock_cycles_instr = 0; + return ret; +} // void Bus::render(sf::Texture &texture, int bank, int tile) // { // this->ppu.render(texture, bank, tile); // } @@ -348,16 +393,30 @@ bool Bus::NMI_interrupt() return this->ppu.NMI_interrupt(this->clock_cycles * 3); } -uint8_t Bus::read_joypad() +uint8_t Bus::read_joypad1() { - if (button_idx > 7) + if (joypad1_idx > 7) { return 1; } - uint8_t button = (joy_pad_byte1 << button_idx); + uint8_t button = (joy_pad_byte1 << joypad1_idx); if (strobe) { - button_idx++; + joypad1_idx++; + } + return button; +} + +uint8_t Bus::read_joypad2() +{ + if (joypad2_idx > 7) + { + return 1; + } + uint8_t button = (joy_pad_byte2 << joypad2_idx); + if (strobe) + { + joypad2_idx++; } return button; } @@ -376,7 +435,34 @@ void Bus::write_controller1(Controller value, int isPressed) joy_pad_byte1 &= ~((uint8_t)(value)); } +void Bus::write_controller2(Controller value, int isPressed) +{ + if (isPressed == 1) + joy_pad_byte2 |= (uint8_t)value; + else if (isPressed == 0) + joy_pad_byte2 &= ~((uint8_t)(value)); +} std::vector Bus::render_texture(std::tuple res) { return this->ppu.render_texture(res); } + +std::optional Bus::check_error() +{ + if (this->err_string.has_value()) + { + return this->err_string; + } + else if (this->ppu.err_string.has_value()) + { + return this->ppu.err_string; + } + return std::nullopt; + // return std::optional(); +} + +void Bus::log_ppu() +{ + + this->ppu.log_ppu(); +} diff --git a/src/emulator/Computer.cpp b/src/emulator/Computer.cpp deleted file mode 100644 index f2153e5..0000000 --- a/src/emulator/Computer.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include "./NESError.h" -// #include -#include - -#include -// #include "Bus.h -#include -#include -#include -#include -#include -namespace fs = std::filesystem; - -#define PC_RESET 0x8000 -#define PC_END 0xffff - -uint8_t current_instruction = 0; - -CPU run(CPU cpu, std::string file_name); - -CPU init(std::string file_name) -{ - initializeInstructionMap(); - std::vector v = file_tobyte_vector(file_name); - Bus bus(load_rom(v), 0x8000); - CPU cpu; - bus.fill(bus.read_16bit(0xfffc)); - cpu.A_Reg = 0; - cpu.status.val = 0; - cpu.X_Reg = 0; - cpu.Y_Reg = 0; - cpu.bus = bus; - cpu.bus.clock_cycles = 0; - std::string window_name = fs::path(file_name) - .filename() - .replace_extension() - .string(); - return run(cpu, window_name); -} -void printCPU_stats(CPU cpu) -{ - - cpu.bus.print_ppu(); - printf("\n===== CPU ON EXIT =========== \n"); - printf("\n"); - printf("Accumaltor: decimal: %d hexa: 0x%x \n", cpu.A_Reg, cpu.A_Reg); - printf("X Register: decimal: %d hexa: 0x%x \n", cpu.X_Reg, cpu.X_Reg); - printf("Y Register: decimal: %d hexa: 0x%x \n", cpu.Y_Reg, cpu.Y_Reg); - printf("Program Counter: 0x%X \n", cpu.bus.get_PC()); - cpu.bus.print_stack(); - std::bitset<7> status(cpu.status.val); - cpu.bus.print_clock(); - std::cout << "cpu status register: 0b" << status << std::endl; - printf("\n============================= \n"); - printf("\n"); -} - -void HandleNMIInterrupts(CPU &cpu) -{ - cpu.bus.push_stack8(cpu.status.val); - - cpu.bus.push_stack16(cpu.bus.get_PC() - 1); - cpu.bus.fetch_next(); - set_interrupt_disabled(1, cpu); - // std::cout << "test" << std::endl; - - cpu.bus.fill(cpu.bus.read_16bit(0xfffa)); - // printf("%x \n", cpu.bus.get_PC()); -} - -/** - * Executes actual code - */ -CPU run(CPU cpu, std::string window_name) -{ - - sf::RenderWindow window(sf::VideoMode(800, 600), window_name); - - window.setFramerateLimit(144); - sf::Texture texture; - texture.create(256, 240); - float scaleX = window.getSize().x / (float)(texture.getSize().x); - float scaleY = window.getSize().y / (float)(texture.getSize().y); - sf::Sprite sprite(texture); - sprite.setScale(scaleX, scaleY); - // uint8_t arr[257]; - // uint8_t data[] - - while (cpu.bus.get_PC() < PC_END && window.isOpen()) - { - for (auto event = sf::Event{}; window.pollEvent(event);) // checks if window is closed or event going - { - if (event.type == sf::Event::Closed) - { - window.close(); - program_success(cpu); - cpu.error_code = EXIT_SUCCESS; - return cpu; - } - } - if (sf::Keyboard::isKeyPressed(sf::Keyboard::W)) - { - cpu.bus.write_controller1(Controller::UP, 1); - } - else - { - cpu.bus.write_controller1(Controller::UP, 0); - } - - if (sf::Keyboard::isKeyPressed(sf::Keyboard::S)) - { - cpu.bus.write_controller1(Controller::DOWN, 1); - } - else - { - cpu.bus.write_controller1(Controller::DOWN, 0); - } - if (sf::Keyboard::isKeyPressed(sf::Keyboard::A)) - { - cpu.bus.write_controller1(Controller::LEFT, 1); - } - else - { - cpu.bus.write_controller1(Controller::LEFT, 0); - } - - if (sf::Keyboard::isKeyPressed(sf::Keyboard::D)) - { - cpu.bus.write_controller1(Controller::RIGHT, 1); - } - else - { - cpu.bus.write_controller1(Controller::RIGHT, 0); - } - - if (sf::Keyboard::isKeyPressed(sf::Keyboard::Space)) - { - cpu.bus.write_controller1(Controller::A, 1); - } - else - { - cpu.bus.write_controller1(Controller::A, 0); - } - if (sf::Keyboard::isKeyPressed(sf::Keyboard::LShift)) - { - cpu.bus.write_controller1(Controller::B, 1); - } - else - { - cpu.bus.write_controller1(Controller::B, 0); - } - if (sf::Keyboard::isKeyPressed(sf::Keyboard::Enter)) - { - cpu.bus.write_controller1(Controller::START, 1); - } - else - { - cpu.bus.write_controller1(Controller::START, 0); - } - if (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)) - { - cpu.bus.write_controller1(Controller::SELECT, 1); - } - else - { - cpu.bus.write_controller1(Controller::SELECT, 0); - } - - cpu.bus.write_8bit(0xfe, ((uint8_t)rand() % 16 + 1)); - - cpu.bus.tick(); - - // NMI interrupts - if (cpu.bus.NMI_interrupt()) - { - HandleNMIInterrupts(cpu); - // printf("%x \n", cpu.bus.get_PC()); - } - current_instruction = cpu.bus.fetch_next(); - // printf("%x \n", cpu.bus.get_PC()); - - if (InstructionValid(current_instruction)) - { - Instruction a = GetInstruction(current_instruction); - a.i(a.addressmode, cpu); - std::vector rgb_data_vector = cpu.bus.render_texture({NES_RES_L, NES_RES_W}); - uint8_t rgb_data[NES_RES_A * 4]; - std::copy(rgb_data_vector.begin(), rgb_data_vector.end(), rgb_data); - // nes_cpu. - // for (int i = 0; i < nes_cpu.size(); i++) - // { - // rgb_da - // } - // cpu.bus.render_texture(rgb_data); - window.clear(); // Change this to the desired color - - texture.update(rgb_data); - // // printf("pc: 0x%x current instrcution 0x%x \n", cpu.bus.get_PC(), current_instruction); - // // cpu.bus.render(texture, 0, 0); - // // cpu.bus.render() - window.draw(sprite); - window.display(); - } - else - { - printf("instruction opcode 0x%x is unrecongnized \n", current_instruction); - program_failure("Unrecognized instruction encountered", cpu, 1); - cpu.error_code = EXIT_FAILURE; - return cpu; - } - } - return cpu; -} diff --git a/src/emulator/Execute.cpp b/src/emulator/Execute.cpp new file mode 100644 index 0000000..c7b3630 --- /dev/null +++ b/src/emulator/Execute.cpp @@ -0,0 +1,122 @@ +#include +#include +#include +#include +#include +#include + +Execute::Execute(CPU cpu) +{ + this->cpu = cpu; + printf("%x \n", this->cpu.bus.get_PC()); + // std::cout << this->cpu.bus.get_PC() << std::end +} + +Execute::Execute() +{ +} +CPU Execute::run() +{ + // std::bitset<7> status(this->cpu.status.val); + // std::cout << status << std::endl; + + if (cpu.bus.NMI_interrupt() && !cpu.interrupt.has_value()) + { + cpu.bus.push_stack8(cpu.status.val); + qInfo() << "NMI interrupt"; + // printf("%x \n", cpu.bus.get_PC()); + cpu.bus.push_stack16(cpu.bus.get_PC() - 1); + // printf("the pc %x \n", cpu.bus.get_PC()); + // printf("print"); + cpu.interrupt = 1; + set_interrupt_disabled(1, cpu); + cpu.bus.fetch_next(); + // set_interrupt_disabled(1, cpu); + set_brk(cpu, 1); + cpu.bus.fill(cpu.bus.read_16bit(0xfffa)); + // cpu.bus.tick(); + // cpu.bus.tick(); + } + if (cpu.bus.check_error().has_value()) + { + qCritical() << "ERROR WITH THE CPU" << cpu.bus.check_error().value(); + cpu.error_code = EXIT_FAILURE; + + return cpu; + // qCritical() << "instruction" << num_to_hexa(current_instr) << "is invalid"; + } + auto current_instr = cpu.bus.fetch_next(); + // printf("0x%x \n", current_instr); + if (InstructionValid(current_instr)) + { + Instruction a = GetInstruction(current_instr); + a.i(a.addressmode, cpu); + cpu.error_code = EXIT_SUCCESS; + return cpu; + } + qCritical() << "instruction" << num_to_hexa(current_instr) << "is invalid"; + + qInfo() << "potential error with the cpu"; + + cpu.error_code = EXIT_FAILURE; + return cpu; +} +std::vector Execute::render() +{ + return cpu.bus.render_texture({NES_RES_L, NES_RES_W}); +} + +int Execute::reset_clock() +{ + return cpu.bus.reset_clock(); +} +void Execute::log_Cpu() +{ + this->cpu.bus.log_ppu(); + qInfo() << "=====CPU on quit======"; + qInfo() << "A register on exit: " << this->cpu.A_Reg; + qInfo() << "X register on exit: " << this->cpu.X_Reg; + qInfo() << "Y register on exit: " << this->cpu.Y_Reg; + qInfo() << "PC on exit: 0x" << num_to_hexa(this->cpu.bus.get_PC()); + qInfo() << "Stack pointer " << num_to_hexa(this->cpu.bus.get_stack_pointer()); + std::bitset<7> + status(this->cpu.status.val); + + qInfo() << "====STATUS REGTISTER BITS===="; + qInfo() << "Carry: " << this->cpu.status.C; + qInfo() << "Zero: " << this->cpu.status.Z; + qInfo() << "Break: " << this->cpu.status.B; + qInfo() << "Interrupt disabled: " << this->cpu.status.I; + qInfo() << "decimal mode: " << this->cpu.status.D; + + qInfo() << "Overflow: " << this->cpu.status.V; + qInfo() << "Negative: " << this->cpu.status.N; + qInfo() << "value: " << status.to_string(); + qInfo() << "==============="; + qInfo() << "clock cycles: " << this->cpu.bus.clock_cycles; + qInfo() << ""; +} + +void Execute::reset() +{ + // Bus bus = Bus(this->rom, NES_START); + cpu.bus.fill(cpu.bus.read_16bit(0xfffc)); + // printf("0x%x\n", bus.get_PC()); + + // cpu.bus = bus; + cpu.A_Reg = 0; + cpu.status.val = 0; + cpu.X_Reg = 0; + cpu.Y_Reg = 0; + cpu.bus.clock_cycles = 0; + cpu.interrupt = {}; +} +void Execute::joypad2(Controller button, int isPressed) +{ + + cpu.bus.write_controller1(button, isPressed); +} +void Execute::joypad1(Controller button, int isPressed) +{ + cpu.bus.write_controller1(button, isPressed); +} diff --git a/src/emulator/InstructionMap.cpp b/src/emulator/Instruction_Map.cpp similarity index 100% rename from src/emulator/InstructionMap.cpp rename to src/emulator/Instruction_Map.cpp diff --git a/src/emulator/Instructions.cpp b/src/emulator/Instructions.cpp index b82668d..5a80e29 100644 --- a/src/emulator/Instructions.cpp +++ b/src/emulator/Instructions.cpp @@ -2,7 +2,7 @@ #include // #include "Memory.h" -#include +#include #include #include #include @@ -133,14 +133,17 @@ void PLA(AddressMode addressType, CPU &cpu) void STA(AddressMode addressType, CPU &cpu) { // TODO store accumulator in mem - uint16_t v = address_mode(addressType, cpu); + // printf("%x \n", v); + // printf("%x \n", cpu.bus.get_PC()); cpu.bus.write_8bit(v, cpu.A_Reg); } void STX(AddressMode addressType, CPU &cpu) { uint16_t v = address_mode(addressType, cpu); + // printf("%x \n", v); + // printf("%x \n", cpu.bus.get_PC()); cpu.bus.write_8bit(v, cpu.X_Reg); } @@ -250,6 +253,7 @@ void BIT(AddressMode addressType, CPU &cpu) { // TODO: bit test uint8_t value = get_value(addressType, cpu); + uint8_t result = cpu.A_Reg & value; set_zero(result, cpu); set_overflow((value & 0b00100000) != 0, cpu); @@ -476,10 +480,12 @@ void CLD(AddressMode addressType, CPU &cpu) void RTI(AddressMode addressType, CPU &cpu) { - // TODO:return from interrupt cpu.bus.fill(cpu.bus.pop_stack16()); cpu.status.val = cpu.bus.pop_stack8(); set_brk(cpu, 0); + set_interrupt_disabled(0, cpu); + + cpu.interrupt = {}; } #pragma endregion setFlags @@ -551,7 +557,7 @@ void BPL(AddressMode addressType, CPU &cpu) { return; } - + // printf("bpl \n"); cpu.bus.fill((uint16_t)((cpu.bus.get_PC() - 1) + new_PC)); } diff --git a/src/emulator/LoadRom.cpp b/src/emulator/Load_Rom.cpp similarity index 82% rename from src/emulator/LoadRom.cpp rename to src/emulator/Load_Rom.cpp index 810f675..05bde0e 100644 --- a/src/emulator/LoadRom.cpp +++ b/src/emulator/Load_Rom.cpp @@ -4,6 +4,8 @@ #include #include #include +#include +#include #define PRG_ROM_SIZE 16384 #define CHR_ROM_SIZE 8192 @@ -15,6 +17,12 @@ enum MirrorType FOUR_SCREEN, }; +enum ColorEncoding +{ + Pal, + Ntsc +}; + // easier to understand over doing bytes and bit operations typedef uint8_t byte_t; struct NESHeader @@ -44,6 +52,18 @@ struct NESHeader }; byte_t val; } flag7; + + byte_t flag8; + + union + { + struct + { + unsigned color_encoding : 1; // NTSC vs pal + unsigned padding : 7; + }; + byte_t val; + } flag9; }; struct Rom { @@ -51,28 +71,32 @@ struct Rom std::vector CHR; uint8_t mapper; MirrorType mirror; + ColorEncoding color_encoding; }; std::vector file_tobyte_vector(std::string file_name) { std::vector instructions; std::ifstream infile(file_name, std::ios::binary); // Open the file for reading std::string line; - if (!infile) - { - std::cerr << "Error opening file." << std::endl; - exit(EXIT_FAILURE); - } while (infile) { uint8_t a = (uint8_t)infile.get(); instructions.push_back(a); } + return instructions; } -Rom load_rom(std::vector instructions) +std::optional load_rom(std::vector instructions) { - + if (instructions.size() == 1) + { + return {}; + } + const int NES_ROM_SIZE = 16401; + std::cout << instructions.size() << std::endl; + if (instructions.size() < NES_ROM_SIZE) + return {}; Rom rom; NESHeader nes_header; memcpy(&nes_header, instructions.data(), sizeof(NESHeader)); @@ -85,8 +109,7 @@ Rom load_rom(std::vector instructions) && nes_header.ident[3] != 0x1a) // all man style should be the default in the VS code formatiro || nes_header.flag7.inesverif == 0xc) { - std::cout << "not NES Rom or NES 1.0 format" << std::endl; - exit(EXIT_FAILURE); + return {}; } // uint8_t map = (instructions[7] & 0b11110000) | (instructions[6] >> 4); rom.mapper = nes_header.flag7.mapper_upper | nes_header.flag6.mapper_lower; @@ -113,5 +136,6 @@ Rom load_rom(std::vector instructions) { rom.CHR.push_back(instructions[i]); } + rom.color_encoding = (nes_header.flag9.color_encoding == 1) ? ColorEncoding::Pal : ColorEncoding::Ntsc; return rom; } \ No newline at end of file diff --git a/src/emulator/PPU.cpp b/src/emulator/PPU.cpp index 374723f..0f22b74 100644 --- a/src/emulator/PPU.cpp +++ b/src/emulator/PPU.cpp @@ -2,10 +2,12 @@ #include #include #include -#include +#include +#include #include -// #include "PPU.h" -// #include +#include + PPU::PPU(std::vector chrrom, MirrorType mirrorType) { @@ -14,21 +16,36 @@ PPU::PPU(std::vector chrrom, MirrorType mirrorType) this->internalDataBuffer = 0; this->reg.ppuAddr.val = 0; this->reg.ppuCtrl.val = 0; + this->reg.ppuStatus.val = 0; + for (int i = 0; i < 2048; i++) this->memory[i] = 0; this->reg.high_ptr = true; this->reg.scrollLatch = false; this->reg.ppumask.val = 0; + this->scanline = 0; this->cycles = 0; + this->err_string = std::nullopt; + this->start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < 255; i++) this->oam[i] = 0; + for (int i = 0; i < 32; i++) + { + this->pallete[i] = 0; + } + this->oam_addr = 0; } PPU::PPU() {} -std::tuple PPU::getColorFromByte(uint16_t byte) + +std::tuple PPU::getColorFromByte(uint16_t byte, std::tuple pallete) { #pragma region SYS_PAL std::tuple system_palette[64] = { - {0x80, 0x80, 0x80}, {0x00, 0x3D, 0xA6}, {0x00, 0x12, 0xB0}, {0x44, 0x00, 0x96}, // + {0x80, 0x80, 0x80}, + {0x00, 0x3D, 0xA6}, + {0x00, 0x12, 0xB0}, + {0x44, 0x00, 0x96}, // {0xA1, 0x00, 0x5E}, {0xC7, 0x00, 0x28}, {0xBA, 0x06, 0x00}, @@ -91,8 +108,75 @@ std::tuple PPU::getColorFromByte(uint16_t byte) {0x11, 0x11, 0x11} // }; #pragma endregion + if (byte == 0) + { + return system_palette[this->pallete[0]]; + } + else if (byte == 1) + { + return system_palette[std::get<1>(pallete)]; + } + else if (byte == 2) + { + return system_palette[std::get<2>(pallete)]; + } + else if (byte == 3) + { + return system_palette[std::get<3>(pallete)]; + } + std::cout << "non" << std::endl; return system_palette[byte]; } + +void PPU::get_chr_tile(uint16_t tile_idx, int banks, std::vector &tile_list) +{ + + // get chr tile + for (int i = banks + tile_idx * 16; i <= ((banks + tile_idx * 16) + 15); i++) + { + + tile_list.push_back(chr_rom[i]); + } +} +std::tuple PPU::bg_pallete(size_t row, size_t column) +{ + size_t attr_table = row / 4 * 8 + column / 4; + uint8_t attr_byte = memory[0x3c0 + attr_table]; + size_t column_b = column % 4 / 2; + size_t row_b = row % 4 / 2; + uint8_t idx = 0; + if (row_b == 0 && column_b == 0) + { + idx = attr_byte & 0b11; + } + else if (row_b == 1 && column_b == 0) + { + idx = (attr_byte >> 2) & 0b11; + } + else if (row_b == 0 && column_b == 1) + { + idx = (attr_byte >> 4) & 0b11; + } + else + { + idx = (attr_byte >> 6) & 0b11; + } + size_t pallete_offset = 1 + idx * 4; + // printf(" offset 1: %d \n", pallete_offset); + // for (int i = 0; i < 32; i++) + // { + // printf("%x \n", this->pallete[i]); + // } + + return { + 0, + this->pallete[pallete_offset], + this->pallete[pallete_offset + 1], + this->pallete[pallete_offset + 2], + + }; +} + uint16_t PPU::mirror(uint16_t address) { uint16_t mirrored_vram = address & 0b10111111111111; @@ -110,6 +194,8 @@ uint16_t PPU::mirror(uint16_t address) uint8_t PPU::read_PPU_data() { uint16_t addr = this->reg.ppuAddr.val; + printf("pallete \n"); + this->reg.ppuAddr.val += reg.ppuCtrl.I ? 32 : 1; if (addr <= 0x1fff) { @@ -125,6 +211,7 @@ uint8_t PPU::read_PPU_data() } else if (addr == 0x3f10 || addr == 0x3f14 || addr == 0x3f18 || addr == 0x3f1c) { + addr = addr - 0x10; return pallete[addr - 0x3f00]; } @@ -132,8 +219,8 @@ uint8_t PPU::read_PPU_data() } uint8_t PPU::read_status() { - uint8_t ret = reg.ppuStatus.val; - reg.ppuStatus.V = 0; + uint8_t ret = this->reg.ppuStatus.val; + this->reg.ppuStatus.V = 0; reg.high_ptr = true; reg.scrollLatch = false; return ret; @@ -157,18 +244,72 @@ void PPU::print_ppu_stats() printf("\n============================= \n"); // printf("\n"); } +void PPU::log_ppu() +{ + std::bitset<8> ppu_status(this->reg.ppuStatus.val); + std::bitset<8> ppu_ctrl(this->reg.ppuCtrl.val); + std::bitset<8> ppu_mask(this->reg.ppumask.val); + + qInfo() << "===== PPU ON EXIT ==========="; + qInfo() << ""; + + qInfo() << "===== OAM ===="; + qInfo() << "oam addr: " << num_to_hexa(this->oam_addr); + qInfo() << "=====PPU ADDR====="; + qInfo() << "PPU addr: " << num_to_hexa(this->reg.ppuAddr.val); + qInfo() << "lo: " << num_to_hexa(this->reg.ppuAddr.lo); + qInfo() << "hi: " << num_to_hexa(this->reg.ppuAddr.hi); + qInfo() << ""; + + qInfo() << "===== PPU status ===="; + qInfo() << "VBlank: " << this->reg.ppuStatus.V; + qInfo() << "0_hit: " << this->reg.ppuStatus.S; + qInfo() << "overflow: " << this->reg.ppuStatus.O; + qInfo() << "status: " << ppu_status.to_string(); + qInfo() << ""; + + qInfo() << "===== PPU ctrl ===="; + qInfo() << "NMI enable (0: off, 1: on): " << this->reg.ppuCtrl.V; + qInfo() << "PPU master/slave select (0: read backdrop from EXT pins; 1: output color on EXT pins): " << this->reg.ppuCtrl.P; + qInfo() << "sprite size (0: 8x8, 1: 8x16): " << this->reg.ppuCtrl.H; + qInfo() << "Background patterntable (0: $0000; 1: $1000): " << this->reg.ppuCtrl.B; + qInfo() << "Sprite patterntable (0: $0000; 1: $1000): " << this->reg.ppuCtrl.S; + qInfo() << "increment (0: add 1 going across, 1: add 32 going down): " << this->reg.ppuCtrl.I; + std::bitset<2> name_table_address(this->reg.ppuCtrl.N); + qInfo() << "name table addreess: " << name_table_address.to_string(); + qInfo() << "ctrl: " << ppu_ctrl.to_string(); + qInfo() << ""; + qInfo() << "===== PPU mask ===="; + qInfo() << "Emphasize blue: " << this->reg.ppumask.B; + qInfo() << "Emphasize green: " << this->reg.ppumask.G; + qInfo() << "Emphasize red: " << this->reg.ppumask.R; + + qInfo() << "Enable sprite rendering: " << this->reg.ppumask.s; + qInfo() << "Enable background rendering: " << this->reg.ppumask.b; + qInfo() << "Show sprites in leftmost 8 pixels of screen: " << this->reg.ppumask.M; + qInfo() << "Show background in leftmost 8 pixels of screen " << this->reg.ppumask.m; + qInfo() << "grey scale (0: normal color, 1: grey scale): " << this->reg.ppumask.g; + qInfo() << "ppu mask: " << ppu_mask.to_string(); + qInfo() << ""; +} void PPU::write_PPU_address(uint8_t val) { + if (this->reg.high_ptr) { - this->reg.ppuAddr.lo = val; + this->reg.ppuAddr.hi = val; } else { - this->reg.ppuAddr.hi = val; + this->reg.ppuAddr.lo = val; } // TODO: fix later // std::cout << "ppu addr" << std::endl; + qInfo() << "addr"; + qInfo() << "val: " << num_to_hexa(this->reg.ppuAddr.val); + qInfo() << "hi: " << num_to_hexa(this->reg.ppuAddr.hi); + qInfo() << "lo: " << num_to_hexa(this->reg.ppuAddr.lo); + // printf("val:%x \n", this->reg.ppuAddr.val); // printf("hi: %x \n", this->reg.ppuAddr.hi); // printf("lo: %x \n", this->reg.ppuAddr.lo); @@ -177,19 +318,42 @@ void PPU::write_PPU_address(uint8_t val) } void PPU::write_PPU_ctrl(uint8_t val) { + auto before = this->reg.ppuCtrl; + this->reg.ppuCtrl.val = val; + if (before.V == 0 && this->reg.ppuCtrl.V == 1 && this->reg.ppuStatus.V == 1) + { + this->reg.ppuCtrl.V = 1; + } } void PPU::write_PPU_mask(uint8_t val) { + + std::bitset<7> ppu_status2(val); + std::cout << "mask: " << ppu_status2 << std::endl; + std::cout << "ppu mask being written to" << std::endl; this->reg.ppumask.val = val; + std::bitset<7> ppu_status(this->reg.ppumask.val); + std::cout << "mask: " << ppu_status << std::endl; + // exit(EXIT_FAILURE); } std::optional PPU::write_PPU_data(uint8_t val) { + uint16_t addr = this->reg.ppuAddr.val; + if (addr == 0) + return 1; + // printf("%x \n", addr); + // std::cout << addr << std::endl; if (addr >= 0x2000 && addr <= 0x2fff) { // uint8_t res = internalDataBuffer; + // if (this->reg.ppumask.b == 0) + // { + // // printf("is 0 \n"); + // return 1; + // } this->memory[mirror(addr)] = val; // std::cout << "saving to vram" << std::endl; // internalDataBuffer = memory[mirror(addr)]; @@ -197,27 +361,43 @@ std::optional PPU::write_PPU_data(uint8_t val) else if (addr == 0x3f10 || addr == 0x3f14 || addr == 0x3f18 || addr == 0x3f1c) { addr = addr - 0x10; - pallete[addr - 0x3f00] = val; + addr &= 0x1F; + + qInfo() << "pallete" << num_to_hexa(addr) << " val: " << num_to_hexa(val); + + this->pallete[addr] = val; + qInfo() << "pallete written to: " << this->pallete[addr]; } else if (addr >= 0x3f00 && addr <= 0x3fff) { - pallete[addr - 0x3f00] = val; + addr &= 0x1F; + this->pallete[addr] = val; + qInfo() << "pallete" << num_to_hexa(addr) << " val: " << num_to_hexa(val); } else if (addr == 0x4014) { - printf("%x\n", addr); + this->write_OAM_data(val); + + // printf("%x\n", addr); } else { // TODO: fails for some reason - std::cout << "\033[91mAttempt to write into PPU READ_ONLY_MEM\033[0m" << std::endl; - printf("0x%x\n", addr); + // std::cout << "\033[91mAttempt to write into PPU READ_ONLY_MEM\033[0m" << std::endl; + // printf("0x%x\n", addr); // exit(EXIT_FAILURE); - return {}; + if (this->reg.ppumask.s != 0) + { + this->err_string = std::optional{"Address 0x" + num_to_hexa(reg.ppuAddr.val) + " is a PPU read only address"}; + return {}; + } + return 1; } this->reg.ppuAddr.val += reg.ppuCtrl.I ? 32 : 1; if (reg.ppuAddr.val > 0x3fff) { + qInfo() << "addr" << num_to_hexa(this->reg.ppuAddr.val) << " val: " << val; + this->reg.ppuAddr.val &= 0b11111111111111; } return 1; @@ -226,15 +406,36 @@ std::optional PPU::write_PPU_data(uint8_t val) bool PPU::tick(uint8_t clock_cycles) { this->cycles += clock_cycles; + // qInfo() << "ppu cycles: " << this->cycles; + if (this->cycles >= 341) { this->scanline += 1; this->cycles -= 341; + // qInfo() << "scanline: " << this->scanline; + // frame change + + if (scanline == 241) + { + reg.ppuStatus.V = 1; + qInfo() << "vblank"; + if (this->reg.ppuCtrl.V == 1) + { + // printf("NMI?"); + + // std::bitset<8> ppu_status(this->reg.ppuCtrl.val); + // std::cout << "after NMI ctrl: " << ppu_status << std::endl; + return true; + } + } + + // ppu reser. this is when it finishes clearing the screen with pizles if (scanline >= 262) { - scanline = 0; - reg.ppuStatus.V = 0; + this->scanline = 0; + this->reg.ppuStatus.V = 0; + qInfo() << "reset"; return true; } } @@ -243,92 +444,50 @@ bool PPU::tick(uint8_t clock_cycles) bool PPU::NMI_interrupt(uint8_t clock_cycles) { - if (this->scanline == 241) { - - if (reg.ppuCtrl.V == 0) + this->reg.ppuStatus.V = 1; + if (this->reg.ppuCtrl.V == 1) { - reg.ppuStatus.V = 1; + + // printf("%d \n",) + // std::cout << "NMI should be firing" << std::endl; + return true; } } + // std::cout << "n" return false; } -// void PPU::render(sf::Texture &texture, int bank, int tile) -// { -// uint8_t data[256 * 240 * 4]; -// bank = this->reg.ppuCtrl.B; -// // printf("ppu addr %x \n", this->reg.ppuAddr.val); -// // for -// int banks = this->reg.ppuCtrl.B ? 0x1000 : 0; - -// for (int ppu_idx = 0; ppu_idx <= 0x03c0; ppu_idx++) -// { - -// tile = this->memory[ppu_idx]; -// int idx = ppu_idx % 32; -// int idy = ppu_idx / 32; -// std::vector tile_list; - -// for (int i = banks + tile * 16; i <= ((banks + tile * 16) + 15); i++) -// { - -// tile_list.push_back(chr_rom[i]); -// } - -// for (int y = 0; y < 8; y++) -// { -// uint8_t upper = tile_list[y]; -// uint8_t lower = tile_list[y + 8]; -// for (int x = 7; x >= 0; x--) -// { -// uint16_t value = (1 & upper) << 1 | (1 & lower); -// upper >>= 1; -// lower >>= 1; -// sf::Color rgb = getColorFromByte(value); -// int tile_x = idx * 8 + x; -// int tile_y = idy * 8 + y; -// int b = (tile_y) * 4 * 240 + (tile_x) * 4; - -// data[b] = rgb.r; -// data[b + 1] = rgb.g; -// data[b + 2] = rgb.b; -// data[b + 3] = 0xff; -// } -// } -// } -// texture.update(data); -// } - /** * @brief gets u a vector of bytes that represent the Texture map warning. assumes you are RGBA so its res * 4 * * @param res * @return std::vector */ - -std::vector PPU::render_texture(std::tuple res) +void PPU::draw_background(std::vector &rgb_ds, int banks, std::tuple res) { - // int bank = this->reg.ppuCtrl.B; - int banks = this->reg.ppuCtrl.B ? 0x1000 : 0; - std::vector rgb_ds; - rgb_ds.resize(std::get<0>(res) * std::get<1>(res) * 4); for (int ppu_idx = 0; ppu_idx < 0x3c0; ppu_idx++) { - uint16_t tile = this->memory[ppu_idx]; + uint16_t tile = this->memory[(ppu_idx)]; /// name tables + int idx = ppu_idx % 32; int idy = ppu_idx / 32; + auto bgpallete = this->bg_pallete(idx, idy); std::vector tile_list; - for (int i = banks + tile * 16; i <= ((banks + tile * 16) + 15); i++) - { + this->get_chr_tile(tile, banks, tile_list); + qDebug() << tile; + // std::vector tile_list = this->get_chr_tile(tile, banks); - tile_list.push_back(chr_rom[i]); - } + // for (int i = banks + tile * 16; i <= ((banks + tile * 16) + 15); i++) + // { + + // tile_list.push_back(chr_rom[i]); + // } for (int y = 0; y < 8; y++) { uint8_t upper = tile_list[y]; @@ -339,7 +498,7 @@ std::vector PPU::render_texture(std::tuple res) upper >>= 1; lower >>= 1; - auto rgb = getColorFromByte(0x0f); + auto rgb = getColorFromByte((this->reg.ppumask.b == 1 ? value : 0), bgpallete); // if (value == 0) // continue; // sf::Color rgb = getColorFromByte(value); @@ -347,22 +506,22 @@ std::vector PPU::render_texture(std::tuple res) int tile_y = idy * 8 + y; // printf("tile_x %d tile_y: %d \n", tile_x, tile_y); - int b = (tile_y) * 4 * std::get<1>(res) + (tile_x) * 4; + int b = (tile_y) * 4 * std::get<0>(res) + (tile_x) * 4; rgb_ds[b] = std::get<0>(rgb); rgb_ds[b + 1] = std::get<1>(rgb); rgb_ds[b + 2] = std::get<2>(rgb); rgb_ds[b + 3] = 0xff; - // printf("combined b %d \n", b); - - // printf("%d \n", rgb_ds.size()); } // printf("=========\n"); } // printf("\n "); } +} +void PPU::draw_sprites(std::vector &rgb_ds, int banks, std::tuple res) +{ - for (int ppu_idx = 255; ppu_idx > 0; ppu_idx -= 4) + for (int ppu_idx = 255; ppu_idx >= 0; ppu_idx -= 4) { union Attribute_byte { @@ -381,6 +540,7 @@ std::vector PPU::render_texture(std::tuple res) int idx = this->oam[ppu_idx]; int idy = this->oam[ppu_idx - 3]; + // std::cout << "a" << std::endl; Attribute_byte attribbyte; attribbyte.val = this->oam[ppu_idx - 1]; // Attribute_byte b = this->oam[ppu_idx - 1]; @@ -394,15 +554,33 @@ std::vector PPU::render_texture(std::tuple res) // uint16_t tile = this->memory[ppu_idx]; // int idx = ppu_idx % 32; // int idy = ppu_idx / 32; - std::vector - tile_list; + banks = this->reg.ppuCtrl.B ? 0x1000 : 0; - for (int i = banks + tile * 16; i <= ((banks + tile * 16) + 15); i++) - { + std::vector tile_list; + auto pallete_idx = attribbyte.pallete; + // printf("%x \n", attribbyte.pallete); + size_t pallete_offset = 16 + (pallete_idx * 4); + // printf("offset: %d \n", pallete_offset); + qDebug() << "sprite pallete 1: " << num_to_hexa(this->pallete[pallete_offset]) << "2: " << num_to_hexa(this->pallete[pallete_offset + 1]) << "3: " << num_to_hexa(this->pallete[pallete_offset + 3]); + // for (int i = 0; i < 32; i++) + // printf("pallete: 0x%x \n", this->pallete[i]); + + // for (int i = 0; i < 255; i++) + // printf("oam: %x \n", this->oam[i]); + std::bitset<2> + u(attribbyte.pallete); + std::cout << u << std::endl; + // printf("tile: %x \n", tile); + qDebug() << "tile: " << num_to_hexa(tile); + std::tuple sprite_palletes = { + 0, + this->pallete[pallete_offset], + this->pallete[pallete_offset + 1], + this->pallete[pallete_offset + 2], - tile_list.push_back(chr_rom[i]); - } + }; + this->get_chr_tile(tile, banks, tile_list); for (int y = 0; y < 8; y++) { uint8_t upper = tile_list[y]; @@ -412,9 +590,9 @@ std::vector PPU::render_texture(std::tuple res) uint16_t value = (1 & upper) << 1 | (1 & lower); upper >>= 1; lower >>= 1; - auto rgb = getColorFromByte(value); - if (value == 0) - continue; + + auto rgb = getColorFromByte(value, sprite_palletes); + // std::cout << rgb << std::endl; int tile_x = 0; if (attribbyte.flip_x) tile_x = idx + 7 - x; @@ -441,6 +619,17 @@ std::vector PPU::render_texture(std::tuple res) } // printf("reset loop\n");s } +} +std::vector PPU::render_texture(std::tuple res) +{ + int banks = this->reg.ppuCtrl.B ? 0x1000 : 0; + std::vector rgb_ds; + rgb_ds.resize(std::get<0>(res) * std::get<1>(res) * 4); + + if (this->chr_rom.size() == 0) + return rgb_ds; + draw_background(rgb_ds, banks, res); + draw_sprites(rgb_ds, banks, res); return rgb_ds; } uint8_t PPU::read_OAM_data() @@ -448,19 +637,27 @@ uint8_t PPU::read_OAM_data() return oam[oam_addr]; } +// oam data. set at 0x2004 void PPU::write_OAM_data(uint8_t val) { - oam[oam_addr] = val; - oam_addr++; + // cant access the OAM if the ctrl S bit is not set + + this->oam[this->oam_addr] = val; + + this->oam_addr++; + if (oam_addr >= 255) + this->oam_addr = 0; + qDebug() << "oam: " << num_to_hexa(this->oam_addr); // oam_addr += (oam_addr + 1) % 256; // printf("%x \n", oam_addr); // std::cout << oam_addr << std::endl; } -void PPU::write_OAM_dma(uint8_t val[256]) +void PPU::write_OAM_dma(std::vector buffer) { // oam[oam_addr] = val; // oam_addr++; } + void PPU::write_OAM_address(uint8_t val) { // std::cout << "write to oam addr" << std::endl; diff --git a/src/emulator/StatusRegister.cpp b/src/emulator/Status_Register.cpp similarity index 98% rename from src/emulator/StatusRegister.cpp rename to src/emulator/Status_Register.cpp index 2df7a2e..916d5fa 100644 --- a/src/emulator/StatusRegister.cpp +++ b/src/emulator/Status_Register.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #define CARRY_BIT 0b0000001 #define ZERO_BIT 0b0000010 diff --git a/src/gui/qt_tetroidnes/Emulator_Worker.cpp b/src/gui/qt_tetroidnes/Emulator_Worker.cpp new file mode 100644 index 0000000..1a32dc1 --- /dev/null +++ b/src/gui/qt_tetroidnes/Emulator_Worker.cpp @@ -0,0 +1,172 @@ +#include + +#include +#include + +#include +#include +#include "Emulator_Worker.h" + +const size_t cpu_cycles_frame = 29782; +EmulatorWorker::EmulatorWorker(Rom rom, QString rom_dest, QWidget *parent) : QObject{parent}, + rom_url(rom_dest), + m_initialized(false), + m_is_running(false), + m_clock_interval(frame_interval_ns), + is_frame_generated(false) +{ + this->rom = rom; + qWarning() << "resetting"; +} +void EmulatorWorker::log_cpu() +{ + exe.log_Cpu(); +} +void EmulatorWorker::shutdown_game() +{ + + this->m_is_running = false; + // exe.log_Cpu(); +} + +void EmulatorWorker::init() +{ + + if (m_initialized) + { + qWarning() << "Initialization function was already called, returning..."; + return; + } + + if (!QUrl(rom_url).isValid()) + { + emit push_error(QString("Could not start game, ROM URL is invalid: %1").arg(rom_url), EXIT_FAILURE); + return; + } + + // Constructor Code + frame_timer = new QChronoTimer(this); + + SettingsManager &settings = SettingsManager::instance(); + + set_clock_interval_speed(settings.speed()); + + frame_timer->setTimerType(Qt::PreciseTimer); + frame_timer->setInterval(std::chrono::nanoseconds(m_clock_interval)); + + qDebug() << "frame timer cycle" << frame_timer->interval().count() << "Nanoseconds"; + qDebug() << "Game Path:" << rom_url; + + // Events + connect(frame_timer, &QChronoTimer::timeout, this, &EmulatorWorker::on_frame_timer_timeout); + + // Setup CPU + initializeInstructionMap(); + // auto rom = load_rom(file_tobyte_vector(rom_url.toStdString())); + // if (rom.has_value() == 0) + // { + // emit push_error("Unrecongnized file format, needs to be NES v1.0 format.", EXIT_FAILURE); + // return; + // } + + Bus bus = Bus(this->rom, NES_START); + CPU cpu = CPU(); + cpu.bus = bus; + this->exe = Execute(cpu); + this->exe.reset(); + // bus.fill(bus.read_16bit(0xfffc)); + // // printf("0x%x\n", bus.get_PC()); + + // cpu.bus = bus; + // cpu.A_Reg = 0; + // cpu.status.val = 0; + // cpu.X_Reg = 0; + // cpu.Y_Reg = 0; + // cpu.bus.clock_cycles = 0; + // cpu.interrupt = {}; + is_frame_generated = false; + qInfo() << "pc: " << num_to_hexa(cpu.bus.get_PC()); + + m_initialized = false; +} + +void EmulatorWorker::on_frame_timer_timeout() +{ + qDebug() << "Frame timeout, is frame generated?" << is_frame_generated; + if (is_frame_generated) + { + render_frame(); + is_frame_generated = false; + process_cpu(); + } +} + +void EmulatorWorker::on_pause_toggle(bool paused) +{ + + if (paused) + { + frame_timer->stop(); + } + else + { + frame_timer->start(); + } +} + +void EmulatorWorker::on_start_main_thread() +{ + init(); + m_is_running = true; + + frame_timer->start(); + process_cpu(); // Kickstarting the CPU so is_frame_generated can become true +} + +void EmulatorWorker::render_frame() +{ + // qDebug() << "Emitting draw_frame signal"; + std::vector render = exe.render(); + emit draw_frame(render); +} + +bool EmulatorWorker::is_running() const +{ + return m_is_running; +} + +void EmulatorWorker::process_cpu() +{ + // Process CPU + CPU result; + int clock_cycles = 0; + qInfo() << "processing cpu"; + + while (clock_cycles < cpu_cycles_frame) + { + result = exe.run(); + if (result.error_code == EXIT_FAILURE) + { + qCritical() << "potential error with the cpu at pc=0x" << num_to_hexa(result.bus.get_PC()); + auto error_cpu = result.bus.check_error().value_or("error with emulator, please check the ROM for faulty instructions"); + auto err_mess = QString("%1-- at PC= 0x%2").arg(QString::fromStdString(error_cpu), QString::fromStdString(num_to_hexa(result.bus.get_PC()))); + emit push_error(err_mess, EXIT_FAILURE); + is_frame_generated = false; + return; + } + clock_cycles += exe.reset_clock(); + } + is_frame_generated = true; +} + +int EmulatorWorker::clock_interval() const +{ + return m_clock_interval; +} + +void EmulatorWorker::set_clock_interval_speed(const float speed) +{ + const float new_interval = static_cast(frame_interval_ns * speed); + qInfo() << "Setting new clock interval\nOld interval:" << m_clock_interval << "New interval:" << new_interval << "Speed multiplier:" << speed; + m_clock_interval = new_interval; +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Filter_Control_Frame.cpp b/src/gui/qt_tetroidnes/Filter_Control_Frame.cpp new file mode 100644 index 0000000..b1f5e2b --- /dev/null +++ b/src/gui/qt_tetroidnes/Filter_Control_Frame.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#include + +#include +#include +#include + +FilterControlFrame::FilterControlFrame(QWidget *parent) : QFrame{parent} +{ + auto &settings = SettingsManager::instance(); + const auto sort_mode = settings.sort_mode(); + const auto sort_order = settings.ascending_order(); + const auto rom_dirs = settings.get_rom_dirs(); + + QHBoxLayout *sort_control_frame_layout = new QHBoxLayout(); + QVBoxLayout *sort_buttons_frame_layout = new QVBoxLayout(); + QHBoxLayout *groupbox_layout = new QHBoxLayout(); + + search_bar = new QLineEdit(this); + search_bar_completer = new QCompleter(search_bar); + sort_buttons_frame = new QFrame(this); + sort_ascending_button = new QPushButton(tr("Ascending"), sort_buttons_frame); + sort_mode_groupbox = new QGroupBox(sort_buttons_frame); + sort_mode_az = new QPushButton(tr("A-Z"), sort_mode_groupbox); + sort_mode_year = new QPushButton(tr("Year"), sort_mode_groupbox); + sort_mode_favorites = new QPushButton(tr("Favorites"), sort_mode_groupbox); + sort_mode_button_group = new QButtonGroup(sort_mode_groupbox); + + // search bar completer + search_bar_completer->setModel(new QStringListModel()); + search_bar_completer->setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + search_bar_completer->setMaxVisibleItems(10); + update_completer_model(rom_dirs); + + search_bar->setCompleter(search_bar_completer); + + // setup sort buttons frame layout + sort_buttons_frame_layout->addWidget(sort_ascending_button); + sort_buttons_frame_layout->addWidget(sort_mode_groupbox); + sort_buttons_frame->setLayout(sort_buttons_frame_layout); + + // setup group box layout + groupbox_layout->addWidget(sort_mode_year); + groupbox_layout->addWidget(sort_mode_favorites); + groupbox_layout->addWidget(sort_mode_az); + sort_mode_groupbox->setLayout(groupbox_layout); + + // setup this layout + sort_control_frame_layout->addWidget(search_bar); + sort_control_frame_layout->addWidget(sort_buttons_frame); + setLayout(sort_control_frame_layout); + + // setup buttons + sort_ascending_button->setCheckable(true); + sort_ascending_button->setChecked(!sort_order); // Ascending == 0, Descending == 1 + sort_ascending_button->setObjectName("SortOrder"); + sort_mode_az->setCheckable(true); + sort_mode_favorites->setCheckable(true); + sort_mode_year->setCheckable(true); + + // setup button group + sort_mode_button_group->setExclusive(true); + sort_mode_button_group->addButton(sort_mode_year, RomList::Year); + sort_mode_button_group->addButton(sort_mode_favorites, RomList::Favorites); + sort_mode_button_group->addButton(sort_mode_az, RomList::AZ); + + switch (sort_mode) + { + case RomList::Year: + sort_mode_year->setChecked(true); + break; + case RomList::Favorites: + sort_mode_favorites->setChecked(true); + break; + case RomList::AZ: + sort_mode_az->setChecked(true); + break; + default: + qWarning() << "Sort mode enum given was invalid! Int given:" << sort_mode; + sort_mode_az->setChecked(true); + break; + } + + // setup search bar + search_bar->setPlaceholderText(tr("Search...")); + + connect(&settings, &SettingsManager::rom_dirs_changed, this, &FilterControlFrame::update_completer_model); +} + +void FilterControlFrame::update_completer_model(QStringList dirs) +{ + const QRegularExpression qregex(R"(\.nes$)"); + auto *completer_model = qobject_cast(search_bar_completer->model()); + QStringList updated_dirs; + + for (auto &path : dirs) + { + QDir dir(path); + QStringList files = dir.entryList(QDir::Files | QDir::NoSymLinks).filter(qregex); + + updated_dirs.append(files); + } + + // Remove extension + updated_dirs.replaceInStrings(qregex, ""); + + completer_model->setStringList(updated_dirs); +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Flow_Layout.cpp b/src/gui/qt_tetroidnes/Flow_Layout.cpp new file mode 100644 index 0000000..bb3539a --- /dev/null +++ b/src/gui/qt_tetroidnes/Flow_Layout.cpp @@ -0,0 +1,171 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include + +#include "Qt/flowlayout.h" +//! [1] +FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) + : QLayout(parent), m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} + +FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) + : m_hSpace(hSpacing), m_vSpace(vSpacing) +{ + setContentsMargins(margin, margin, margin, margin); +} +//! [1] + +//! [2] +FlowLayout::~FlowLayout() +{ +} +//! [2] + +//! [3] +void FlowLayout::addItem(QLayoutItem *item) +{ + itemList.append(item); +} +//! [3] + +//! [4] +int FlowLayout::horizontal_spacing() const +{ + if (m_hSpace >= 0) { + return m_hSpace; + } else { + return smart_spacing(QStyle::PM_LayoutHorizontalSpacing); + } +} + +int FlowLayout::vertical_spacing() const +{ + if (m_vSpace >= 0) { + return m_vSpace; + } else { + return smart_spacing(QStyle::PM_LayoutVerticalSpacing); + } +} +//! [4] + +//! [5] +int FlowLayout::count() const +{ + return itemList.size(); +} + +QLayoutItem *FlowLayout::itemAt(int index) const +{ + return itemList.value(index); +} + +QLayoutItem *FlowLayout::takeAt(int index) +{ + if (index >= 0 && index < itemList.size()) + return itemList.takeAt(index); + return nullptr; +} +//! [5] + +//! [6] +Qt::Orientations FlowLayout::expandingDirections() const +{ + return { }; +} +//! [6] + +//! [7] +bool FlowLayout::hasHeightForWidth() const +{ + return true; +} + +int FlowLayout::heightForWidth(int width) const +{ + int height = do_layout(QRect(0, 0, width, 0), true); + return height; +} +//! [7] + +//! [8] +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + do_layout(rect, false); +} + +QSize FlowLayout::sizeHint() const +{ + return minimumSize(); +} + +QSize FlowLayout::minimumSize() const +{ + QSize size; + for (const QLayoutItem *item : std::as_const(itemList)) + size = size.expandedTo(item->minimumSize()); + + const QMargins margins = contentsMargins(); + size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom()); + return size; +} +//! [8] + +//! [9] +int FlowLayout::do_layout(const QRect &rect, bool testOnly) const +{ + int left, top, right, bottom; + getContentsMargins(&left, &top, &right, &bottom); + QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom); + int x = effectiveRect.x(); + int y = effectiveRect.y(); + int lineHeight = 0; + //! [9] + + //! [10] + for (QLayoutItem *item : std::as_const(itemList)) { + const QWidget *wid = item->widget(); + int spaceX = horizontal_spacing(); + if (spaceX == -1) + spaceX = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal); + int spaceY = vertical_spacing(); + if (spaceY == -1) + spaceY = wid->style()->layoutSpacing( + QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical); + //! [10] + //! [11] + int nextX = x + item->sizeHint().width() + spaceX; + if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) { + x = effectiveRect.x(); + y = y + lineHeight + spaceY; + nextX = x + item->sizeHint().width() + spaceX; + lineHeight = 0; + } + + if (!testOnly) + item->setGeometry(QRect(QPoint(x, y), item->sizeHint())); + + x = nextX; + lineHeight = qMax(lineHeight, item->sizeHint().height()); + } + return y + lineHeight - rect.y() + bottom; +} +//! [11] +//! [12] +int FlowLayout::smart_spacing(QStyle::PixelMetric pm) const +{ + QObject *parent = this->parent(); + if (!parent) { + return -1; + } else if (parent->isWidgetType()) { + QWidget *pw = static_cast(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } else { + return static_cast(parent)->spacing(); + } +} +//! [12] diff --git a/src/gui/qt_tetroidnes/Game_Display.cpp b/src/gui/qt_tetroidnes/Game_Display.cpp new file mode 100644 index 0000000..2759f08 --- /dev/null +++ b/src/gui/qt_tetroidnes/Game_Display.cpp @@ -0,0 +1,304 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +constexpr const uint32_t rgb_data_size = NES_RES_A * 4; + +GameDisplay::GameDisplay(Rom rom, QWidget *parent, QString rom_url) : QWidget{parent}, + render_window(new sf::RenderWindow(sf::VideoMode(800, 600), "OpenGL", sf::Style::Default)), + frames_per_sec_timer(new QTimer(this)), + time_between_draw_timer(new QTimer(this)), + m_paused(false), + sprite(new sf::Sprite()), + emu_thread(new QThread(this)), + emu_worker(new EmulatorWorker(rom, rom_url)), + crt_shader(new sf::Shader()), + err_code(0), + frame_count(0), + time_between_draw_ms(0) +{ + setAttribute(Qt::WA_PaintOnScreen); + setAttribute(Qt::WA_OpaquePaintEvent); + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_DeleteOnClose); + + setWindowFlags(Qt::Window); + resize(800, 600); // TODO: MAKE THIS MORE FLEXIBLE IN SETTINGS + + game_title = QString::fromStdString( + std::filesystem::path(rom_url.toStdString()) + .filename() + .replace_extension() + .string()); + + setWindowTitle(QString("%1 - %2").arg( + qApp->applicationName(), + game_title)); + + setFocusPolicy(Qt::StrongFocus); + + SettingsManager &settings = SettingsManager::instance(); + m_is_emu_on_dif_thread = settings.run_emulator_on_seperate_thread(); + + frames_per_sec_timer->setInterval(1000); + frames_per_sec_timer->setTimerType(Qt::PreciseTimer); + + time_between_draw_timer->setInterval(1); + + if (m_is_emu_on_dif_thread) + { + emu_worker->moveToThread(emu_thread); + } + else + { + emu_worker->setParent(this); // Auto-sets a QObject's thread to the parent's + } + + // Events + connect(&settings, &SettingsManager::crt_shader_changed, this, &GameDisplay::on_crt_shader_changed); + connect(emu_worker, &EmulatorWorker::draw_frame, this, &GameDisplay::on_update); + connect(emu_worker, &EmulatorWorker::push_error, this, &GameDisplay::on_push_error); + connect(&settings, &SettingsManager::speed_changed, emu_worker, &EmulatorWorker::set_clock_interval_speed); + connect(this, &GameDisplay::pause_toggle, emu_worker, &EmulatorWorker::on_pause_toggle); + connect(emu_thread, &QThread::started, emu_worker, &EmulatorWorker::on_start_main_thread); + connect(emu_thread, &QThread::finished, emu_worker, &EmulatorWorker::deleteLater); + connect(frames_per_sec_timer, &QTimer::timeout, this, &GameDisplay::on_framerate_timer_timeout); + connect(time_between_draw_timer, &QTimer::timeout, this, [this](){ time_between_draw_ms++; }); +} + +void GameDisplay::on_crt_shader_changed(const bool b) +{ + if (b) + { + draw_func = [this](sf::Drawable &drawable) + { + crt_shader->setUniform("time", (float)(time_between_draw_ms) * 0.001f); + + render_window->draw(drawable, crt_shader.get()); + }; + } + else + { + draw_func = [this](sf::Drawable &drawable) + { render_window->draw(drawable); }; + } +} + +bool GameDisplay::is_paused() const +{ + return m_paused; +} + +void GameDisplay::set_paused(const bool b) +{ + if (b == m_paused) {return;} + + m_paused = b; + + qInfo() << "Pause toggle triggered! Is paused?" << m_paused; + emit pause_toggle(m_paused); +} + +void GameDisplay::on_push_error(QString msg, int error_code) +{ + qInfo() << "error"; + set_paused(true); + QMessageBox::critical( + this, + "TetroidNES - " + tr("Error"), + msg); + + this->err_code = error_code; + + qCritical() << msg; + + close(); +} + +void GameDisplay::on_init() +{ + qInfo() << "Initializing game window..."; + + if (!texture.create(NES_RES_L, NES_RES_W)) + { + on_push_error("Texture failed to be created!", EXIT_FAILURE); + return; + } + + sprite->setOrigin(sprite->getTextureRect().width / 2, sprite->getTextureRect().height / 2); + sprite->setTexture(texture); + update_game_scale(); + + qInfo() << "About to start thread..."; + if (m_is_emu_on_dif_thread) + { + emu_thread->start(); + } + else + { + emu_worker->on_start_main_thread(); + } + + frames_per_sec_timer->start(); + time_between_draw_timer->start(); +} + +void GameDisplay::on_update(std::vector rgb_data_vector) +{ + uint8_t rgb_data[rgb_data_size]; + std::copy(rgb_data_vector.begin(), rgb_data_vector.end(), rgb_data); + + // Display next frame + render_window->clear(); + texture.update(rgb_data); + + draw_func(*sprite.get()); + render_window->display(); + + frame_count ++; + qDebug() << "Milliseconds from previous draw call:" << time_between_draw_ms; + time_between_draw_ms = 0; +} + +void GameDisplay::on_framerate_timer_timeout() +{ + setWindowTitle( + QString("%1 Speed: %%2 | FPS: %3").arg(game_title, QString::number(speed_percent(frame_count, ntsc_frame_rate)), QString::number(frame_count))); + frame_count = 0; +} + +void GameDisplay::showEvent(QShowEvent *event) +{ + // Initial initialization of the SFML widget + if (!m_initialized) + { + + // Create an SFML window for rendering with the id of the window in which the drawing will be done + render_window->create(sf::WindowHandle(winId())); + + // Setup shader component + auto shader_qfile = QFile(":/shaders/crt_shader.frag"); + if (!shader_qfile.open(QIODevice::ReadOnly | QIODevice::Text)) + { + on_push_error(QString("Failed loading shader file from QFile"), EXIT_FAILURE); + return; + } + + QTextStream in(&shader_qfile); + std::string shader_text = in.readAll().toStdString(); + + if (!crt_shader->loadFromMemory(shader_text, sf::Shader::Fragment)) + { + on_push_error(QString("Failed loading shader from memory"), EXIT_FAILURE); + return; + } + + // Setting shader's variables + crt_shader->setUniform("density", 1.9f); + crt_shader->setUniform("opacityScanline", 0.2f); + crt_shader->setUniform("opacityNoise", 0.2f); + crt_shader->setUniform("curvature", 7.5f); + crt_shader->setUniform("vigantteWidth", 50.0f); + crt_shader->setUniform("Res", sf::Glsl::Vec2({800.0f, 600.0f})); + crt_shader->setUniform("brightness", 0.9f); + crt_shader->setUniform("warp_brightness", 0.1f); + + on_crt_shader_changed(SettingsManager::instance().crt_shader()); + + // Initializing drawing objects + on_init(); + + m_initialized = true; + } +} + +void GameDisplay::close_game() +{ + emu_worker->shutdown_game(); + + render_window->close(); + + emu_thread->quit(); + emu_thread->wait(); +} + +void GameDisplay::closeEvent(QCloseEvent *event) +{ + + if (!m_initialized || err_code == EXIT_FAILURE) + { + + close_game(); + qInfo() << "CPU exited unsuccessfully"; + + event->accept(); + } + else + { + int message_box_result = QMessageBox::question( + this, + "TetroidNES - " + tr("Confirmation"), + tr("Are you sure you want to quit?") + "\n" + tr("(Remember to save before quitting!)"), + QMessageBox::Yes | QMessageBox::No); + if (message_box_result == QMessageBox::No) + { + event->ignore(); + } + else + { + close_game(); + emu_worker->log_cpu(); + + qInfo() << "CPU exited successfully"; + event->accept(); + } + } +} + +void GameDisplay::update_game_scale() +{ + QSize widget_size = size(); + sf::Vector2u texture_size = texture.getSize(); + sprite->setScale( + static_cast(widget_size.width()) / texture_size.x, + static_cast(widget_size.height()) / texture_size.y); +} + +void GameDisplay::center_display() +{ +} + +bool GameDisplay::initialized() const { return m_initialized; } + +QPaintEngine *GameDisplay::paintEngine() const +{ + return nullptr; +} + +void GameDisplay::resizeEvent(QResizeEvent *event) +{ + update_game_scale(); +} + +void GameDisplay::keyPressEvent(QKeyEvent *event) +{ + if (event->key() == Qt::Key_G) + { + set_paused(!m_paused); + } +} + +GameDisplay::~GameDisplay() +{ +} diff --git a/src/gui/qt_tetroidnes/Log.cpp b/src/gui/qt_tetroidnes/Log.cpp new file mode 100644 index 0000000..835ee4e --- /dev/null +++ b/src/gui/qt_tetroidnes/Log.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +QtMessageHandler originalHandler = nullptr; + +void check_log_dir() +{ + const short int max_size = 32; + QStringList log_dir = QDir("logs", "*.txt", QDir::Name, QDir::Files).entryList(); + auto log_dir_size = log_dir.length(); + + if (log_dir_size > max_size) + { + auto logs_to_be_deleted = log_dir.sliced(0, log_dir_size - max_size); + qDebug() << "Over" << max_size << "logs reached, deleting older logs:" << logs_to_be_deleted.join(", "); + for (auto &file_name : logs_to_be_deleted) + { + if (!QFile::remove("logs/" + file_name)) + { + qWarning() << "Could not remove log file:" << file_name; + } + } + } +} +void logToFile(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + QString message = qFormatLogMessage(type, context, msg); + LogNotifier &log_notifier = LogNotifier::instance(); + std::filesystem::create_directories("logs"); + std::time_t t = std::time(0); // t is an integer type + // char *intStr = itoa(t); + // std::string str = std::string(intStr); + std::string time_stamp = std::to_string(t); + auto file = "logs/log_" + time_stamp + ".txt"; + static FILE *f = fopen(file.c_str(), "a"); + fprintf(f, "%s\n", qPrintable(message)); + fflush(f); + if (originalHandler) + { + originalHandler(type, context, msg); + } + + emit log_notifier.log_pushed(message); +} + +void InitLogs() +{ + originalHandler = qInstallMessageHandler(logToFile); + check_log_dir(); + qSetMessagePattern("%{type} | %{function}:%{line} | %{time dd/MM/yyyy h:mm:ss} | %{message}"); +} diff --git a/src/gui/qt_tetroidnes/Log_Display.cpp b/src/gui/qt_tetroidnes/Log_Display.cpp new file mode 100644 index 0000000..5400f29 --- /dev/null +++ b/src/gui/qt_tetroidnes/Log_Display.cpp @@ -0,0 +1,28 @@ +#include + +#include + +LogDisplay::LogDisplay(QWidget *parent) : QWidget{parent}, + text_display(new QPlainTextEdit(this)), + log_notifier(LogNotifier::instance()) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + setAttribute(Qt::WA_AcceptDrops, false); + + setWindowFlag(Qt::WindowType::Window); + setWindowTitle("TetroidNES - " + tr("Log System")); + + QLayout *layout = new QVBoxLayout(); + + text_display->setReadOnly(true); + + layout->addWidget(text_display); + setLayout(layout); + + connect(&log_notifier, &LogNotifier::log_pushed, this, &LogDisplay::append_line); +} + +void LogDisplay::append_line(QString line) +{ + text_display->appendPlainText(line); +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Main.cpp b/src/gui/qt_tetroidnes/Main.cpp new file mode 100644 index 0000000..f50e8e9 --- /dev/null +++ b/src/gui/qt_tetroidnes/Main.cpp @@ -0,0 +1,37 @@ +#define VERSION "1.0.0" +#include +#include +#include +#include + +#include +#include + +int main(int argc, char **argv) +{ + // originalHandler = qInstallMessageHandler(logToFile); + InitLogs(); + // int &c = argc + QApplication a(argc, argv); + a.setApplicationName("TetroidNES"); + + a.setApplicationVersion(VERSION); + + qInfo() << "STARTING" << a.applicationName() << "VERSION" << a.applicationVersion(); + QTranslator translator; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &locale : uiLanguages) + { + const QString baseName = "tetroidnes_" + QLocale(locale).name(); + if (translator.load(":/i18n/" + baseName)) + { + a.installTranslator(&translator); + break; + } + } + + MainWindow w; + w.show(); + + return a.exec(); +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Main_Window.cpp b/src/gui/qt_tetroidnes/Main_Window.cpp new file mode 100644 index 0000000..b809487 --- /dev/null +++ b/src/gui/qt_tetroidnes/Main_Window.cpp @@ -0,0 +1,288 @@ +#include +#include +#include +#include "ui_mainwindow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr const float slide_pos_multiplier = 0.1f; + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) +{ + ui->setupUi(this); + + setAttribute(Qt::WA_AcceptDrops, true); + setAttribute(Qt::WA_QuitOnClose, true); + setAttribute(Qt::WA_DeleteOnClose, true); + + auto *widget_layout = new QVBoxLayout(); + main_menubar = new MenuBar(this); + sort_control_frame = new FilterControlFrame(ui->centralwidget); + rom_list_scroll = new QScrollArea(ui->centralwidget); + rom_list = new RomList(ui->centralwidget); + page_info = new QLabel("Page 1 of 1", this); + + // widget layout + widget_layout->addWidget(sort_control_frame); + widget_layout->addWidget(rom_list_scroll); + widget_layout->setAlignment(Qt::AlignTop); + + // rom list scroll bar + rom_list_scroll->setWidgetResizable(true); + rom_list_scroll->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + rom_list_scroll->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + rom_list_scroll->setWidget(rom_list); + + // central widget + ui->centralwidget->setLayout(widget_layout); + + // setup + setWindowTitle(QString("%1 - %2").arg(qApp->applicationName(), qApp->applicationVersion())); + setMenuBar(main_menubar); + page_info->setObjectName("PageInfo"); + statusBar()->addPermanentWidget(page_info); + update_page_info(); + + // events + connect(sort_control_frame->sort_mode_button_group, &QButtonGroup::idReleased, this, &MainWindow::sort_mode_button_released); + connect(sort_control_frame->sort_ascending_button, &QPushButton::toggled, this, &MainWindow::sort_order_button_toggled); + connect(sort_control_frame->search_bar, &QLineEdit::textEdited, this, &MainWindow::search_bar_edited); + connect(rom_list_scroll->verticalScrollBar(), &QScrollBar::valueChanged, this, &MainWindow::rom_list_scroll_value_changed); +} + +void MainWindow::create_display(QString rom_link) +{ + + // std::shared_ptr display = std::make_shared(this, rom_link); + std::optional rom = load_rom(file_tobyte_vector(rom_link.toStdString())); + if (!rom.has_value()) + { + qInfo() << "Not a INES v1.0 ROM. \n (you should see NES at the top of the file if it is a INES v1.0 ROM and be greater then in size)"; + QMessageBox::critical( + this, + "TetroidNES - " + tr("Error"), + "Not a INES v1.0 ROM. \n (you should see NES at the top of the file if it is a INES v1.0 ROM and be greater then in size)"); + return; + } + auto *display = new GameDisplay(rom.value(), nullptr, rom_link); + connect(display, &QWidget::destroyed, this, &MainWindow::on_gamedisplay_destroyed); + + display->show(); + + if (SettingsManager::instance().minimize_gui_on_game_start()) + { + setWindowState(Qt::WindowMinimized); + } +} + +void MainWindow::on_gamedisplay_destroyed() +{ + if (isMinimized() && !is_a_game_running()) + { + showNormal(); + } +} + +void MainWindow::update_page_info() +{ + page_info->setText(tr("%1 %2 %3 %4 | %5 %6").arg("Page", QString::number(rom_list->current_page()), "of", QString::number(rom_list->total_pages()), "Items displayed:", QString::number(rom_list->items_per_page()))); +} + +void MainWindow::wheelEvent(QWheelEvent *event) +{ + + auto *scrollbar = rom_list_scroll->verticalScrollBar(); + + if (scrollbar->isVisible()) + { + return; + } + + const bool scrolled_up = (event->angleDelta().y() > 0); + const int current_page = rom_list->current_page(); + const int total_pages = rom_list->total_pages(); + const int min = scrollbar->minimum(); + int max = scrollbar->maximum(); + + if (!scrolled_up && current_page < total_pages) + { + rom_list->set_current_page(current_page + 1); + qApp->processEvents(); // Makes sure scroll bar updates max/min values + max = scrollbar->maximum(); + scrollbar->setSliderPosition(min + static_cast(max * slide_pos_multiplier)); + } + else if (scrolled_up && current_page > 1) + { + rom_list->set_current_page(current_page - 1); + qApp->processEvents(); // Makes sure scroll bar updates max/min values + max = scrollbar->maximum(); + scrollbar->setSliderPosition(max - static_cast(max * slide_pos_multiplier)); + } + update_page_info(); + + QMainWindow::wheelEvent(event); +} + +void MainWindow::rom_list_scroll_value_changed(const int value) +{ + auto *scrollbar = rom_list_scroll->verticalScrollBar(); + const auto current_page = rom_list->current_page(); + const int total_pages = rom_list->total_pages(); + const int min = scrollbar->minimum(); + int max = scrollbar->maximum(); + // qDebug() << "Current Page Before:" << current_page + // << "Value:" << value + // << "Max/Min Value:" << scrollbar->maximum() << "/" << scrollbar->minimum() + // << "Total pages:" << rom_list->total_pages(); + + if (value >= max && current_page < total_pages) + { + // qDebug() << "Going up from page" << current_page << "to" << current_page + 1; + + rom_list->set_current_page(current_page + 1); + qApp->processEvents(); // Makes sure scroll bar updates max/min values + max = scrollbar->maximum(); + scrollbar->setSliderPosition(min + static_cast(max * slide_pos_multiplier)); + update_page_info(); + } + else if (value <= min && current_page > 1) + { + // qDebug() << "Going down from page" << current_page << "to" << current_page - 1; + + rom_list->set_current_page(current_page - 1); + qApp->processEvents(); // Makes sure scroll bar updates max/min values + max = scrollbar->maximum(); + scrollbar->setSliderPosition(max - static_cast(max * slide_pos_multiplier)); + update_page_info(); + } + // qDebug() << "Current Page After:" << current_page; +} + +void MainWindow::sort_mode_button_released(const int id) const +{ + const auto sort_mode = RomList::SortMode(id); + auto &settings = SettingsManager::instance(); + QString search_bar_text = sort_control_frame->search_bar->text(); + const bool regex = !search_bar_text.isEmpty(); + + if (sort_mode == settings.sort_mode()) + { + return; + } + + rom_list->set_current_mode(sort_mode, regex); + settings.set_sort_mode(sort_mode); + + if (regex) + { + rom_list->search(search_bar_text); + } +} + +void MainWindow::sort_order_button_toggled(const bool toggled) const +{ + const auto sort_order = Qt::SortOrder(!toggled); + + rom_list->set_current_order(sort_order); + SettingsManager::instance().set_ascending_order(sort_order); +} + +void MainWindow::search_bar_edited(QString string) const +{ + rom_list->search(string); +} + +void MainWindow::dragEnterEvent(QDragEnterEvent *event) +{ + auto mime_data = event->mimeData(); + QUrl url; + if (mime_data->hasUrls() && !mime_data->urls().isEmpty()) + { + url = mime_data->urls()[0]; + } + else + { + qDebug() << "Drag enter event data does not have urls or is empty"; + event->setDropAction(Qt::DropAction::IgnoreAction); + return; + } + + if (url.isValid() && url.isLocalFile() && url.toLocalFile().endsWith(".nes")) + { + qDebug() << "Drag enter event data is a valid QUrl:" << url.toLocalFile(); + event->setDropAction(Qt::DropAction::MoveAction); + event->accept(); + } + else + { + qDebug() << "Drag enter event data is not a valid QUrl:" << url; + event->setDropAction(Qt::DropAction::IgnoreAction); + } + + QMainWindow::dragEnterEvent(event); +} + +void MainWindow::dragMoveEvent(QDragMoveEvent *event) +{ + event->setDropAction(Qt::DropAction::MoveAction); + event->accept(); + QMainWindow::dragMoveEvent(event); +} + +void MainWindow::dropEvent(QDropEvent *event) +{ + create_display(event->mimeData()->urls()[0].toLocalFile()); + QMainWindow::dropEvent(event); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + const bool game_run = is_a_game_running(); + qInfo() << "Quitting... games are running: " << game_run; + if (game_run) + { + int message_box_result = QMessageBox::question( + this, + "TetroidNES - " + tr("Confirmation"), + tr("Are you sure you want to quit?\n(Games are still running)"), + QMessageBox::Yes | QMessageBox::No); + + if (message_box_result == QMessageBox::No) + { + event->ignore(); + } + else + { + event->accept(); + } + } + else + { + if (event == nullptr) + { + qDebug() << "nul"; + } + qDebug() << "closing window"; + + event->accept(); + } +} + +MainWindow::~MainWindow() +{ + qDebug() << "main window Destructor"; + + if (ui != nullptr) + { + delete ui; + qDebug() << "deleting UI"; + } +} diff --git a/src/gui/qt_tetroidnes/Menu_Bar.cpp b/src/gui/qt_tetroidnes/Menu_Bar.cpp new file mode 100644 index 0000000..1893323 --- /dev/null +++ b/src/gui/qt_tetroidnes/Menu_Bar.cpp @@ -0,0 +1,107 @@ +#include +#include +#include + +#include +#include +#include + +#include +#include + +MenuBar::MenuBar(QWidget *parent) : QMenuBar{parent}, + file(addMenu(tr("File"))), + file_open(file->addAction(tr("Open..."))), + file_open_recent(file->addMenu(tr("Open Recent"))), + edit(addMenu(tr("Edit"))), + settings_open(edit->addAction(tr("Settings"))), + tools(addMenu(tr("Tools"))), + log_display_open(tools->addAction(tr("Open Log Display"))), + help(addMenu(tr("Help"))) +{ + auto &settings = SettingsManager::instance(); + + // setup + setNativeMenuBar(true); + refresh_recent_roms(settings.get_recent_roms()); + + // setup menus + // file + file_open->setShortcut(QKeySequence("Ctrl+O")); + + // edit + settings_open->setShortcut(QKeySequence("Ctrl+B")); + + // tools + log_display_open->setShortcut(QKeySequence(Qt::Key_F8)); + + // events + connect(file_open, &QAction::triggered, this, &MenuBar::open_rom); + connect(settings_open, &QAction::triggered, this, &MenuBar::open_settings); + connect(log_display_open, &QAction::triggered, this, &MenuBar::open_log_display); + connect(&settings, &SettingsManager::recent_roms_changed, this, &MenuBar::refresh_recent_roms); +} + +void MenuBar::open_settings() +{ + SettingsWidget *settings = new SettingsWidget(qobject_cast(parent())); + settings->show(); +} + +void MenuBar::open_log_display() +{ + LogDisplay *log_display = new LogDisplay(qobject_cast(parent())); + log_display->show(); +} + +void MenuBar::refresh_recent_roms(QStringList dirs) +{ + file_open_recent->clear(); + + for (auto &s : dirs) + { + auto url = QUrl(s); + auto *action = new QAction(url.fileName(), file_open_recent); + action->setData(QVariant(s)); + + connect(action, &QAction::triggered, this, [this, action](){start_rom(action->data().toString());}); + + file_open_recent->addAction(action); + } +} + +void MenuBar::open_rom() +{ + auto file_dialog = QFileDialog( + nullptr, + tr("Choose ROM to open..."), + QString(), + QString("NES ROM (*.nes)")); + file_dialog.setFileMode(QFileDialog::ExistingFile); + + if (file_dialog.exec() == QFileDialog::Accepted && !file_dialog.selectedUrls().isEmpty()) + { + + for (auto &url : file_dialog.selectedUrls()) + { + SettingsManager::instance().append_recent_roms(url.toLocalFile()); + start_rom(url); + } + } +} + +void MenuBar::start_rom(QUrl url) +{ + MainWindow *mw = qobject_cast(parent()); + mw->create_display(url.toLocalFile()); +} + +void MenuBar::start_rom(QString url) +{ + MainWindow *mw = qobject_cast(parent()); + mw->create_display(url); +} + +MenuBar::~MenuBar() +{ +} diff --git a/src/gui/qt_tetroidnes/Rom_Data.cpp b/src/gui/qt_tetroidnes/Rom_Data.cpp new file mode 100644 index 0000000..34f1c4c --- /dev/null +++ b/src/gui/qt_tetroidnes/Rom_Data.cpp @@ -0,0 +1,48 @@ +#include + +#include + +RomData::RomData(QObject *parent, uint16_t year, QByteArray img, QString title, bool favorited, QUrl path) : QObject{parent} +{ + m_year = year; + m_img = img; + m_title = title; + m_favorited = favorited; + m_path = path; +} +RomData::~RomData() +{ +} + +bool RomData::is_empty() const +{ return m_path.isEmpty() || !m_path.isValid(); } + +uint16_t RomData::year() const +{ return m_year; } + +void RomData::set_year(const uint16_t year) +{ m_year = year; } + +QString RomData::title() const +{ return m_title; } + +void RomData::set_title(QString title) +{ m_title = title; } + +QByteArray RomData::img() const +{ return m_img; } + +void RomData::set_img(QByteArray img) +{ m_img = img; } + +bool RomData::favorited() const +{ return m_favorited; } + +void RomData::set_favorited(const bool b) +{ m_favorited = b; } + +QUrl RomData::path() const +{return m_path; } + +void RomData::set_path(QUrl path) +{m_path = path; } \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Rom_List.cpp b/src/gui/qt_tetroidnes/Rom_List.cpp new file mode 100644 index 0000000..d0d3c42 --- /dev/null +++ b/src/gui/qt_tetroidnes/Rom_List.cpp @@ -0,0 +1,389 @@ +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +RomList::RomList(QWidget *parent) : QWidget{parent}, + data(new QList()), + main_layout(new FlowLayout()), + m_current_order(Qt::AscendingOrder), + m_current_page(1), + m_total_pages(1), + m_items_per_page(0) +{ + auto &settings = SettingsManager::instance(); + + main_layout->setContentsMargins(0, 150, 0, 150); + setLayout(main_layout); + + setObjectName("RomList"); + + m_items_per_page = 10; // TODO: Change this so the program remembers what the user chose last time + + on_rom_dirs_changed(QStringList()); + + set_current_mode(settings.sort_mode()); + set_current_order(settings.ascending_order()); + + connect(&settings, &SettingsManager::rom_dirs_changed, this, &RomList::on_rom_dirs_changed); + + qInfo() << "Finished setting up romlist"; +} + +void RomList::cleanup_romdata() +{ + qDeleteAll(data->begin(), data->end()); + data->clear(); +} + +void RomList::on_rom_dirs_changed(QStringList dirs) +{ + cleanup_romdata(); + setup_display(); + update_total_pages(); + m_current_page = 1; + update_display(); +} + +void RomList::setup_display() +{ + const QStringList rom_dirs = SettingsManager::instance().get_rom_dirs(); + const QRegularExpression qregex(R"(\.nes$)"); + + if (rom_dirs.isEmpty()) + { + qDebug() << "Rom dirs were empty"; + return; + } + + qDebug() << "Searching rom dirs, size:" << rom_dirs.length(); + + for (auto &directory_url : rom_dirs) + { + + auto dir = QDir(directory_url); + + if (!dir.isAbsolute() || !dir.exists() || dir.isEmpty()) + { + continue; + } + + // TODO: MAKE A WAY TO PARSE METADATA OF ROMS TO SET THESE + uint16_t year = 1980; + QByteArray image; + bool favorite = false; + + const QStringList files = dir.entryList(QDir::Files | QDir::NoSymLinks).filter(qregex); + if (files.isEmpty()) + { + qDebug() << dir.dirName() << "is empty, skipping iteration..."; + continue; + } + + for (auto &url : files) + { + auto final_url = QUrl(dir.absoluteFilePath(url)); + auto rom_title = url.section('.', 0, 0); + + qDebug() + << "File:" << final_url.toLocalFile() + << "Title:" << rom_title; + + auto *romdata = new RomData(nullptr, year, image, rom_title, favorite, final_url); + data->append(romdata); + } + } + qDebug() << "Finished!"; +} + +RomData* RomList::get_romdata(const int page, const int index) +{ + uint32_t romlist_index = m_items_per_page * (page - 1) + index; + RomData *romdata = nullptr; + + if (data->size() - 1 > romlist_index && romlist_index >= 0) + { + //qDebug() << "Valid index:" << romlist_index; + romdata = data->at(romlist_index); + } + + qDebug() + << "page:" << page + << "index:" << index + << "Rom data size:" << data->size() + // This expression wouldn't work unless I type casted it :shrug: + << "Is nullptr (Out of bounds):" << (bool)(romdata == nullptr); + + return romdata; +} + +uint32_t RomList::items_per_page() const { return m_items_per_page; } + +void RomList::set_items_per_page(uint32_t newNum) +{ + m_items_per_page = newNum; + update_display(); +} + +void RomList::set_current_page(uint32_t i) +{ + if (i == m_current_page) + { + return; + } + m_current_page = i; + update_display(); +} + +uint32_t RomList::current_page() const { return m_current_page; } + +void RomList::update_total_pages() +{ + qDebug() + << "Data length:" << data->length() + << "Items per page:" << items_per_page(); + if (data->length() <= 1) + { + qInfo() << "Data length is lower or equal to 1! Setting total pages to 1"; + m_total_pages = 1; + } + else + { + m_total_pages = (data->length() + (items_per_page() - 1)) / items_per_page(); // Always rounds up a page + } +} +uint32_t RomList::total_pages() const { return m_total_pages; } + +// Probably needs another refactor? +void RomList::update_display() +{ + auto list_of_widgets = findChildren(); + int widgets_count = list_of_widgets.size(); + + qDebug() << "Updating display, Widgets count:" << widgets_count; + + // Add widgets if there are less items than items that need to be shown + if (m_items_per_page > widgets_count) + { + + qDebug() << "Adding widgets"; + + for (int i = 0; i < m_items_per_page - widgets_count; i++) + { + auto *new_widget = new RomListItem(nullptr, this); + + main_layout->addWidget(new_widget); + list_of_widgets.append(new_widget); + } + widgets_count = list_of_widgets.size(); + } + // Remove widgets if there are more items than items that need to be shown + else if (m_items_per_page < widgets_count) + { + qDebug() << "Removing widgets"; + for (int i = widgets_count; i > widgets_count - (widgets_count - m_items_per_page); i--) + { + qDebug() << i; + auto *widget = list_of_widgets[i]; + + widget->setParent(nullptr); + list_of_widgets.removeLast(); + widget->deleteLater(); + } + widgets_count = list_of_widgets.size(); + } + + qDebug() << "Applying data to widgets"; + + // Apply data + for (int i = 0; i < widgets_count; i++) + { + auto *romdata = get_romdata(m_current_page, i); + auto romlistitem = list_of_widgets[i]; + if (romdata != nullptr) + { + romlistitem->set_romdata(romdata); + // Hide empty items and show valid items + romdata->is_empty() ? romlistitem->hide() : romlistitem->show(); + } + else + { + romlistitem->hide(); + } + + } + + qDebug() << "Done updating display"; +} + +const bool RomList::compare_regex(const RomData *a, const RomData *b, const QRegularExpression &expr, const SortMode &mode) +{ + const bool match_a = expr.match(a->title()).hasMatch(); + const bool match_b = expr.match(b->title()).hasMatch(); + + if (match_a != match_b) + { + return match_a; + } + else + { + switch (mode) + { + case Year: + return compare_year(a, b); + case Favorites: + return compare_favorite(a, b); + case AZ: + return compare_alphabet(a, b); + } + return false; + } +} + +const bool RomList::compare_year(const RomData *a, const RomData *b) +{ + if (a->year() != b->year()) + { + return a->year() < b->year(); + } + else + { + return compare_alphabet(a, b); + } +} + +const bool RomList::compare_favorite(const RomData *a, const RomData *b) +{ + if (a->favorited() != b->favorited()) + { + return a->favorited() < b->favorited(); + } + else + { + return compare_alphabet(a, b); + } +} + +const bool RomList::compare_alphabet(const RomData *a, const RomData *b) +{ + // Decide the lowest length to prevent out of bounds error + const int lowestLength = (a->title().length() > b->title().length()) ? b->title().length() : a->title().length(); + for (int i = 0; i < lowestLength; i++) + { + if (a->title().at(i).toLower() == b->title().at(i).toLower()) + { + continue; + } + + return a->title().at(i).toLower() < b->title().at(i).toLower(); + } + + return false; +} + +void RomList::search(QString &expr) +{ + auto regular_expression = QRegularExpression(expr); + SortMode mode = current_mode(); + + if (current_order() == Qt::AscendingOrder) + { + std::sort(data->begin(), data->end(), + [®ular_expression, &mode](const RomData *a, const RomData *b) + { + return compare_regex(a, b, regular_expression, mode); + }); + } + else + { + std::sort(data->rbegin(), data->rend(), + [®ular_expression, &mode](const RomData *a, const RomData *b) + { + return compare_regex(a, b, regular_expression, mode); + }); + } + + update_display(); +} + +// TODO: Make this function not have two switch cases that essientially do the same thing +void RomList::set_current_mode(const SortMode &mode, const bool update) +{ + m_current_mode = mode; + if (!update) + { + return; + } + + if (current_order() == Qt::AscendingOrder) + { + auto begin = data->begin(); + auto end = data->end(); + + switch (mode) + { + case Year: + std::sort(begin, end, compare_year); + break; + case Favorites: + std::sort(begin, end, compare_favorite); + break; + case AZ: + std::sort(begin, end, compare_alphabet); + break; + } + } + else + { + auto rbegin = data->rbegin(); + auto rend = data->rend(); + + switch (mode) + { + case Year: + std::sort(rbegin, rend, compare_year); + break; + case Favorites: + std::sort(rbegin, rend, compare_favorite); + break; + case AZ: + std::sort(rbegin, rend, compare_alphabet); + break; + } + } + + update_display(); +} + +RomList::SortMode RomList::current_mode() const +{ + return m_current_mode; +} + +void RomList::set_current_order(const Qt::SortOrder order) +{ + if (m_current_order == order) + return; + + m_current_order = order; + std::reverse(data->begin(), data->end()); + + update_display(); +} + +Qt::SortOrder RomList::current_order() const +{ + return m_current_order; +} + +RomList::~RomList() +{ + cleanup_romdata(); +} diff --git a/src/gui/qt_tetroidnes/Rom_List_Item.cpp b/src/gui/qt_tetroidnes/Rom_List_Item.cpp new file mode 100644 index 0000000..5eab8a9 --- /dev/null +++ b/src/gui/qt_tetroidnes/Rom_List_Item.cpp @@ -0,0 +1,106 @@ +#include + +#include +#include +#include + +RomListItem::RomListItem(RomData *data, QWidget *parent) : QWidget{parent} +{ + + setMinimumSize(250, 250); + + auto *layout = new QVBoxLayout(); + auto *buttons_layout = new QHBoxLayout(); + + title = new QLabel(this); + play = new QPushButton(this); + year = new QLabel(this); + buttons_frame = new QFrame(this); + favorite_button = new QPushButton(buttons_frame); + + favorite_button->setCheckable(true); + favorite_button->setText("Favorite button"); + + title->setAlignment(Qt::AlignCenter); + year->setAlignment(Qt::AlignCenter); + + layout->addWidget(buttons_frame); + layout->addWidget(play); + layout->addWidget(title); + layout->addWidget(year); + setLayout(layout); + + buttons_layout->addWidget(favorite_button); + buttons_frame->setLayout(buttons_layout); + + if (data != nullptr) + { + set_romdata(data); + qDebug() << data->title(); + } + + // Events + connect(favorite_button, &QPushButton::clicked, this, &RomListItem::favorite_button_clicked); + connect(play, &QPushButton::clicked, this, &RomListItem::play_button_clicked); +} + +RomListItem::~RomListItem() +{ +} + +void RomListItem::set_romdata(RomData *data) +{ + m_romdata = data; + update_data(); +} + +RomData* RomListItem::romdata() const +{ + return m_romdata; +} + +void RomListItem::favorite_button_clicked(int checked) +{ + RomList *rom_list = qobject_cast(parent()); + + m_romdata->set_favorited(checked); + + // Refresh display + if (rom_list->current_mode() == RomList::SortMode::Favorites) + { + rom_list->set_current_mode(RomList::SortMode::Favorites); + } +} + +void RomListItem::update_data() +{ + title->setText(m_romdata->title()); + year->setText(QString::number(m_romdata->year())); + if (m_romdata->img().isNull()) + { + auto placeholder_img = QPixmap(175, 175); + placeholder_img.fill(Qt::darkGreen); + play->setIcon(QIcon(placeholder_img)); + play->setIconSize(placeholder_img.size()); + } + // else {play->setIcon(QIcon());) + + favorite_button->setChecked(m_romdata->favorited()); +} + +void RomListItem::play_button_clicked() +{ + + auto path = m_romdata->path(); + + if (!path.isValid()) + { + qCritical() << "Path is not valid (Try restarting the program)"; + return; + } + + qInfo() << "Starting" << title->text(); + qDebug() << "Path:" << path; + SettingsManager::instance().append_recent_roms(path.toString()); + start_game(path.toString()); +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Settings_Display.cpp b/src/gui/qt_tetroidnes/Settings_Display.cpp new file mode 100644 index 0000000..b652f55 --- /dev/null +++ b/src/gui/qt_tetroidnes/Settings_Display.cpp @@ -0,0 +1,205 @@ +#include + +#include +#include +#include +#include + +#include +#include + +#define ABOUT_TEXT "dummy text" // Placeholder until we figure out how to store long strings of text +// +// QSTACKEDWIDGET CONTROLLER +// +SettingsDisplay::SettingsDisplay(QWidget *parent) : QStackedWidget{parent} +{ + general = new GeneralSettingsDisplay(this); + emulator = new EmulatorSettingsDisplay(this); + about = new About(this); + + addWidget(general); + addWidget(emulator); + addWidget(about); + + setCurrentIndex(0); + + // Events + connect(general->add_directory, &QPushButton::clicked, this, &SettingsDisplay::on_add_directory_clicked); + connect(general->min_on_game_start_checkbox, &QCheckBox::toggled, this, &SettingsDisplay::on_min_gui_on_start_checkbox_toggled); + + connect(emulator->speed_combobox, &QComboBox::currentIndexChanged, this, &SettingsDisplay::on_speed_index_changed); +} + +SettingsDisplay::~SettingsDisplay() +{ +} + +void SettingsDisplay::on_speed_index_changed(const int idx) +{ +} + +void SettingsDisplay::on_add_directory_clicked() +{ + QFileDialog file_dialog; + + file_dialog.setFileMode(QFileDialog::Directory); + + if (file_dialog.exec()) + { + general->directories->appendPlainText(file_dialog.selectedFiles().join("\n")); + } +} + +void SettingsDisplay::on_min_gui_on_start_checkbox_toggled(const bool toggled) +{ +} +// +// GENERAL SETTINGS +// +GeneralSettingsDisplay::GeneralSettingsDisplay(QWidget *parent) : QWidget{parent} +{ + SettingsManager &settings = SettingsManager::instance(); + const auto settings_rom_dirs = settings.get_rom_dirs(); + const auto settings_min_on_game_start = settings.minimize_gui_on_game_start(); + + QVBoxLayout *layout = new QVBoxLayout(); + + // General settings groupbox + general_groupbox = new QGroupBox(tr("General"), this); + QVBoxLayout *general_groupbox_layout = new QVBoxLayout(); + min_on_game_start_checkbox = new QCheckBox(tr("Minimize GUI on game start"), general_groupbox); + + min_on_game_start_checkbox->setObjectName("min_gui_on_game_start"); + min_on_game_start_checkbox->setChecked(settings_min_on_game_start); + + general_groupbox_layout->addWidget(min_on_game_start_checkbox); + general_groupbox->setLayout(general_groupbox_layout); + + // Search directories groupbox + + directory_groupbox = new QGroupBox(tr("Search Directories"), this); + QVBoxLayout *directory_groupbox_layout = new QVBoxLayout(); + directories = new QPlainTextEdit(directory_groupbox); + add_directory = new QPushButton(directory_groupbox); + + add_directory->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd)); + add_directory->setToolTip(tr("Add directory for TetroidNES to search for ROMs in")); + + directories->setObjectName("rom_directory"); + directories->setPlainText(settings_rom_dirs.join("\n")); + directories->setToolTip(tr("Add/Remove/Edit directories for TetroidNES to search for ROMs in")); + + directory_groupbox_layout->addWidget(directories); + directory_groupbox_layout->addWidget(add_directory); + directory_groupbox_layout->setAlignment(add_directory, Qt::AlignLeft); + directory_groupbox->setLayout(directory_groupbox_layout); + + // Layout + layout->addWidget(general_groupbox); + layout->addWidget(directory_groupbox); + setLayout(layout); + +} + +GeneralSettingsDisplay::~GeneralSettingsDisplay() +{ +} +// +// EMULATOR SETTINGS +// +EmulatorSettingsDisplay::EmulatorSettingsDisplay(QWidget *parent) : QWidget{parent} +{ + SettingsManager &settings = SettingsManager::instance(); + const auto settings_speed = settings.speed(); + const auto settings_crt_shader = settings.crt_shader(); + const auto settings_run_on_dif_thread = settings.run_emulator_on_seperate_thread(); + const auto default_combobox_key = QString("100%"); + int speed_combobox_current_idx; + + auto *layout = new QVBoxLayout(); + auto *emulator_groupbox_layout = new QVBoxLayout(); + auto *emu_speed_groupbox_layout = new QVBoxLayout(); + + // Emulator GroupBox + emulator_groupbox = new QGroupBox(tr("Emulator"), this); + + // Threaded CheckBox + threaded_checkbox = new QCheckBox(tr("Enable Threading"), this); + threaded_checkbox->setObjectName("run_emu_in_different_thread"); + threaded_checkbox->setChecked(settings_run_on_dif_thread); + + // Crt shader CheckBox + crt_shader_checkbox = new QCheckBox("CRT Filter", this); + crt_shader_checkbox->setObjectName("crt_shader"); + crt_shader_checkbox->setChecked(settings_crt_shader); + + // Emu Speed GroupBox + emu_speed_groupbox = new QGroupBox(tr("Emulation Speed"), this); + + // Emu speed ComboBox + speed_combobox = new QComboBox(emu_speed_groupbox); + speed_combobox->setObjectName("speed"); + speed_combobox->addItem("25%", QVariant(0.25f)); + speed_combobox->addItem("50%", QVariant(0.5f)); + speed_combobox->addItem("75%", QVariant(0.75f)); + speed_combobox->addItem(default_combobox_key, QVariant(1.f)); + speed_combobox->addItem("200%", QVariant(2.f)); + speed_combobox->addItem("400%", QVariant(4.f)); + speed_combobox->addItem("Unlimited", QVariant(0.f)); + + // Check for invalid speed value in settings file + speed_combobox_current_idx = speed_combobox->findData(QVariant(settings_speed)); + if (speed_combobox_current_idx == -1) + { + float default_combobox_value; + + qWarning() + << "Emulation speed multiplier" + << settings.speed() + << "was not found in speed_combobox, setting to" + << default_combobox_key; + + speed_combobox_current_idx = speed_combobox->findText(default_combobox_key); + + default_combobox_value = speed_combobox->itemData(speed_combobox_current_idx).toFloat(); + + settings.set_speed(default_combobox_value); // Correct invalid speed value to default + } + speed_combobox->setCurrentIndex(speed_combobox_current_idx); + + // Emulator GroupBox Layout + emulator_groupbox_layout->addWidget(threaded_checkbox); + emulator_groupbox_layout->addWidget(crt_shader_checkbox); + emulator_groupbox->setLayout(emulator_groupbox_layout); + + // Emulator Speed GroupBox Layout + emu_speed_groupbox_layout->addWidget(speed_combobox); + emu_speed_groupbox->setLayout(emu_speed_groupbox_layout); + + layout->addWidget(emulator_groupbox); + layout->addWidget(emu_speed_groupbox); + setLayout(layout); +} + +EmulatorSettingsDisplay::~EmulatorSettingsDisplay() +{ +} +// +// ABOUT +// +About::About(QWidget *parent) : QWidget{parent} +{ + QVBoxLayout *layout = new QVBoxLayout(); + + text = new QLabel(ABOUT_TEXT, this); + text->setTextFormat(Qt::PlainText); // PlainText is placeholder until we decide what text format we want + + layout->addWidget(text); + + setLayout(layout); +} + +About::~About() +{ +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Settings_Manager.cpp b/src/gui/qt_tetroidnes/Settings_Manager.cpp new file mode 100644 index 0000000..1681009 --- /dev/null +++ b/src/gui/qt_tetroidnes/Settings_Manager.cpp @@ -0,0 +1,137 @@ +#include +#include "settingsmanager.h" + +constexpr const auto key_romdir = "romdir"; +constexpr const auto key_min_game_on_start = "minimize_gui_on_game_start"; +constexpr const auto key_speed = "emu_speed"; +constexpr const auto key_run_emulator_on_seperate_thread = "run_emulator_on_seperate_thread"; +constexpr const auto key_crt_shader = "crt_shader"; +constexpr const auto key_sort_mode = "QOL/sort_mode"; +constexpr const auto key_ascend_order = "QOL/ascending_order"; +constexpr const auto key_recent_roms = "QOL/recent_roms"; + +constexpr const uint max_recent_roms = 10; + +SettingsManager::SettingsManager() +{ +} + +QStringList SettingsManager::get_rom_dirs() +{ + return m_settings.value(key_romdir, QStringList()).toStringList(); +} + +void SettingsManager::set_rom_dir(QStringList dir) +{ + m_settings.setValue(key_romdir, dir); + + emit rom_dirs_changed(dir); +} + +void SettingsManager::set_rom_dir(QString dir) +{ + auto rom_dirs = get_rom_dirs(); + rom_dirs.append(dir); + + m_settings.setValue(key_romdir, rom_dirs); + + emit rom_dirs_changed(rom_dirs); +} + +void SettingsManager::set_save_dir(QString dir) +{ + m_settings.setPath(QSettings::IniFormat, QSettings::UserScope, dir); +} + +QString SettingsManager::save_dir() const +{ + return m_settings.fileName(); +} + +bool SettingsManager::minimize_gui_on_game_start() const +{ + return m_settings.value(key_min_game_on_start, false).toBool(); +} + +void SettingsManager::set_minimize_gui_on_game_start(const bool b) +{ + return m_settings.setValue(key_min_game_on_start, b); +} + +float SettingsManager::speed() const +{ + return m_settings.value(key_speed, 1.f).toFloat(); +} + +void SettingsManager::set_speed(const float speed) +{ + m_settings.setValue(key_speed, speed); + emit speed_changed(speed); +} + +void SettingsManager::set_sort_mode(const RomList::SortMode sort_mode) +{ + m_settings.setValue(key_sort_mode, sort_mode); +} + +RomList::SortMode SettingsManager::sort_mode() const +{ + return RomList::SortMode(m_settings.value(key_sort_mode, RomList::AZ).toInt()); +} + +Qt::SortOrder SettingsManager::ascending_order() const +{ + return Qt::SortOrder(m_settings.value(key_ascend_order, Qt::AscendingOrder).toInt()); +} + +void SettingsManager::set_ascending_order(const Qt::SortOrder sort_order) +{ + m_settings.setValue(key_ascend_order, sort_order); +} + +bool SettingsManager::crt_shader() const +{ + return m_settings.value(key_crt_shader, false).toBool(); +} + +void SettingsManager::set_crt_shader(bool b) +{ + m_settings.setValue(key_crt_shader, b); + emit crt_shader_changed(b); +} + +void SettingsManager::set_recent_roms(QStringList dirs) +{ + if (dirs.length() > max_recent_roms) + { + dirs.resize(max_recent_roms); + } + + dirs.removeDuplicates(); + + m_settings.setValue(key_recent_roms, dirs); + emit recent_roms_changed(dirs); +} + +void SettingsManager::append_recent_roms(QString dir) +{ + auto recent_roms = get_recent_roms(); + recent_roms.append(dir); + + set_recent_roms(recent_roms); +} + +QStringList SettingsManager::get_recent_roms() +{ + return m_settings.value(key_recent_roms, QStringList()).toStringList(); +} + +void SettingsManager::set_run_emulator_on_seperate_thread(bool b) +{ + m_settings.setValue(key_run_emulator_on_seperate_thread, b); +} + +bool SettingsManager::run_emulator_on_seperate_thread() const +{ + return m_settings.value(key_run_emulator_on_seperate_thread, true).toBool(); +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/Settings_Widget.cpp b/src/gui/qt_tetroidnes/Settings_Widget.cpp new file mode 100644 index 0000000..7bd0728 --- /dev/null +++ b/src/gui/qt_tetroidnes/Settings_Widget.cpp @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include + +#include +#include +#include + +SettingsWidget::SettingsWidget(QWidget *parent) : QWidget{parent} +{ + const int stretch_setting_display = 100; + + setAttribute(Qt::WA_DeleteOnClose, true); + setAttribute(Qt::WA_AcceptDrops, false); + + setWindowFlag(Qt::WindowType::Window); + setWindowTitle("TetroidNES - " + tr("Settings")); + + auto *layout = new QVBoxLayout(); + auto *layout_controls = new QHBoxLayout(); + + controls_frame = new QFrame(this); + setting_category = new QListWidget(this); + setting_display = new SettingsDisplay(this); + button_statusbar = new QStatusBar(this); + apply_changes = new QPushButton(tr("Apply"), button_statusbar); + cancel_changes = new QPushButton(tr("Cancel"), button_statusbar); + + // setting_category setup + QStringList items; + items << "General" << "Emulator" << "About"; + setting_category->setSelectionMode(QListWidget::SingleSelection); + setting_category->addItems(items); + setting_category->item(0)->setSelected(true); + + // Buttons setup + apply_changes->setObjectName("apply"); + cancel_changes->setObjectName("cancel"); + + // Status bar setup + button_statusbar->addPermanentWidget(apply_changes); + button_statusbar->addPermanentWidget(cancel_changes); + + // Apply layouts + layout_controls->addWidget(setting_category); + layout_controls->addWidget(setting_display, stretch_setting_display); + controls_frame->setLayout(layout_controls); + + layout->addWidget(controls_frame); + layout->addWidget(button_statusbar); + setLayout(layout); + + // Events + connect(setting_category, &QListWidget::itemClicked, this, &SettingsWidget::on_setting_category_item_clicked); + connect(apply_changes, &QPushButton::clicked, this, &SettingsWidget::on_apply_changes_clicked); + connect(cancel_changes, &QPushButton::clicked, this, &SettingsWidget::on_cancel_changes_clicked); +} + +SettingsWidget::~SettingsWidget() +{ +} + +void SettingsWidget::on_setting_category_item_clicked(const QListWidgetItem *item) +{ + const int category_index = setting_category->row(item); + setting_display->setCurrentIndex(category_index); +} + +void SettingsWidget::on_apply_changes_clicked() +{ + + auto &settings = SettingsManager::instance(); + auto rom_dir = setting_display->findChild("rom_directory"); + auto min_gui_on_game_start = setting_display->findChild("min_gui_on_game_start"); + auto speed = setting_display->findChild("speed"); + auto threaded = setting_display->findChild("run_emu_in_different_thread"); + auto crt_shader = setting_display->findChild("crt_shader"); + + // ROM Directories + QStringList string_list; + + for (auto &string : rom_dir->toPlainText().split("\n")) + { + if (QFileInfo(string).isDir() && QFileInfo(string).isAbsolute()) + { + string_list << string; + } + } + + if (string_list != settings.get_rom_dirs()) + { + settings.set_rom_dir(string_list); + } + + // Minimize GUI on game start + const bool min_gui_on_game_start_checked = min_gui_on_game_start->isChecked(); + if (min_gui_on_game_start_checked != settings.minimize_gui_on_game_start()) + { + settings.set_minimize_gui_on_game_start(min_gui_on_game_start_checked); + } + + // Game speed multiplier + const float speed_multipler = speed->currentData().toFloat(); + if (speed_multipler != settings.speed()) + { + settings.set_speed(speed_multipler); + } + + // Emulator threading mode + const bool is_threaded = threaded->isChecked(); + settings.set_run_emulator_on_seperate_thread(is_threaded); + + // Emulator crt shader + const bool is_crt_shader_on = crt_shader->isChecked(); + settings.set_crt_shader(is_crt_shader_on); + + // Finished saving settings + qInfo() << "saving settings in " << SAVE_DIR; + QMessageBox::information(this, tr("Settings saved"), + tr("Your settings have been saved")); +} + +void SettingsWidget::on_cancel_changes_clicked() +{ +} + +void SettingsWidget::closeEvent(QCloseEvent *event) +{ + if (!apply_changes->isEnabled()) + { + QWidget::closeEvent(event); + qInfo() << "You didnt save your settings "; + return; + } + + int message_box_result = QMessageBox::question( + this, + "TetroidNES - " + tr("Confirmation"), + tr("Settings are unsaved!\nAre you sure you want to close the settings?"), + QMessageBox::Yes | QMessageBox::Cancel); + + if (message_box_result == QMessageBox::Yes) + { + event->accept(); + } + else + { + event->ignore(); + } +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/mainwindow.ui b/src/gui/qt_tetroidnes/mainwindow.ui new file mode 100644 index 0000000..0c8b869 --- /dev/null +++ b/src/gui/qt_tetroidnes/mainwindow.ui @@ -0,0 +1,21 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + Tetroid NES + + + + + + + diff --git a/src/gui/qt_tetroidnes/resources/images/icon.jpg b/src/gui/qt_tetroidnes/resources/images/icon.jpg new file mode 100644 index 0000000..7630b13 Binary files /dev/null and b/src/gui/qt_tetroidnes/resources/images/icon.jpg differ diff --git a/src/gui/qt_tetroidnes/resources/resources.qrc b/src/gui/qt_tetroidnes/resources/resources.qrc new file mode 100644 index 0000000..2850f3f --- /dev/null +++ b/src/gui/qt_tetroidnes/resources/resources.qrc @@ -0,0 +1,6 @@ + + + images/icon.jpg + shaders/crt_shader.frag + + \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/resources/shaders/crt_shader.frag b/src/gui/qt_tetroidnes/resources/shaders/crt_shader.frag new file mode 100644 index 0000000..1b981aa --- /dev/null +++ b/src/gui/qt_tetroidnes/resources/shaders/crt_shader.frag @@ -0,0 +1,74 @@ +#version 150 + +uniform sampler2D tex0; +uniform float mouse_x_offset; // 0.5 +uniform float time; + +uniform float density; +uniform float opacityScanline; +uniform float opacityNoise; +uniform float curvature; +uniform float vigantteWidth; +uniform vec2 Res; + +uniform float brightness; +uniform float warp_brightness; + +in vec4 Vertex_UV; +out vec4 FragColor; + +float random (vec2 st) { + //these are constants dont need to parameterized i dont think + + float noiseScale = 43758.5453123; + float noiseY = 78.233; + float noiseX = 12.9898; + return fract(sin(dot(st.xy, + vec2(noiseX,noiseY)))* + noiseScale); +} + +vec2 curve(vec2 uv, float warp){ + vec2 dc = abs(0.5-uv); + dc *= dc; + + uv.x -= 0.5; uv.x *= 1.0+(dc.y*(0.3*warp)); + uv.x += 0.5; + uv.y -= 0.5; uv.y *= 1.0+(dc.x*(0.4*warp)); + uv.y += 0.5; + return uv; +} + +void main(){ + //TODO: Uniform these + vec4 ntsc_color = vec4(1.2, 1.2, 1.4, 1.0); + vec4 black_color = vec4(0.0,0.0,0.0,1.0); + + vec2 uv = gl_FragCoord.xy / Res; + uv.y = (1.0 - uv.y); + + + vec2 dc = abs(0.5-uv); + dc *= dc; + vec4 tc = texture(tex0, uv.xy ); //texture + vec2 curve = uv * 2. - 1.; + float offset = length(curve) / curvature; + curve += curve * offset * offset; + curve = curve * 0.5 + 0.5; + + + vec2 vignetteThreshold = vigantteWidth / Res.xy; + vec2 vignette = smoothstep(vec2(0), vignetteThreshold, 1. - abs(curve * 2. - 1.)); + //adjusts color and addes the warp affefct + tc = tc * vignette.x * vignette.y; + tc = pow(tc, ntsc_color) * brightness + warp_brightness; + + float count = Res.y * density; + vec2 sl = vec2(sin(uv.y * count), cos(uv.y * count)); + vec4 scanlines = vec4(sl.x, sl.y, sl.x, 1.0); + tc += tc * scanlines * opacityScanline; + tc += tc * vec4(random(uv*time)) * opacityNoise; + vec3 scanlinelerp = mix(tc.rgb, black_color.xyz, opacityScanline); + FragColor = vec4(scanlinelerp, 1.0); + +} \ No newline at end of file diff --git a/src/gui/qt_tetroidnes/translations/tetroidnes_en_US.ts b/src/gui/qt_tetroidnes/translations/tetroidnes_en_US.ts new file mode 100644 index 0000000..edd0d34 --- /dev/null +++ b/src/gui/qt_tetroidnes/translations/tetroidnes_en_US.ts @@ -0,0 +1,3 @@ + + + diff --git a/src/include/Emulator/APU.h b/src/include/Emulator/APU.h index 42aed32..a6b0c64 100644 --- a/src/include/Emulator/APU.h +++ b/src/include/Emulator/APU.h @@ -1,7 +1,8 @@ +#pragma once + #include +#include -#ifndef APU_H -#define APU_H class APU { private: @@ -9,6 +10,4 @@ class APU public: APU(); -}; - -#endif \ No newline at end of file +}; \ No newline at end of file diff --git a/src/include/Emulator/BitOperations.h b/src/include/Emulator/BitOperations.h index 3d8bcac..6387715 100644 --- a/src/include/Emulator/BitOperations.h +++ b/src/include/Emulator/BitOperations.h @@ -1,5 +1,5 @@ #include -#include "Computer.h" +#include uint8_t add(uint8_t a, uint8_t b, CPU &cpu, uint8_t &carry); diff --git a/src/include/Emulator/Bus.h b/src/include/Emulator/Bus.h index 0e094b5..16e2c1a 100644 --- a/src/include/Emulator/Bus.h +++ b/src/include/Emulator/Bus.h @@ -1,10 +1,12 @@ +#pragma once + #include #include #include #include +#include +#include -#ifndef CONTROILER_H -#define CONTROILER_H enum class Controller { A = 0b00000001, @@ -16,9 +18,7 @@ enum class Controller LEFT = 0b01000000, RIGHT = 0b10000000 }; -#endif -#ifndef BUS_H -#define BUS_H + class Bus { private: @@ -30,10 +30,14 @@ class Bus uint16_t program_counter; uint8_t stack_pointer; uint16_t stack; - uint8_t button_idx; + uint8_t joypad1_idx; + uint8_t joypad2_idx; + + std::optional err_string; public: size_t clock_cycles; + size_t clock_cycles_instr; uint8_t stored_instructions[2]; bool strobe; uint8_t joy_pad_byte1; @@ -65,11 +69,16 @@ class Bus void set_stack_pointer(uint8_t value); void print_stack(); // prints true value of stack void tick(); + int reset_clock(); // void render(sf::Texture &texture, int bank, int tile); bool NMI_interrupt(); void print_ppu(); - uint8_t read_joypad(); + uint8_t read_joypad1(); + uint8_t read_joypad2(); void write_controller1(Controller value, int isPressed); + void write_controller2(Controller value, int isPressed); + std::vector render_texture(std::tuple res); -}; -#endif \ No newline at end of file + std::optional check_error(); + void log_ppu(); +}; \ No newline at end of file diff --git a/src/include/Emulator/Computer.h b/src/include/Emulator/EmulatorUtil.h similarity index 77% rename from src/include/Emulator/Computer.h rename to src/include/Emulator/EmulatorUtil.h index 8e498ea..df0344b 100644 --- a/src/include/Emulator/Computer.h +++ b/src/include/Emulator/EmulatorUtil.h @@ -1,14 +1,14 @@ +#pragma once + #include #include #include - +#include // #include #define NES_RES_A 256 * 240 #define NES_RES_W 240 #define NES_RES_L 256 -#ifndef CPU_H -#define CPU_H struct CPU { uint8_t A_Reg; @@ -22,7 +22,7 @@ struct CPU unsigned C : 1; // Carry unsigned Z : 1; // Zero unsigned I : 1; // interrupt disabled - unsigned D : 1; // Decimal mode (un-used) + unsigned D : 1; // Decimal mode (un-used in the NES) unsigned B : 1; // break unsigned Unused : 1; unsigned V : 1; // overflow @@ -34,10 +34,9 @@ struct CPU int error_code; Bus bus; + std::optional interrupt; }; -#endif -#ifndef ADDRESS_MODE -#define ADDRESS_MODE + enum class AddressMode { ACCUMULATOR, @@ -53,8 +52,4 @@ enum class AddressMode INDIRECT_Y, RELATIVE, IMPLIED -}; -#endif -CPU run(CPU cpu, std::string file_name); -void printCPU_stats(CPU cpu); -CPU init(std::string file_name); +}; \ No newline at end of file diff --git a/src/include/Emulator/Execute.h b/src/include/Emulator/Execute.h new file mode 100644 index 0000000..dda039b --- /dev/null +++ b/src/include/Emulator/Execute.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#define NES_START 0x8000 + +class Execute +{ +private: + CPU cpu; + +public: + Execute(CPU cpu); + Execute(); + CPU run(); + std::vector render(); + int reset_clock(); + void log_Cpu(); + void reset(); + void joypad1(Controller button, int isPressed); + void joypad2(Controller button, int isPressed); +}; \ No newline at end of file diff --git a/src/include/Emulator/Instructions.h b/src/include/Emulator/Instructions.h index 22ebd5f..9f0b600 100644 --- a/src/include/Emulator/Instructions.h +++ b/src/include/Emulator/Instructions.h @@ -1,7 +1,5 @@ #include - -#include "Computer.h" - +#include void LDA(AddressMode addressType, CPU &cpu); void LDX(AddressMode addressType, CPU &cpu); void LDY(AddressMode addressType, CPU &cpu); diff --git a/src/include/Emulator/LoadRom.h b/src/include/Emulator/LoadRom.h index 11a5b87..b876d76 100644 --- a/src/include/Emulator/LoadRom.h +++ b/src/include/Emulator/LoadRom.h @@ -1,25 +1,31 @@ +#pragma once + #include #include +#include +#include -#ifndef MIRROR_TYPE_H -#define MIRROR_TYPE_H enum MirrorType { VERTICAL, HORIZONTAL, FOUR_SCREEN, }; -#endif -#ifndef ROM_H -#define ROM_H + +enum ColorEncoding +{ + Pal, + Ntsc +}; + struct Rom { std::vector PRG; std::vector CHR; uint8_t mapper; MirrorType mirror; + ColorEncoding color_encoding; }; -#endif std::vector file_tobyte_vector(std::string file_name); -Rom load_rom(std::vector instructions); \ No newline at end of file +std::optional load_rom(std::vector instructions); \ No newline at end of file diff --git a/src/include/Emulator/PPU.h b/src/include/Emulator/PPU.h index 7279625..dd22162 100644 --- a/src/include/Emulator/PPU.h +++ b/src/include/Emulator/PPU.h @@ -1,3 +1,5 @@ +#pragma once + #include #include // #include @@ -5,13 +7,15 @@ #include "LoadRom.h" #include #include +#include -#ifndef PPU_H -#define PPU_H +#include class PPU { private: - std::tuple getColorFromByte(uint16_t byte); + std::tuple bg_pallete(size_t row, size_t column); + std::tuple getColorFromByte(uint16_t byte, std::tuple pallete); + void get_chr_tile(uint16_t tile_idx, int banks, std::vector &tile_list); // sf::Color getColorFromByte(uint16_t byte); struct Registers { @@ -19,14 +23,15 @@ class PPU { struct { - unsigned B : 1; // color emphasis Blue + unsigned g : 1; // greyscale + unsigned m : 1; // background left column disable + unsigned M : 1; // sprite left column disable + unsigned b : 1; // background enable + + unsigned s : 1; // sprite enable unsigned R : 1; // color emphasis Red unsigned G : 1; // color emphasis Green - unsigned s : 1; // sprite enable - unsigned b : 1; // background enable - unsigned M : 1; // sprite left column disable - unsigned m : 1; // background left column disable - unsigned g : 1; // greyscale + unsigned B : 1; // color emphasis Blue }; uint8_t val; } ppumask; @@ -43,13 +48,17 @@ class PPU { struct { + unsigned N : 2; // increment mode + unsigned I : 1; // increment mode + unsigned S : 1; // sprite tile select (ignored in 8x16 sprite mode) + + unsigned B : 1; // background tile select + + unsigned H : 1; // sprite height + unsigned P : 1; // PPU master/slave + unsigned V : 1; // NMI enable - unsigned H : 1; // sprite height - unsigned B : 1; // background tile select - unsigned S : 1; // sprite tile select (ignored in 8x16 sprite mode) - unsigned I : 1; // increment mode - unsigned N : 2; // increment mode }; uint8_t val; @@ -58,10 +67,11 @@ class PPU { struct { - unsigned padding : 4; + unsigned padding : 5; unsigned O : 1; unsigned S : 1; + // unsigned : 1; unsigned V : 1; }; @@ -83,19 +93,23 @@ class PPU size_t cycles; uint16_t scanline; + void draw_background(std::vector &rgb_ds, int banks, std::tuple res); + void draw_sprites(std::vector &rgb_ds, int banks, std::tuple res); + public: PPU(std::vector chrrom, MirrorType mirrorType); PPU(); uint8_t read_PPU_data(); uint8_t read_OAM_data(); - + std::optional err_string; + std::chrono::high_resolution_clock::time_point start; void write_PPU_address(uint8_t val); void write_PPU_ctrl(uint8_t val); void write_PPU_mask(uint8_t val); std::optional write_PPU_data(uint8_t val); void write_OAM_data(uint8_t val); - void write_OAM_dma(uint8_t val[256]); + void write_OAM_dma(std::vector buffer); void write_OAM_address(uint8_t val); bool NMI_interrupt(uint8_t clock_cycles); bool tick(uint8_t clock_cycles); @@ -104,6 +118,5 @@ class PPU // void render(sf::Texture &texture, int bank, int tile); std::vector render_texture(std::tuple res); -}; - -#endif \ No newline at end of file + void log_ppu(); +}; \ No newline at end of file diff --git a/src/include/Emulator/StatusRegister.h b/src/include/Emulator/StatusRegister.h index 6b302f6..35901ce 100644 --- a/src/include/Emulator/StatusRegister.h +++ b/src/include/Emulator/StatusRegister.h @@ -1,4 +1,4 @@ -#include "Computer.h" +#include #include void set_zero(uint8_t value, CPU &cpu); diff --git a/src/include/Qt/Emulator_Worker.h b/src/include/Qt/Emulator_Worker.h new file mode 100644 index 0000000..6a546b6 --- /dev/null +++ b/src/include/Qt/Emulator_Worker.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include +#include +#include + +#include + +class EmulatorWorker : public QObject +{ + Q_OBJECT + +public: + explicit EmulatorWorker(Rom rom, QString rom_url, QWidget *parent = nullptr); + void shutdown_game(); + void log_cpu(); + void on_start_main_thread(); + int clock_interval() const; + bool is_running() const; +public slots: + void set_clock_interval_speed(float speed); + void on_frame_timer_timeout(); + void on_pause_toggle(bool paused); +signals: + void draw_frame(std::vector vector); + void push_error(QString msg, int error_code); + +private: + bool is_frame_generated; + QChronoTimer *frame_timer; + Execute exe; + QString rom_url; + Rom rom; + bool m_initialized; + bool m_is_running; + int m_clock_interval; + void init(); + +private slots: + void process_cpu(); + void render_frame(); +}; \ No newline at end of file diff --git a/src/include/Qt/Log_Display.h b/src/include/Qt/Log_Display.h new file mode 100644 index 0000000..ec46e6e --- /dev/null +++ b/src/include/Qt/Log_Display.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +class LogNotifier : public QObject +{ + Q_OBJECT +public: + static LogNotifier& instance() + { + static LogNotifier instance; + return instance; + } + LogNotifier(const LogNotifier&) = delete; + LogNotifier& operator=(const LogNotifier&) = delete; +signals: + void log_pushed(QString message); +private: + LogNotifier() + { + } +}; + +class LogDisplay : public QWidget +{ + Q_OBJECT + +public: + explicit LogDisplay(QWidget *parent = nullptr); + +public slots: + void append_line(QString line); + +private: + LogNotifier &log_notifier; + QPlainTextEdit *text_display; + +}; \ No newline at end of file diff --git a/src/include/Qt/filtercontrolframe.h b/src/include/Qt/filtercontrolframe.h new file mode 100644 index 0000000..b0e6852 --- /dev/null +++ b/src/include/Qt/filtercontrolframe.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class FilterControlFrame : public QFrame +{ + Q_OBJECT +public: + explicit FilterControlFrame(QWidget *parent = nullptr); + + friend class MainWindow; // Handles FilterControlFrame +private: + QLineEdit *search_bar; + QCompleter *search_bar_completer; + QFrame *sort_buttons_frame; + QPushButton *sort_ascending_button; + QGroupBox *sort_mode_groupbox; + QPushButton *sort_mode_az; + QPushButton *sort_mode_year; + QPushButton *sort_mode_favorites; + QButtonGroup *sort_mode_button_group; + +private slots: + void update_completer_model(QStringList dirs); +}; \ No newline at end of file diff --git a/src/include/Qt/flowlayout.h b/src/include/Qt/flowlayout.h new file mode 100644 index 0000000..ec5e43f --- /dev/null +++ b/src/include/Qt/flowlayout.h @@ -0,0 +1,39 @@ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#pragma once + +#include +#include +#include +//! [0] +class FlowLayout : public QLayout +{ + Q_OBJECT +public: + explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1); + explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1); + ~FlowLayout(); + + void addItem(QLayoutItem *item) override; + int horizontal_spacing() const; + int vertical_spacing() const; + Qt::Orientations expandingDirections() const override; + bool hasHeightForWidth() const override; + int heightForWidth(int) const override; + int count() const override; + QLayoutItem *itemAt(int index) const override; + QSize minimumSize() const override; + void setGeometry(const QRect &rect) override; + QSize sizeHint() const override; + QLayoutItem *takeAt(int index) override; + +private: + int do_layout(const QRect &rect, bool testOnly) const; + int smart_spacing(QStyle::PixelMetric pm) const; + + QList itemList; + int m_hSpace; + int m_vSpace; +}; +//! [0] diff --git a/src/include/Qt/gamedisplay.h b/src/include/Qt/gamedisplay.h new file mode 100644 index 0000000..663a081 --- /dev/null +++ b/src/include/Qt/gamedisplay.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#include + +class GameDisplay : public QWidget +{ + Q_OBJECT +public: + explicit GameDisplay(Rom rom, QWidget *parent = nullptr, QString rom_url = QString()); + void update_game_scale(); + void center_display(); + bool is_paused() const; + void set_paused(const bool b); + bool initialized() const; + ~GameDisplay(); + +signals: + void pause_toggle(bool paused); + +public slots: + void on_push_error(QString msg, int error_code); + void on_update(std::vector rgb_data_vector); + +private: + void on_init(); + void close_game(); + + QString game_title; + int frame_count; + QTimer *frames_per_sec_timer; + QTimer *time_between_draw_timer; + QScopedPointer render_window; + QScopedPointer crt_shader; + bool m_initialized = false; + sf::Texture texture; + QScopedPointer sprite; + QThread *emu_thread; + std::function draw_func; + EmulatorWorker *emu_worker; + int err_code; + int time_between_draw_ms; + bool m_paused; + bool m_is_emu_on_dif_thread; + +private slots: + void on_crt_shader_changed(const bool b); + void on_framerate_timer_timeout(); + +protected: + void keyPressEvent(QKeyEvent *event) override; + void showEvent(QShowEvent *event) override; + void closeEvent(QCloseEvent *event) override; + QPaintEngine *paintEngine() const override; + void resizeEvent(QResizeEvent *event) override; +}; \ No newline at end of file diff --git a/src/include/Qt/log.h b/src/include/Qt/log.h new file mode 100644 index 0000000..c66241a --- /dev/null +++ b/src/include/Qt/log.h @@ -0,0 +1,17 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void check_log_dir(); +void InitLogs(); +void logToFile(QtMsgType type, const QMessageLogContext &context, const QString &msg); \ No newline at end of file diff --git a/src/include/Qt/mainwindow.h b/src/include/Qt/mainwindow.h new file mode 100644 index 0000000..9478783 --- /dev/null +++ b/src/include/Qt/mainwindow.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { +class MainWindow; +} +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + void update_page_info(); + void create_display(QString rom_link); +public slots: + void sort_mode_button_released(const int id) const; + void sort_order_button_toggled(const bool toggled) const; + void search_bar_edited(QString string) const; + void rom_list_scroll_value_changed(const int value); + void on_gamedisplay_destroyed(); +private: + RomList *rom_list; + FilterControlFrame *sort_control_frame; + QScrollArea *rom_list_scroll; + MenuBar *main_menubar; + QLabel *page_info; +protected: + void wheelEvent(QWheelEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dropEvent(QDropEvent *event) override; + void closeEvent(QCloseEvent *event) override; + +private: + Ui::MainWindow *ui; + +}; \ No newline at end of file diff --git a/src/include/Qt/menubar.h b/src/include/Qt/menubar.h new file mode 100644 index 0000000..f98a5a2 --- /dev/null +++ b/src/include/Qt/menubar.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +class MenuBar : public QMenuBar +{ + Q_OBJECT +public: + explicit MenuBar(QWidget *parent = nullptr); + ~MenuBar(); + +public slots: + void open_rom(); + void open_settings(); + void open_log_display(); + void refresh_recent_roms(QStringList dirs); + +private: + void start_rom(QUrl url); + void start_rom(QString url); + + QMenu *file; + QAction *file_open; + QMenu *file_open_recent; + + QMenu *edit; + QAction *settings_open; + + QMenu *tools; + QAction *log_display_open; + + QMenu *help; + +}; \ No newline at end of file diff --git a/src/include/Qt/romdata.h b/src/include/Qt/romdata.h new file mode 100644 index 0000000..6469ff4 --- /dev/null +++ b/src/include/Qt/romdata.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +class RomData : public QObject +{ + Q_OBJECT +public: + explicit RomData(QObject *parent = nullptr, + uint16_t year = 0, + QByteArray img = QByteArray(), + QString title = QString(), + bool favorited = false, + QUrl path = QUrl("") + ); + ~RomData(); + uint16_t year() const; + void set_year(const uint16_t year); + QString title() const; + void set_title(QString title); + QByteArray img() const; + void set_img(QByteArray img); + bool favorited() const; + void set_favorited(const bool b); + QUrl path() const; + void set_path(QUrl path); + bool is_empty() const; + +private: + uint16_t m_year; + QByteArray m_img; + QString m_title; + bool m_favorited; + QUrl m_path; +}; \ No newline at end of file diff --git a/src/include/Qt/romlist.h b/src/include/Qt/romlist.h new file mode 100644 index 0000000..92e238e --- /dev/null +++ b/src/include/Qt/romlist.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +class RomList : public QWidget +{ + Q_OBJECT +public: + enum SortMode {Year, Favorites, AZ}; + explicit RomList(QWidget *parent = nullptr); + ~RomList(); + void set_current_mode(const SortMode &mode, const bool update = true); + SortMode current_mode() const; + void set_current_order(const Qt::SortOrder order); + Qt::SortOrder current_order() const; + void set_items_per_page(uint32_t i); + void set_current_page(uint32_t i); + uint32_t current_page() const; + uint32_t total_pages() const; + void update_total_pages(); + uint32_t items_per_page() const; + RomData* get_romdata(const int page, const int index); + void search(QString &expr); + void update_display(); +private: + inline static const bool compare_year(const RomData *a, const RomData *b); + inline static const bool compare_favorite(const RomData *a, const RomData *b); + inline static const bool compare_alphabet(const RomData *a, const RomData *b); + inline static const bool compare_regex(const RomData *a, const RomData *b, const QRegularExpression &expr, const SortMode &mode); + void setup_display(); + void cleanup_romdata(); + FlowLayout *main_layout; + QScopedPointer> data; + RomList::SortMode m_current_mode; + Qt::SortOrder m_current_order; + + uint32_t m_current_page; + uint32_t m_total_pages; + uint32_t m_items_per_page; +private slots: + void on_rom_dirs_changed(QStringList dirs); +}; \ No newline at end of file diff --git a/src/include/Qt/romlistitem.h b/src/include/Qt/romlistitem.h new file mode 100644 index 0000000..70557ce --- /dev/null +++ b/src/include/Qt/romlistitem.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +#include + +class RomListItem: public QWidget +{ + Q_OBJECT +public: + explicit RomListItem(RomData *data = nullptr, QWidget *parent = nullptr); + ~RomListItem(); + RomData* romdata() const; + void set_romdata(RomData *data); + +private slots: + void favorite_button_clicked(int checked); + void play_button_clicked(); +private: + void update_data(); + + QLabel *title; + QLabel *year; + QPushButton *play; + QFrame *buttons_frame; + QPushButton *favorite_button; + RomData* m_romdata; + +signals: +}; \ No newline at end of file diff --git a/src/include/Qt/settingsdisplay.h b/src/include/Qt/settingsdisplay.h new file mode 100644 index 0000000..5438016 --- /dev/null +++ b/src/include/Qt/settingsdisplay.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class GeneralSettingsDisplay : public QWidget +{ + Q_OBJECT + +public: + explicit GeneralSettingsDisplay(QWidget *parent = nullptr); + ~GeneralSettingsDisplay(); + + friend class SettingsDisplay; + +private: + QGroupBox *general_groupbox; + QCheckBox *min_on_game_start_checkbox; + + QGroupBox *directory_groupbox; + QPlainTextEdit *directories; + QPushButton *add_directory; + +}; + +class EmulatorSettingsDisplay : public QWidget +{ + Q_OBJECT + +public: + explicit EmulatorSettingsDisplay(QWidget *parent = nullptr); + ~EmulatorSettingsDisplay(); + + friend class SettingsDisplay; + +private: + QGroupBox *emulator_groupbox; + QGroupBox *emu_speed_groupbox; + QComboBox *speed_combobox; + QCheckBox *threaded_checkbox; + QCheckBox *crt_shader_checkbox; + +}; + +class About : public QWidget +{ + Q_OBJECT + +public: + explicit About(QWidget *parent = nullptr); + ~About(); + + friend class SettingsDisplay; + +private: + QLabel *text; + +}; + +class SettingsDisplay : public QStackedWidget +{ + Q_OBJECT + +public: + explicit SettingsDisplay(QWidget *parent = nullptr); + ~SettingsDisplay(); + +private: + GeneralSettingsDisplay *general; + EmulatorSettingsDisplay *emulator; + About *about; + +private slots: + void on_add_directory_clicked(); + void on_min_gui_on_start_checkbox_toggled(const bool toggled); + void on_speed_index_changed(const int idx); + +}; \ No newline at end of file diff --git a/src/include/Qt/settingsmanager.h b/src/include/Qt/settingsmanager.h new file mode 100644 index 0000000..b8ba698 --- /dev/null +++ b/src/include/Qt/settingsmanager.h @@ -0,0 +1,62 @@ +#pragma once + +#include +#include + +#include +#include + +class SettingsManager : public QObject +{ + Q_OBJECT + +public: + + static SettingsManager& instance() + { + static SettingsManager instance; + return instance; + } + + SettingsManager(const SettingsManager&) = delete; + SettingsManager& operator=(const SettingsManager&) = delete; + + QStringList get_rom_dirs(); + void set_rom_dir(QStringList dir); + void set_rom_dir(QString dir); + + void set_save_dir(QString dir); + QString save_dir() const; + + bool minimize_gui_on_game_start() const; + void set_minimize_gui_on_game_start(const bool b); + + float speed() const; + void set_speed(const float speed); + + bool run_emulator_on_seperate_thread() const; + void set_run_emulator_on_seperate_thread(bool b); + + RomList::SortMode sort_mode() const; + void set_sort_mode(const RomList::SortMode sort_mode); + + Qt::SortOrder ascending_order() const; + void set_ascending_order(const Qt::SortOrder sort_order); + + bool crt_shader() const; + void set_crt_shader(bool b); + + QStringList get_recent_roms(); + void append_recent_roms(QString dir); + void set_recent_roms(QStringList dirs); + +signals: + void crt_shader_changed(bool on); + void speed_changed(float speed); + void recent_roms_changed(QStringList dirs); + void rom_dirs_changed(QStringList dirs); + +private: + SettingsManager(); + QSettings m_settings = QSettings(SAVE_DIR, QSettings::IniFormat, this); +}; \ No newline at end of file diff --git a/src/include/Qt/settingswidget.h b/src/include/Qt/settingswidget.h new file mode 100644 index 0000000..2bd870d --- /dev/null +++ b/src/include/Qt/settingswidget.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +//class SettingsDisplay; // TODO: Foward declaration, prevents "'SettingsDisplay' does not name a type" error +class SettingsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit SettingsWidget(QWidget *parent = nullptr); + ~SettingsWidget(); + +private: + QListWidget *setting_category; + SettingsDisplay *setting_display; + QFrame *controls_frame; + QPushButton *apply_changes; + QPushButton *cancel_changes; + QStatusBar *button_statusbar; + +private slots: + void on_setting_category_item_clicked(const QListWidgetItem *item); + void on_apply_changes_clicked(); + void on_cancel_changes_clicked(); + +protected: + void closeEvent(QCloseEvent *event) override; + +}; \ No newline at end of file diff --git a/src/include/Qt/util.h b/src/include/Qt/util.h new file mode 100644 index 0000000..acd2b31 --- /dev/null +++ b/src/include/Qt/util.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +#include +#include + +#define SAVE_DIR "./save/config.cfg" + +inline std::string num_to_hexa(uint16_t num) +{ + return QString::number(num, 16).toUpper().toStdString(); +} + +// inline QString num_to_hexa(uint8_t num) +// { +// return QString::number(num, 8).toUpper(); +// } + +inline bool is_a_game_running() +{ + + for (auto &widget : qApp->topLevelWidgets()) + { + if (widget->inherits("GameDisplay")) + { + if (qobject_cast(widget)->initialized()) + { + return true; + } + } + } + + // Code reaches this point if all game display objects are not initialized + return false; +} + +inline void start_game(QString path) +{ + if (is_a_game_running()) + { + qInfo() << "Can't open game while a game is already running"; + return; + } + for (auto &widget : qApp->topLevelWidgets()) + { + if (widget->inherits("MainWindow")) + { + qDebug() << "File path:" << path; + qobject_cast(widget)->create_display(path); + break; + } + } +} \ No newline at end of file diff --git a/src/include/Qt/utilemulator.h b/src/include/Qt/utilemulator.h new file mode 100644 index 0000000..d580f21 --- /dev/null +++ b/src/include/Qt/utilemulator.h @@ -0,0 +1,22 @@ +#include + +// TODO: Program is hard-coded to work only for NTSC, make a way to use both +constexpr const float ntsc_frame_rate = 60.0f; +constexpr const float pal_frame_rate = 50.0f; + +constexpr const float frame_interval_ns = 16666666.67f; + +inline std::chrono::nanoseconds framerate_to_ns(const float frame_rate) +{ + const auto ms = std::chrono::duration(1.0 / static_cast(frame_rate) * 1000.0); + const auto ns = std::chrono::duration_cast(ms); + + // qDebug() << "frame_rate:" << frame_rate << "ms:" << ms.count() << "ns:" << ns.count(); + return ns; +} + +inline int speed_percent(const int frame_count, const float frame_rate) +{ + return static_cast( + static_cast(frame_count) / frame_rate * 100.f); +} \ No newline at end of file