Compare commits
3 commits
main
...
stinky-deb
Author | SHA1 | Date | |
---|---|---|---|
Alex Janka | e2bbd98f2d | ||
Alex Janka | 37fbdb0ed0 | ||
Alex Janka | 0636cb7dc3 |
6
.cargo/config
Normal file
6
.cargo/config
Normal file
|
@ -0,0 +1,6 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --release --"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
lto = "off"
|
|
@ -1,11 +0,0 @@
|
|||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
lto = false
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -2,5 +2,4 @@
|
|||
/test-roms
|
||||
/bootrom
|
||||
/wavs
|
||||
/profiles
|
||||
/lib/shaders
|
||||
/profiles
|
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
[submodule "vendored/nih-plug"]
|
||||
path = vendored/nih-plug
|
||||
url = https://github.com/italicsjenga/nih-plug
|
||||
branch = raw-window-handle-0.5.0
|
||||
[submodule "vendored/baseview"]
|
||||
path = vendored/baseview
|
||||
url = https://github.com/italicsjenga/baseview
|
||||
branch = raw-window-handle-0.5.0
|
||||
[submodule "vendored/rust_minifb"]
|
||||
path = vendored/rust_minifb
|
||||
url = https://github.com/italicsjenga/rust_minifb
|
||||
branch = raw-window-handle-0.5.0
|
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"files.associations": {
|
||||
"*.slang": "txt"
|
||||
},
|
||||
}
|
4645
Cargo.lock
generated
4645
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
34
Cargo.toml
34
Cargo.toml
|
@ -1,31 +1,5 @@
|
|||
[workspace]
|
||||
members = ["lib", "frontend-common", "gb-vst", "xtask", "gui", "cli"]
|
||||
default-members = ["cli"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
log = "0.4.22"
|
||||
env_logger = "0.11.5"
|
||||
thiserror = "1.0.63"
|
||||
raw-window-handle = "0.6.2"
|
||||
gb-emu-lib = { path = "./lib", features = ["config"] }
|
||||
frontend-common = { path = "./frontend-common" }
|
||||
baseview = { git = "https://git.alexjanka.com/alex/baseview", default-features = false }
|
||||
nih_plug = { git = "https://git.alexjanka.com/alex/nih-plug" }
|
||||
nih_plug_xtask = { git = "https://git.alexjanka.com/alex/nih-plug" }
|
||||
librashader = { git = "https://git.alexjanka.com/alex/librashader", default-features = false }
|
||||
librashader-common = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-presets = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-preprocess = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-reflect = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-runtime = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-runtime-vk = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
librashader-cache = { git = "https://git.alexjanka.com/alex/librashader" }
|
||||
ash = "0.38.0"
|
||||
ash-window = "0.13.0"
|
||||
|
||||
[patch."https://github.com/RustAudio/baseview.git"]
|
||||
baseview = { git = "https://git.alexjanka.com/alex/baseview" }
|
||||
|
||||
[patch.crates-io]
|
||||
anymap = { git = "https://git.alexjanka.com/alex/anymap" }
|
||||
members = ["lib", "gb-emu", "gb-vst", "gb-vst/xtask"]
|
||||
default-members = ["gb-emu"]
|
||||
exclude = ["./vendored"]
|
||||
resolver = "2"
|
674
LICENSE
674
LICENSE
|
@ -1,674 +0,0 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
|
@ -1,4 +0,0 @@
|
|||
### Config directories
|
||||
* Windows - %APPDATA%\Local\alexjanka\TWINC
|
||||
* Linux - ~/.config/TWINC
|
||||
* macOS - ~/Library/Application Support/com.alexjanka.TWINC
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "TWINC Game Boy (CGB/DMG) emulator CLI"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "com.alexjanka.TWINC.cli"
|
||||
|
||||
[features]
|
||||
default = ["wgpu"]
|
||||
wgpu = ["frontend-common/wgpu"]
|
||||
pixels = ["frontend-common/pixels"]
|
||||
vulkan = ["frontend-common/vulkan"]
|
||||
|
||||
[dependencies]
|
||||
frontend-common = { workspace = true }
|
||||
gb-emu-lib = { workspace = true }
|
||||
clap = { version = "4.5.15", features = ["derive"] }
|
||||
ctrlc = "3.4.4"
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
anyhow = { version = "1.0.86", features = ["backtrace"] }
|
205
cli/src/main.rs
205
cli/src/main.rs
|
@ -1,205 +0,0 @@
|
|||
#![feature(let_chains, if_let_guard, iter_array_chunks)]
|
||||
|
||||
use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
|
||||
use frontend_common::{audio, debug::Debugger, window::ActiveWindowManager};
|
||||
use gb_emu_lib::{
|
||||
config::{ConfigManager, CONFIG_MANAGER},
|
||||
connect::{EmulatorCoreTrait, EmulatorMessage, SerialTarget, SramType, StdoutType},
|
||||
};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::mpsc::channel,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Subcommand, Debug, Clone, Copy)]
|
||||
enum Commands {
|
||||
PrintConfig {
|
||||
/// Which config to print
|
||||
#[arg(long, value_enum, default_value_t = ConfigType::Base)]
|
||||
config: ConfigType,
|
||||
|
||||
/// Print current config instead of default
|
||||
#[arg(long)]
|
||||
current: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
enum ConfigType {
|
||||
Base,
|
||||
Standalone,
|
||||
}
|
||||
|
||||
#[derive(ValueEnum, Debug, Clone, Copy)]
|
||||
enum SerialTargetOption {
|
||||
None,
|
||||
Ascii,
|
||||
Hex,
|
||||
}
|
||||
|
||||
impl From<SerialTargetOption> for SerialTarget {
|
||||
fn from(value: SerialTargetOption) -> Self {
|
||||
match value {
|
||||
SerialTargetOption::None => Self::None,
|
||||
SerialTargetOption::Ascii => Self::Stdout(StdoutType::Ascii),
|
||||
SerialTargetOption::Hex => Self::Stdout(StdoutType::Hex),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gameboy (DMG/CGB) emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(group(ArgGroup::new("saves").args(["save","no_save"])))]
|
||||
#[command(subcommand_negates_reqs = true)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
|
||||
/// Path to ROM
|
||||
#[arg(value_hint = clap::ValueHint::FilePath, required = true)]
|
||||
rom: Option<PathBuf>,
|
||||
|
||||
/// Save path
|
||||
#[arg(long, value_hint = clap::ValueHint::FilePath)]
|
||||
save: Option<PathBuf>,
|
||||
|
||||
/// Skip save file
|
||||
#[arg(long)]
|
||||
no_save: bool,
|
||||
|
||||
/// Output link port to stdout as either ASCII or hex
|
||||
#[arg(long, value_enum, default_value_t = SerialTargetOption::None)]
|
||||
serial: SerialTargetOption,
|
||||
|
||||
/// Show tile window
|
||||
#[arg(long)]
|
||||
tile_window: bool,
|
||||
|
||||
/// Show layer window
|
||||
#[arg(long)]
|
||||
layer_window: bool,
|
||||
|
||||
/// Mute audio
|
||||
#[arg(long)]
|
||||
mute: bool,
|
||||
|
||||
/// Run debug console
|
||||
#[arg(long)]
|
||||
debug: bool,
|
||||
// /// Use webcam as Pocket Camera emulation
|
||||
// #[arg(short, long)]
|
||||
// camera: bool,
|
||||
/// Record frames to image sequence
|
||||
#[arg(long)]
|
||||
record: bool,
|
||||
}
|
||||
|
||||
impl From<Args> for frontend_common::RunOptions {
|
||||
fn from(value: Args) -> Self {
|
||||
Self {
|
||||
rom: value
|
||||
.rom
|
||||
.expect("error with clap - this shouldn't be possible!"),
|
||||
save: if value.no_save {
|
||||
SramType::None
|
||||
} else {
|
||||
match value.save {
|
||||
Some(path) => SramType::File(path),
|
||||
None => SramType::Auto,
|
||||
}
|
||||
},
|
||||
serial: value.serial.into(),
|
||||
tile_window: value.tile_window,
|
||||
layer_window: value.layer_window,
|
||||
mute: value.mute,
|
||||
debug: value.debug,
|
||||
record: value.record,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
unsafe {
|
||||
if std::env::var_os("RUST_BACKTRACE").is_none() {
|
||||
std::env::set_var("RUST_BACKTRACE", "1");
|
||||
}
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "cli=info,frontend_common=info,gb_emu_lib=info");
|
||||
}
|
||||
}
|
||||
|
||||
env_logger::init();
|
||||
let args = Args::parse();
|
||||
|
||||
if let Some(subcommand) = args.command {
|
||||
match subcommand {
|
||||
Commands::PrintConfig { config, current } => {
|
||||
if let Some(string) = if current {
|
||||
match config {
|
||||
ConfigType::Base => ConfigManager::get_custom_config_string(
|
||||
CONFIG_MANAGER.load_or_create_base_config(),
|
||||
)
|
||||
.ok(),
|
||||
ConfigType::Standalone => CONFIG_MANAGER
|
||||
.load_custom_config::<frontend_common::StandaloneConfig>()
|
||||
.and_then(|v| ConfigManager::get_custom_config_string(v).ok()),
|
||||
}
|
||||
} else {
|
||||
match config {
|
||||
ConfigType::Base => ConfigManager::get_custom_config_string(
|
||||
gb_emu_lib::config::Config::default(),
|
||||
)
|
||||
.ok(),
|
||||
ConfigType::Standalone => ConfigManager::get_custom_config_string(
|
||||
frontend_common::StandaloneConfig::default(),
|
||||
)
|
||||
.ok(),
|
||||
}
|
||||
} {
|
||||
println!("{string}");
|
||||
} else {
|
||||
log::error!("Error getting config string");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
|
||||
|
||||
{
|
||||
let sender = sender.clone();
|
||||
ctrlc::set_handler(move || {
|
||||
sender.send(EmulatorMessage::Exit).unwrap();
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let (record, mute, debug) = (args.record, args.mute, args.debug);
|
||||
let prepared = frontend_common::prepare(args.into(), receiver);
|
||||
let (output, stream) = audio::create_output(mute);
|
||||
let mut window_manager = ActiveWindowManager::new(sender, stream, record);
|
||||
let mut core = frontend_common::run(prepared, &mut window_manager, output)?;
|
||||
if debug {
|
||||
let mut debugger = Debugger::new(Box::new(core));
|
||||
let mut since = Instant::now();
|
||||
loop {
|
||||
if since.elapsed() >= UPDATE_INTERVAL {
|
||||
window_manager.update_events();
|
||||
since = Instant::now();
|
||||
}
|
||||
debugger.step();
|
||||
}
|
||||
} else {
|
||||
std::thread::spawn(move || loop {
|
||||
if core.run(100) {
|
||||
exit(0);
|
||||
}
|
||||
});
|
||||
window_manager.run_events_blocking().unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const UPDATE_INTERVAL: Duration = Duration::from_millis(1);
|
|
@ -1,28 +0,0 @@
|
|||
[package]
|
||||
name = "frontend-common"
|
||||
version = "0.5.1"
|
||||
edition = "2021"
|
||||
description = "Frontend common library for TWINC Game Boy (CGB/DMG) emulator"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
pixels = ["gb-emu-lib/pixels-renderer"]
|
||||
vulkan = ["gb-emu-lib/vulkan-renderer", "gb-emu-lib/vulkan-debug"]
|
||||
vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"]
|
||||
wgpu = ["gb-emu-lib/wgpu-renderer"]
|
||||
|
||||
[dependencies]
|
||||
gb-emu-lib = { workspace = true }
|
||||
gilrs = "0.10.9"
|
||||
cpal = { version = "0.15.3", features = ["jack"] }
|
||||
futures = "0.3.30"
|
||||
send_wrapper = { version = "0.6.0", optional = true }
|
||||
winit = { version = "0.29.15", features = ["rwh_05"] }
|
||||
winit_input_helper = "0.16.0"
|
||||
raw-window-handle = { workspace = true }
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
image = { version = "0.25.2", default-features = false, features = ["png"] }
|
||||
bytemuck = "1.16.3"
|
||||
chrono = "0.4.38"
|
||||
log = { workspace = true }
|
||||
anyhow = "1.0.86"
|
|
@ -1,87 +0,0 @@
|
|||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
Stream,
|
||||
};
|
||||
use futures::executor;
|
||||
use gb_emu_lib::connect::{AsyncConsumer, AudioOutput, DownsampleType};
|
||||
|
||||
use crate::access_config;
|
||||
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold;
|
||||
|
||||
pub fn create_output(muted: bool) -> (AudioOutput, Stream) {
|
||||
#[cfg(target_os = "linux")]
|
||||
let host = cpal::host_from_id(cpal::HostId::Jack).unwrap_or_else(|_| cpal::default_host());
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("no output device available");
|
||||
|
||||
let config = device
|
||||
.default_output_config()
|
||||
.expect("Couldn't get default config for audio output");
|
||||
|
||||
let (buffers_per_frame, default_buffer_size) = {
|
||||
let configs = access_config();
|
||||
(
|
||||
configs.standalone_config.buffers_per_frame,
|
||||
configs.standalone_config.output_buffer_size,
|
||||
)
|
||||
};
|
||||
|
||||
let sample_rate = config.sample_rate().0;
|
||||
|
||||
let mut stream_config = config.config();
|
||||
|
||||
if let cpal::SupportedBufferSize::Range { min, max } = config.buffer_size() {
|
||||
stream_config.buffer_size =
|
||||
cpal::BufferSize::Fixed(default_buffer_size.min(*max).max(*min));
|
||||
}
|
||||
|
||||
log::info!("Using buffer size {:?}", stream_config.buffer_size);
|
||||
|
||||
let (output, mut rx) = AudioOutput::new(sample_rate as f32, buffers_per_frame, DOWNSAMPLE_TYPE);
|
||||
|
||||
let stream = if muted {
|
||||
device
|
||||
.build_output_stream(
|
||||
&stream_config,
|
||||
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
for _ in data.chunks_exact_mut(2) {
|
||||
match executor::block_on(rx.pop()) {
|
||||
Some(_) => {}
|
||||
None => panic!("Audio queue disconnected!"),
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
log::error!("audio error: {err}");
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
} else {
|
||||
device
|
||||
.build_output_stream(
|
||||
&config.config(),
|
||||
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
for v in data.chunks_exact_mut(2) {
|
||||
if let Some(a) = executor::block_on(rx.pop()) {
|
||||
v.copy_from_slice(&a);
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
log::error!("audio error: {err}");
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
stream.play().unwrap();
|
||||
|
||||
(output, stream)
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
use gb_emu_lib::connect::PocketCamera;
|
||||
use nokhwa::{
|
||||
pixel_format,
|
||||
utils::{CameraIndex, RequestedFormat, RequestedFormatType},
|
||||
Camera,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
|
||||
pub struct Webcam {
|
||||
camera: SendWrapper<Camera>,
|
||||
scaled_buffer: [u8; 128 * 128],
|
||||
}
|
||||
|
||||
impl Webcam {
|
||||
pub fn new() -> Self {
|
||||
let format = RequestedFormat::new::<pixel_format::RgbFormat>(
|
||||
RequestedFormatType::AbsoluteHighestResolution,
|
||||
);
|
||||
|
||||
let mut camera = SendWrapper::new(
|
||||
Camera::new(CameraIndex::Index(0), format).expect("Couldn't open camera"),
|
||||
);
|
||||
camera
|
||||
.set_frame_format(nokhwa::utils::FrameFormat::YUYV)
|
||||
.expect("couldnt set frame format to yuyv");
|
||||
|
||||
if let Ok(formats) = camera.compatible_fourcc() {
|
||||
if formats.is_empty() {
|
||||
println!("no compatible frame formats listed");
|
||||
}
|
||||
for f in formats {
|
||||
println!("compatible frame format: {f:?}");
|
||||
}
|
||||
} else {
|
||||
println!("couldnt get frame formats")
|
||||
}
|
||||
|
||||
println!("current camera format: {:?}", camera.camera_format());
|
||||
|
||||
if let Ok(formats) = camera.compatible_camera_formats() {
|
||||
if formats.is_empty() {
|
||||
println!("camera formats is empty");
|
||||
}
|
||||
for f in formats {
|
||||
println!("supports camera format {f:?}");
|
||||
}
|
||||
} else {
|
||||
println!("couldnt get camera formats");
|
||||
}
|
||||
|
||||
Self {
|
||||
camera,
|
||||
scaled_buffer: [0; 128 * 128],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PocketCamera for Webcam {
|
||||
fn get_image(&mut self) -> [u8; 128 * 128] {
|
||||
self.scaled_buffer
|
||||
}
|
||||
|
||||
fn begin_capture(&mut self) {
|
||||
let _height = self.camera.resolution().height() as usize;
|
||||
let width = self.camera.resolution().width() as usize;
|
||||
// let frame = self.camera.frame_raw().expect("couldn't get frame");
|
||||
self.camera
|
||||
.set_frame_format(nokhwa::utils::FrameFormat::RAWRGB)
|
||||
.unwrap();
|
||||
let frame = self.camera.frame().expect("couldn't get frame");
|
||||
println!("source format: {:?}", frame.source_frame_format());
|
||||
|
||||
let decoded = frame
|
||||
.decode_image::<pixel_format::RgbFormat>()
|
||||
.expect("couldn't decode image");
|
||||
let pixels = decoded.pixels().collect::<Vec<_>>();
|
||||
|
||||
for y in 0..128 {
|
||||
for x in 0..128 {
|
||||
self.scaled_buffer[y * 128 + x] = pixels[((y * width) + x) * 2][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
self.camera.open_stream().expect("couldn't open stream");
|
||||
self.camera
|
||||
.set_frame_format(nokhwa::utils::FrameFormat::YUYV)
|
||||
.unwrap();
|
||||
|
||||
println!(
|
||||
"opened stream! current format: {:?}",
|
||||
self.camera.camera_format()
|
||||
);
|
||||
// let format = RequestedFormat::new::<pixel_format::LumaFormat>(
|
||||
// RequestedFormatType::AbsoluteHighestResolution,
|
||||
// );
|
||||
// self.camera
|
||||
// .set_camera_requset(format)
|
||||
// .expect("couldn't set format again");
|
||||
}
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
use gb_emu_lib::connect::EmulatorCoreTrait;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::{self, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
pub enum CommandErr {
|
||||
InvalidCommand,
|
||||
InvalidArgument,
|
||||
}
|
||||
|
||||
pub enum Commands {
|
||||
Watch(u16),
|
||||
Break(u16),
|
||||
Step,
|
||||
Continue,
|
||||
}
|
||||
|
||||
impl FromStr for Commands {
|
||||
type Err = CommandErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let trimmed = s.trim().to_lowercase();
|
||||
let words: Vec<&str> = trimmed.split_whitespace().collect();
|
||||
|
||||
match words.as_slice() {
|
||||
["step"] => Ok(Self::Step),
|
||||
["continue"] => Ok(Self::Continue),
|
||||
["watch", addr] => {
|
||||
if let Ok(addr) = u16::from_str_radix(addr.trim().trim_start_matches("0x"), 16) {
|
||||
Ok(Self::Watch(addr))
|
||||
} else {
|
||||
Err(CommandErr::InvalidArgument)
|
||||
}
|
||||
}
|
||||
["break", addr] => {
|
||||
if let Ok(addr) = u16::from_str_radix(addr.trim().trim_start_matches("0x"), 16) {
|
||||
Ok(Self::Break(addr))
|
||||
} else {
|
||||
Err(CommandErr::InvalidArgument)
|
||||
}
|
||||
}
|
||||
_ => Err(CommandErr::InvalidCommand),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Debugger {
|
||||
core: Box<dyn EmulatorCoreTrait>,
|
||||
stepping: bool,
|
||||
last_command: String,
|
||||
watches: HashMap<u16, u8>,
|
||||
breakpoints: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Debugger {
|
||||
pub fn new(core: Box<dyn EmulatorCoreTrait>) -> Self {
|
||||
Self {
|
||||
core,
|
||||
stepping: true,
|
||||
last_command: String::from(""),
|
||||
watches: HashMap::new(),
|
||||
breakpoints: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step(&mut self) {
|
||||
self.core.process_messages();
|
||||
if self.should_pause() {
|
||||
println!("cycles: {}", self.core.cycle_count());
|
||||
println!();
|
||||
println!("{}", self.core.print_reg());
|
||||
println!();
|
||||
print!(">");
|
||||
io::stdout().flush().unwrap();
|
||||
let mut line = String::new();
|
||||
line = match io::stdin().read_line(&mut line) {
|
||||
Ok(_) => line,
|
||||
Err(_) => String::from(""),
|
||||
};
|
||||
if line.trim().is_empty() {
|
||||
line = self.last_command.clone();
|
||||
} else {
|
||||
self.last_command = line.clone();
|
||||
}
|
||||
if let Ok(command) = Commands::from_str(&line) {
|
||||
match command {
|
||||
Commands::Watch(address) => {
|
||||
println!("watching {address:#X}");
|
||||
self.watches.insert(address, self.core.get_memory(address));
|
||||
return;
|
||||
}
|
||||
Commands::Step => self.stepping = true,
|
||||
Commands::Continue => self.stepping = false,
|
||||
Commands::Break(address) => {
|
||||
if !self.breakpoints.contains(&address) {
|
||||
self.breakpoints.push(address)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Invalid command");
|
||||
self.step();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.core.run(1);
|
||||
}
|
||||
|
||||
fn should_pause(&mut self) -> bool {
|
||||
let mut should_pause = self.stepping;
|
||||
for (address, data) in &mut self.watches {
|
||||
let new_data = self.core.get_memory(*address);
|
||||
if new_data != *data {
|
||||
should_pause = true;
|
||||
println!("Memory at 0x{address:0>4X} changed:");
|
||||
println!(" from 0b{0:0>8b}/0x{0:0>2X}", *data);
|
||||
println!(" to 0b{0:0>8b}/0x{0:0>2X}", new_data);
|
||||
*data = new_data;
|
||||
}
|
||||
}
|
||||
for address in &self.breakpoints {
|
||||
if self.core.pc() == *address {
|
||||
println!("Breakpoint at 0x{address:0>4X} reached");
|
||||
should_pause = true;
|
||||
}
|
||||
}
|
||||
should_pause
|
||||
}
|
||||
}
|
|
@ -1,249 +0,0 @@
|
|||
#![feature(let_chains, if_let_guard, iter_array_chunks)]
|
||||
|
||||
#[cfg(feature = "camera")]
|
||||
use camera::Webcam;
|
||||
|
||||
use gb_emu_lib::{
|
||||
config::{NamedConfig, CONFIG_MANAGER},
|
||||
connect::{
|
||||
AudioOutput, CgbRomType, EmulatorMessage, EmulatorOptions, Rom, RomFile, SerialTarget,
|
||||
SramType,
|
||||
},
|
||||
EmulatorCore,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{mpsc::Receiver, OnceLock},
|
||||
};
|
||||
use window::{RendererChannel, WindowManager, WindowType};
|
||||
|
||||
pub mod audio;
|
||||
#[cfg(feature = "camera")]
|
||||
mod camera;
|
||||
pub mod debug;
|
||||
pub mod window;
|
||||
|
||||
#[cfg(all(
|
||||
not(feature = "vulkan"),
|
||||
not(feature = "pixels"),
|
||||
not(feature = "wgpu")
|
||||
))]
|
||||
compile_error!("select one rendering backend!");
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct StandaloneConfig {
|
||||
pub scale_factor: usize,
|
||||
pub group_screenshots_by_rom: bool,
|
||||
pub buffers_per_frame: usize,
|
||||
pub output_buffer_size: u32,
|
||||
}
|
||||
|
||||
impl NamedConfig for StandaloneConfig {
|
||||
fn name() -> String {
|
||||
String::from("standalone")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StandaloneConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale_factor: 3,
|
||||
group_screenshots_by_rom: true,
|
||||
buffers_per_frame: 5,
|
||||
output_buffer_size: 64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Configs {
|
||||
standalone_config: StandaloneConfig,
|
||||
emu_config: gb_emu_lib::config::Config,
|
||||
config_dir: PathBuf,
|
||||
rom_title: String,
|
||||
}
|
||||
|
||||
static CONFIGS: OnceLock<Configs> = OnceLock::new();
|
||||
|
||||
fn access_config<'a>() -> &'a Configs {
|
||||
CONFIGS.get().expect("accessed config before it was set!")
|
||||
}
|
||||
|
||||
pub struct RunOptions {
|
||||
pub rom: PathBuf,
|
||||
pub save: SramType,
|
||||
pub serial: SerialTarget,
|
||||
pub tile_window: bool,
|
||||
pub layer_window: bool,
|
||||
pub mute: bool,
|
||||
pub debug: bool,
|
||||
pub record: bool,
|
||||
}
|
||||
|
||||
impl RunOptions {
|
||||
pub fn new(rom: PathBuf) -> Self {
|
||||
Self {
|
||||
rom,
|
||||
save: SramType::Auto,
|
||||
serial: SerialTarget::None,
|
||||
tile_window: false,
|
||||
layer_window: false,
|
||||
mute: false,
|
||||
debug: false,
|
||||
record: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PreparedEmulator {
|
||||
scale_override: usize,
|
||||
shader_path: Option<PathBuf>,
|
||||
resizable: bool,
|
||||
rom: Rom,
|
||||
receiver: Receiver<EmulatorMessage<[u8; 4]>>,
|
||||
serial: SerialTarget,
|
||||
tile_window: bool,
|
||||
layer_window: bool,
|
||||
}
|
||||
|
||||
pub fn prepare(
|
||||
options: RunOptions,
|
||||
receiver: Receiver<EmulatorMessage<[u8; 4]>>,
|
||||
) -> PreparedEmulator {
|
||||
let config = CONFIG_MANAGER.load_or_create_base_config();
|
||||
let standalone_config: StandaloneConfig = CONFIG_MANAGER.load_or_create_config();
|
||||
|
||||
let rom_file = RomFile::Path(options.rom);
|
||||
|
||||
let rom = rom_file.load(options.save).expect("Error parsing rom");
|
||||
|
||||
let configs = CONFIGS.get_or_init(|| Configs {
|
||||
standalone_config,
|
||||
emu_config: config.clone(),
|
||||
config_dir: CONFIG_MANAGER.dir(),
|
||||
rom_title: rom.get_title().to_owned(),
|
||||
});
|
||||
|
||||
let will_be_cgb = rom.rom_type == CgbRomType::CgbOnly || config.prefer_cgb;
|
||||
|
||||
let shader_path = if will_be_cgb {
|
||||
config.vulkan_config.cgb_shader_path.as_ref()
|
||||
} else {
|
||||
config.vulkan_config.dmg_shader_path.as_ref()
|
||||
}
|
||||
.map(|v| CONFIG_MANAGER.dir().join(v));
|
||||
|
||||
let resizable = shader_path.is_some()
|
||||
&& if will_be_cgb {
|
||||
config.vulkan_config.cgb_shader_resizable
|
||||
} else {
|
||||
config.vulkan_config.dmg_shader_resizable
|
||||
};
|
||||
|
||||
let scale_override = match if will_be_cgb {
|
||||
config.vulkan_config.cgb_resolution_override
|
||||
} else {
|
||||
config.vulkan_config.dmg_resolution_override
|
||||
} {
|
||||
gb_emu_lib::config::ResolutionOverride::Scale(scale) => Some(scale),
|
||||
gb_emu_lib::config::ResolutionOverride::Default => None,
|
||||
}
|
||||
.unwrap_or(configs.standalone_config.scale_factor);
|
||||
|
||||
PreparedEmulator {
|
||||
scale_override,
|
||||
shader_path,
|
||||
resizable,
|
||||
rom,
|
||||
receiver,
|
||||
serial: options.serial,
|
||||
tile_window: options.tile_window,
|
||||
layer_window: options.layer_window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run<W>(
|
||||
prepared: PreparedEmulator,
|
||||
window_manager: &mut W,
|
||||
output: AudioOutput,
|
||||
) -> anyhow::Result<EmulatorCore<[u8; 4]>>
|
||||
where
|
||||
W: WindowManager,
|
||||
{
|
||||
let configs = access_config();
|
||||
|
||||
let window = window_manager.add(
|
||||
WindowType::Main,
|
||||
prepared.scale_override,
|
||||
prepared.shader_path,
|
||||
prepared.resizable,
|
||||
)?;
|
||||
|
||||
let tile_window = if prepared.tile_window {
|
||||
Some(new_tile_window(window_manager)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let layer_window = if prepared.layer_window {
|
||||
Some(new_layer_window(window_manager)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let emulator_options = EmulatorOptions::new_with_config(
|
||||
configs.emu_config.clone(),
|
||||
configs.config_dir.clone(),
|
||||
Some(window),
|
||||
prepared.rom,
|
||||
output,
|
||||
)
|
||||
.with_serial_target(prepared.serial)
|
||||
.with_tile_window(tile_window)
|
||||
.with_layer_window(layer_window);
|
||||
|
||||
// let core: Box<dyn EmulatorCoreTrait> = if args.camera {
|
||||
// Box::new(EmulatorCore::init(receiver, options, Webcam::new()))
|
||||
// } else {
|
||||
// Box::new(EmulatorCore::init(receiver, options, NoCamera::default()))
|
||||
// };
|
||||
|
||||
// #[cfg(not(feature = "camera"))]
|
||||
// let core: Box<dyn EmulatorCoreTrait> =
|
||||
// Box::new(EmulatorCore::init(receiver, options, camera));
|
||||
// #[cfg(feature = "camera")]
|
||||
// let core = Box::new(EmulatorCore::init(receiver, options, Webcam::new()));
|
||||
|
||||
// let emu = if args.debug {
|
||||
// EmulatorTypes::Debug(Debugger::new(core))
|
||||
// } else {
|
||||
// EmulatorTypes::Normal(core)
|
||||
// };
|
||||
|
||||
EmulatorCore::init(true, prepared.receiver, emulator_options)
|
||||
}
|
||||
|
||||
pub fn new_tile_window<W>(window_manager: &mut W) -> anyhow::Result<RendererChannel>
|
||||
where
|
||||
W: WindowManager,
|
||||
{
|
||||
window_manager.add(
|
||||
WindowType::Tile,
|
||||
access_config().standalone_config.scale_factor,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_layer_window<W>(window_manager: &mut W) -> anyhow::Result<RendererChannel>
|
||||
where
|
||||
W: WindowManager,
|
||||
{
|
||||
window_manager.add(
|
||||
WindowType::Layer,
|
||||
access_config().standalone_config.scale_factor.min(2),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
use std::{path::PathBuf, sync::mpsc::Sender};
|
||||
|
||||
use gb_emu_lib::{connect::RendererMessage, renderer::ActiveBackend};
|
||||
|
||||
pub mod winit_manager;
|
||||
pub type ActiveWindowManager = winit_manager::WinitWindowManager<ActiveBackend>;
|
||||
|
||||
pub type RendererChannel = Sender<RendererMessage<[u8; 4]>>;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum WindowType {
|
||||
Main,
|
||||
Tile,
|
||||
Layer,
|
||||
}
|
||||
|
||||
pub trait WindowManager {
|
||||
fn add(
|
||||
&mut self,
|
||||
window_type: WindowType,
|
||||
factor: usize,
|
||||
shader_path: Option<PathBuf>,
|
||||
resizable: bool,
|
||||
) -> anyhow::Result<RendererChannel>;
|
||||
}
|
|
@ -1,492 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use cpal::Stream;
|
||||
use gb_emu_lib::{
|
||||
connect::{EmulatorMessage, JoypadState, RendererMessage, ResolutionData},
|
||||
renderer::{RendererBackend, RendererBackendManager},
|
||||
util::PrintErrors,
|
||||
};
|
||||
use gilrs::{Button, Gilrs};
|
||||
use image::ImageBuffer;
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
#[cfg(target_os = "linux")]
|
||||
use winit::platform::wayland::WindowBuilderExtWayland;
|
||||
use winit::{
|
||||
dpi::PhysicalSize,
|
||||
event::{Event, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop, EventLoopWindowTarget},
|
||||
keyboard::KeyCode,
|
||||
platform::pump_events::EventLoopExtPumpEvents,
|
||||
window::{Window, WindowBuilder, WindowId},
|
||||
};
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
use crate::access_config;
|
||||
|
||||
use super::{RendererChannel, WindowManager, WindowType};
|
||||
|
||||
pub struct WinitWindowManager<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
{
|
||||
event_loop: EventLoop<()>,
|
||||
data: WinitWindowManagerData<Backend>,
|
||||
record_main: bool,
|
||||
}
|
||||
|
||||
struct WinitWindowManagerData<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
{
|
||||
main_window: Option<WindowId>,
|
||||
windows: HashMap<WindowId, WindowRenderer<Backend>>,
|
||||
window_data_manager: Arc<Backend::RendererBackendManager>,
|
||||
input: WinitInputHelper,
|
||||
sender: Sender<EmulatorMessage<[u8; 4]>>,
|
||||
gamepad_handler: Gilrs,
|
||||
joypad_state: JoypadState,
|
||||
_stream: Stream,
|
||||
}
|
||||
|
||||
impl<Backend> WinitWindowManager<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
<Backend as RendererBackend>::RendererError: Sync + Send + 'static,
|
||||
{
|
||||
pub fn new(
|
||||
sender: Sender<EmulatorMessage<[u8; 4]>>,
|
||||
_stream: Stream,
|
||||
record_main: bool,
|
||||
) -> Self {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
|
||||
let window_data_manager = Arc::new(Backend::RendererBackendManager::new(
|
||||
event_loop.display_handle().unwrap(),
|
||||
));
|
||||
Self {
|
||||
event_loop,
|
||||
data: WinitWindowManagerData {
|
||||
main_window: None,
|
||||
windows: HashMap::new(),
|
||||
window_data_manager,
|
||||
input: WinitInputHelper::new(),
|
||||
sender,
|
||||
gamepad_handler: Gilrs::new().unwrap(),
|
||||
joypad_state: Default::default(),
|
||||
_stream,
|
||||
},
|
||||
record_main,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_events(&mut self) {
|
||||
self.event_loop
|
||||
.pump_events(None, |event, target| self.data.handler(true, event, target));
|
||||
}
|
||||
|
||||
pub fn run_events_blocking(mut self) -> Result<(), winit::error::EventLoopError> {
|
||||
self.event_loop
|
||||
.run(move |event, target| self.data.handler(false, event, target))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Backend> WindowManager for WinitWindowManager<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
<Backend as RendererBackend>::RendererError: Sync + Send + 'static,
|
||||
{
|
||||
fn add(
|
||||
&mut self,
|
||||
window_type: WindowType,
|
||||
factor: usize,
|
||||
shader_path: Option<PathBuf>,
|
||||
resizable: bool,
|
||||
) -> anyhow::Result<RendererChannel> {
|
||||
let is_main = window_type == WindowType::Main;
|
||||
|
||||
self.data.add(
|
||||
factor,
|
||||
shader_path,
|
||||
resizable,
|
||||
&self.event_loop,
|
||||
is_main,
|
||||
is_main && self.record_main,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Backend> WinitWindowManagerData<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
<Backend as RendererBackend>::RendererError: Sync + Send + 'static,
|
||||
{
|
||||
fn add(
|
||||
&mut self,
|
||||
factor: usize,
|
||||
shader_path: Option<PathBuf>,
|
||||
resizable: bool,
|
||||
event_loop: &EventLoop<()>,
|
||||
is_main: bool,
|
||||
record: bool,
|
||||
) -> anyhow::Result<RendererChannel> {
|
||||
let (r, sender) = WindowRenderer::new(
|
||||
factor,
|
||||
event_loop,
|
||||
self.window_data_manager.clone(),
|
||||
shader_path,
|
||||
resizable,
|
||||
record,
|
||||
)?;
|
||||
let id = r.window.id();
|
||||
self.windows.insert(id, r);
|
||||
if is_main {
|
||||
self.main_window = Some(id);
|
||||
}
|
||||
Ok(sender)
|
||||
}
|
||||
|
||||
fn handler(&mut self, run_return: bool, event: Event<()>, target: &EventLoopWindowTarget<()>) {
|
||||
target.set_control_flow(ControlFlow::Poll);
|
||||
|
||||
if self.input.update(&event) && self.input.key_pressed(KeyCode::Space) {
|
||||
if let Some(window) = self
|
||||
.main_window
|
||||
.as_ref()
|
||||
.and_then(|id| self.windows.get(id))
|
||||
{
|
||||
let image = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||
window.width as u32,
|
||||
window.height as u32,
|
||||
bytemuck::cast_slice(&window.queued_buf.buf),
|
||||
)
|
||||
.unwrap();
|
||||
let configs = access_config();
|
||||
let screenshot_dir = if configs.standalone_config.group_screenshots_by_rom {
|
||||
configs
|
||||
.config_dir
|
||||
.join(format!("screenshots/{}", configs.rom_title))
|
||||
} else {
|
||||
configs.config_dir.clone()
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&screenshot_dir)
|
||||
.expect("could not create screenshot directory!");
|
||||
|
||||
let screenshot_path = screenshot_dir.join(format!(
|
||||
"{} - {}.png",
|
||||
chrono::DateTime::<chrono::Local>::from(std::time::SystemTime::now())
|
||||
.to_rfc3339(),
|
||||
configs.rom_title,
|
||||
));
|
||||
image
|
||||
.save(screenshot_path)
|
||||
.expect("Could not save screenshot!");
|
||||
}
|
||||
}
|
||||
|
||||
self.process_input();
|
||||
|
||||
match event {
|
||||
Event::Resumed => {
|
||||
self.sender.send(EmulatorMessage::Start).unwrap();
|
||||
}
|
||||
Event::AboutToWait => {
|
||||
for window in self.windows.values_mut() {
|
||||
window.process();
|
||||
window.render(&self.window_data_manager);
|
||||
}
|
||||
|
||||
if run_return {
|
||||
target.exit();
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { window_id, event } => match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
if let Some(w) = self.windows.get_mut(&window_id) {
|
||||
w.on_resize(size);
|
||||
}
|
||||
}
|
||||
WindowEvent::RedrawRequested => {
|
||||
if let Some(w) = self.windows.get_mut(&window_id) {
|
||||
w.render(&self.window_data_manager);
|
||||
}
|
||||
}
|
||||
WindowEvent::CloseRequested => {
|
||||
if self.main_window.is_some_and(|v| v == window_id) {
|
||||
self.sender.send(EmulatorMessage::Exit).unwrap();
|
||||
} else {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
// else if let Some(window) = self
|
||||
// .main_window
|
||||
// .as_ref()
|
||||
// .and_then(|id| self.windows.get(id))
|
||||
// {
|
||||
// window.window.focus_window();
|
||||
// }
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_input(&mut self) {
|
||||
self.joypad_state.reset();
|
||||
|
||||
while let Some(event) = self.gamepad_handler.next_event() {
|
||||
if let gilrs::EventType::ButtonPressed(button, _) = event.event {
|
||||
match button {
|
||||
Button::DPadDown => self.joypad_state.down = true,
|
||||
Button::DPadUp => self.joypad_state.up = true,
|
||||
Button::DPadLeft => self.joypad_state.left = true,
|
||||
Button::DPadRight => self.joypad_state.right = true,
|
||||
Button::Start => self.joypad_state.start = true,
|
||||
Button::Select => self.joypad_state.select = true,
|
||||
Button::East => self.joypad_state.a = true,
|
||||
Button::South => self.joypad_state.b = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for (_id, pad) in self.gamepad_handler.gamepads() {
|
||||
self.joypad_state.down |= pad.is_pressed(Button::DPadDown);
|
||||
self.joypad_state.up |= pad.is_pressed(Button::DPadUp);
|
||||
self.joypad_state.left |= pad.is_pressed(Button::DPadLeft);
|
||||
self.joypad_state.right |= pad.is_pressed(Button::DPadRight);
|
||||
self.joypad_state.start |= pad.is_pressed(Button::Start);
|
||||
self.joypad_state.select |= pad.is_pressed(Button::Select);
|
||||
self.joypad_state.a |= pad.is_pressed(Button::East);
|
||||
self.joypad_state.b |= pad.is_pressed(Button::South);
|
||||
}
|
||||
}
|
||||
|
||||
self.joypad_state.down |=
|
||||
self.input.key_held(KeyCode::ArrowDown) || self.input.key_held(KeyCode::KeyS);
|
||||
self.joypad_state.up |=
|
||||
self.input.key_held(KeyCode::ArrowUp) || self.input.key_held(KeyCode::KeyW);
|
||||
self.joypad_state.left |=
|
||||
self.input.key_held(KeyCode::ArrowLeft) || self.input.key_held(KeyCode::KeyA);
|
||||
self.joypad_state.right |=
|
||||
self.input.key_held(KeyCode::ArrowRight) || self.input.key_held(KeyCode::KeyD);
|
||||
self.joypad_state.start |= self.input.key_held(KeyCode::Equal);
|
||||
self.joypad_state.select |= self.input.key_held(KeyCode::Minus);
|
||||
self.joypad_state.a |= self.input.key_held(KeyCode::Quote);
|
||||
self.joypad_state.b |= self.input.key_held(KeyCode::Semicolon);
|
||||
|
||||
self.sender
|
||||
.send(EmulatorMessage::JoypadUpdate(self.joypad_state))
|
||||
.expect("error sending joypad state");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WindowRenderer<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
{
|
||||
receiver: Receiver<RendererMessage<[u8; 4]>>,
|
||||
renderer: Backend,
|
||||
window: Window,
|
||||
width: usize,
|
||||
height: usize,
|
||||
factor: usize,
|
||||
queued_buf: QueuedBuf,
|
||||
recording: Option<RecordInfo>,
|
||||
awaiting_resize: bool,
|
||||
}
|
||||
|
||||
struct QueuedBuf {
|
||||
buf: Vec<[u8; 4]>,
|
||||
displayed: bool,
|
||||
}
|
||||
|
||||
impl Default for QueuedBuf {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
buf: Default::default(),
|
||||
displayed: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QueuedBuf {
|
||||
fn update(&mut self, new: Vec<[u8; 4]>) {
|
||||
self.buf = new;
|
||||
self.displayed = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl<Backend> WindowRenderer<Backend>
|
||||
where
|
||||
Backend: RendererBackend,
|
||||
<Backend as RendererBackend>::RendererError: Sync + Send + 'static,
|
||||
{
|
||||
#[allow(unused_variables)]
|
||||
fn new(
|
||||
factor: usize,
|
||||
event_loop: &EventLoop<()>,
|
||||
manager: Arc<Backend::RendererBackendManager>,
|
||||
shader_path: Option<PathBuf>,
|
||||
resizable: bool,
|
||||
record: bool,
|
||||
) -> anyhow::Result<(Self, RendererChannel)> {
|
||||
let window = WindowBuilder::new()
|
||||
.with_title("Gameboy")
|
||||
.with_resizable(resizable);
|
||||
#[cfg(target_os = "linux")]
|
||||
let window = window.with_name("TWINC", "");
|
||||
let window = window.build(event_loop)?;
|
||||
|
||||
let real_factor = (window.scale_factor() * factor as f64) as u32;
|
||||
let inner_size = window.inner_size();
|
||||
let resolutions = ResolutionData {
|
||||
real_width: inner_size.width,
|
||||
real_height: inner_size.height,
|
||||
scaled_width: inner_size.width / real_factor,
|
||||
scaled_height: inner_size.height / real_factor,
|
||||
};
|
||||
let renderer = RendererBackend::new(resolutions, &window, shader_path, manager);
|
||||
|
||||
let renderer = renderer?;
|
||||
|
||||
let recording = if record {
|
||||
let configs = access_config();
|
||||
|
||||
let dir = configs.config_dir.join(format!(
|
||||
"recordings/{} - {}",
|
||||
chrono::DateTime::<chrono::Local>::from(std::time::SystemTime::now()).to_rfc3339(),
|
||||
configs.rom_title,
|
||||
));
|
||||
|
||||
std::fs::create_dir_all(&dir).expect("could not create screenshot directory!");
|
||||
|
||||
Some(RecordInfo { dir, frame_num: 0 })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
|
||||
Ok((
|
||||
Self {
|
||||
receiver,
|
||||
renderer,
|
||||
window,
|
||||
width: 1,
|
||||
height: 1,
|
||||
factor,
|
||||
queued_buf: Default::default(),
|
||||
recording,
|
||||
awaiting_resize: false,
|
||||
},
|
||||
sender,
|
||||
))
|
||||
}
|
||||
|
||||
fn render(&mut self, manager: &Backend::RendererBackendManager) {
|
||||
let inner_size = self.window.inner_size();
|
||||
if !self.queued_buf.displayed {
|
||||
self.renderer
|
||||
.new_frame(&self.queued_buf.buf)
|
||||
.some_or_print();
|
||||
self.queued_buf.displayed = true;
|
||||
}
|
||||
|
||||
self.renderer
|
||||
.render(
|
||||
ResolutionData {
|
||||
real_width: inner_size.width,
|
||||
real_height: inner_size.height,
|
||||
scaled_width: self.width as u32,
|
||||
scaled_height: self.height as u32,
|
||||
},
|
||||
manager,
|
||||
)
|
||||
.some_or_print();
|
||||
}
|
||||
|
||||
fn process(&mut self) {
|
||||
while let Ok(message) = self.receiver.try_recv() {
|
||||
match message {
|
||||
RendererMessage::Prepare { width, height } => self.attempt_resize(width, height),
|
||||
RendererMessage::Resize { width, height } => self.attempt_resize(width, height),
|
||||
RendererMessage::Display { buffer } => self.display(buffer),
|
||||
RendererMessage::SetTitle { title } => self.window.set_title(&title),
|
||||
RendererMessage::Rumble { rumble: _ } => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn attempt_resize(&mut self, width: usize, height: usize) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
|
||||
let real_factor = (self.window.scale_factor() * self.factor as f64) as u32;
|
||||
|
||||
let real_width = (width as u32) * real_factor;
|
||||
let real_height = (height as u32) * real_factor;
|
||||
self.awaiting_resize = true;
|
||||
|
||||
if let Some(size) = self
|
||||
.window
|
||||
.request_inner_size(PhysicalSize::new(real_width, real_height))
|
||||
{
|
||||
self.on_resize(size);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_resize(&mut self, size: PhysicalSize<u32>) {
|
||||
self.awaiting_resize = false;
|
||||
let resolutions = ResolutionData {
|
||||
real_width: size.width,
|
||||
real_height: size.height,
|
||||
scaled_width: self.width as u32,
|
||||
scaled_height: self.height as u32,
|
||||
};
|
||||
|
||||
self.renderer
|
||||
.resize(resolutions, &self.window)
|
||||
.some_or_print();
|
||||
|
||||
self.window.request_redraw();
|
||||
}
|
||||
|
||||
fn display(&mut self, buffer: Vec<[u8; 4]>) {
|
||||
if self.awaiting_resize {
|
||||
log::warn!(
|
||||
"window {}: received buffer before resize complete",
|
||||
self.window.title()
|
||||
);
|
||||
return;
|
||||
}
|
||||
self.queued_buf.update(buffer);
|
||||
|
||||
self.window.request_redraw();
|
||||
|
||||
if let Some(ref mut info) = self.recording {
|
||||
let image = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||
self.width as u32,
|
||||
self.height as u32,
|
||||
bytemuck::cast_slice(&self.queued_buf.buf),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let frame_path = info.dir.join(format!("{:0>5}.png", info.frame_num));
|
||||
image.save(frame_path).expect("Could not save frame!");
|
||||
|
||||
info.frame_num += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RecordInfo {
|
||||
dir: PathBuf,
|
||||
frame_num: usize,
|
||||
}
|
19
gb-emu/Cargo.toml
Normal file
19
gb-emu/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "gb-emu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
camera = ["dep:nokhwa", "dep:send_wrapper"]
|
||||
|
||||
[dependencies]
|
||||
gb-emu-lib = { path = "../lib" }
|
||||
clap = { version = "4.1.8", features = ["derive"] }
|
||||
minifb = { path = "../vendored/rust_minifb" }
|
||||
gilrs = "0.10"
|
||||
cpal = "0.15"
|
||||
futures = "0.3"
|
||||
ctrlc = "3.2.5"
|
||||
nokhwa = { version = "0.10.3", features = ["input-opencv"], optional = true }
|
||||
send_wrapper = { version = "0.6.0", optional = true }
|
52
gb-emu/src/audio.rs
Normal file
52
gb-emu/src/audio.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
use cpal::{
|
||||
traits::{DeviceTrait, HostTrait, StreamTrait},
|
||||
Stream,
|
||||
};
|
||||
use futures::executor;
|
||||
use gb_emu_lib::connect::{AudioOutput, DownsampleType};
|
||||
|
||||
const FRAMES_TO_BUFFER: usize = 1;
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold;
|
||||
|
||||
pub fn create_output() -> (AudioOutput, Stream) {
|
||||
let host = cpal::default_host();
|
||||
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("no output device available");
|
||||
|
||||
let mut supported_configs_range = device
|
||||
.supported_output_configs()
|
||||
.expect("error while querying configs");
|
||||
let config = supported_configs_range
|
||||
.next()
|
||||
.expect("no supported config?!")
|
||||
.with_max_sample_rate();
|
||||
|
||||
let sample_rate = config.sample_rate().0;
|
||||
|
||||
let (output, mut rx) =
|
||||
AudioOutput::new(sample_rate as f32, true, FRAMES_TO_BUFFER, DOWNSAMPLE_TYPE);
|
||||
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
&config.config(),
|
||||
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
for v in data.chunks_exact_mut(2) {
|
||||
match executor::block_on(rx.pop()) {
|
||||
Some(a) => v.copy_from_slice(&a),
|
||||
None => panic!("Audio queue disconnected!"),
|
||||
}
|
||||
}
|
||||
},
|
||||
move |err| {
|
||||
// react to errors here.
|
||||
println!("audio error: {err}");
|
||||
},
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
stream.play().unwrap();
|
||||
|
||||
(output, stream)
|
||||
}
|
47
gb-emu/src/camera.rs
Normal file
47
gb-emu/src/camera.rs
Normal file
|
@ -0,0 +1,47 @@
|
|||
use gb_emu_lib::connect::PocketCamera;
|
||||
use nokhwa::{
|
||||
pixel_format,
|
||||
utils::{CameraIndex, RequestedFormat, RequestedFormatType},
|
||||
Camera,
|
||||
};
|
||||
use send_wrapper::SendWrapper;
|
||||
|
||||
pub struct Webcam {
|
||||
camera: SendWrapper<Camera>,
|
||||
buffer: [u8; 128 * 128],
|
||||
}
|
||||
|
||||
impl Webcam {
|
||||
pub fn new() -> Self {
|
||||
let format = RequestedFormat::new::<pixel_format::LumaAFormat>(
|
||||
RequestedFormatType::AbsoluteHighestResolution,
|
||||
);
|
||||
|
||||
Self {
|
||||
camera: SendWrapper::new(Camera::new(CameraIndex::Index(0), format).unwrap()),
|
||||
buffer: [0; 128 * 128],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PocketCamera for Webcam {
|
||||
fn get_image(&mut self) -> [u8; 128 * 128] {
|
||||
self.buffer
|
||||
}
|
||||
|
||||
fn begin_capture(&mut self) {
|
||||
let height = self.camera.resolution().height() as usize;
|
||||
let width = self.camera.resolution().width() as usize;
|
||||
let frame = self.camera.frame_raw().expect("couldn't get frame");
|
||||
|
||||
for y in 0..128 {
|
||||
for x in 0..128 {
|
||||
self.buffer[y * 128 + x] = frame[((y * width) + x) * 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init(&mut self) {
|
||||
self.camera.open_stream().expect("couldn't open stream");
|
||||
}
|
||||
}
|
121
gb-emu/src/main.rs
Normal file
121
gb-emu/src/main.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
#![feature(let_chains)]
|
||||
|
||||
#[cfg(feature = "camera")]
|
||||
use camera::Webcam;
|
||||
use clap::{ArgGroup, Parser};
|
||||
|
||||
use gb_emu_lib::{
|
||||
connect::{EmulatorMessage, EmulatorOptions, NoCamera, RomFile, SerialTarget},
|
||||
EmulatorCore,
|
||||
};
|
||||
use gilrs::Gilrs;
|
||||
|
||||
use std::sync::mpsc::channel;
|
||||
use window::WindowRenderer;
|
||||
|
||||
mod audio;
|
||||
#[cfg(feature = "camera")]
|
||||
mod camera;
|
||||
mod window;
|
||||
|
||||
/// Gameboy (DMG-A/B/C) emulator
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
#[command(group(ArgGroup::new("prints").args(["verbose","cycle_count"])))]
|
||||
#[command(group(ArgGroup::new("saves").args(["save","no_save"])))]
|
||||
struct Args {
|
||||
/// ROM path
|
||||
#[arg(short, long)]
|
||||
rom: String,
|
||||
|
||||
/// Save path
|
||||
#[arg(long)]
|
||||
save: Option<String>,
|
||||
|
||||
/// Skip save file
|
||||
#[arg(long)]
|
||||
no_save: bool,
|
||||
|
||||
/// BootROM path
|
||||
#[arg(short, long)]
|
||||
bootrom: Option<String>,
|
||||
|
||||
/// Verbose print
|
||||
#[arg(short, long)]
|
||||
verbose: bool,
|
||||
|
||||
/// Connect the serial port output to stdout
|
||||
#[arg(short, long)]
|
||||
connect_serial: bool,
|
||||
|
||||
/// Show cycle count
|
||||
#[arg(long)]
|
||||
cycle_count: bool,
|
||||
|
||||
/// Show tile window
|
||||
#[arg(short, long)]
|
||||
tile_window: bool,
|
||||
|
||||
/// Step emulation by...
|
||||
#[arg(long)]
|
||||
step_by: Option<usize>,
|
||||
|
||||
/// Scale display by...
|
||||
#[arg(short, long)]
|
||||
scale_factor: Option<usize>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
|
||||
let factor = if let Some(factor) = args.scale_factor {
|
||||
factor
|
||||
} else {
|
||||
3
|
||||
};
|
||||
|
||||
let tile_window: Option<WindowRenderer> = if args.tile_window {
|
||||
Some(WindowRenderer::new(factor, None))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let (sender, receiver) = channel::<EmulatorMessage>();
|
||||
|
||||
ctrlc::set_handler(move || sender.send(EmulatorMessage::Stop).unwrap()).unwrap();
|
||||
|
||||
let (output, _stream) = audio::create_output();
|
||||
|
||||
#[cfg(feature = "webcam")]
|
||||
let camera = Webcam::new();
|
||||
#[cfg(not(feature = "webcam"))]
|
||||
let camera = NoCamera::default();
|
||||
|
||||
let options = EmulatorOptions::new_with_camera(
|
||||
WindowRenderer::new(factor, Some(Gilrs::new().unwrap())),
|
||||
RomFile::Path(args.rom),
|
||||
output,
|
||||
camera,
|
||||
)
|
||||
.with_save_path(args.save)
|
||||
.with_serial_target(if args.connect_serial {
|
||||
SerialTarget::Stdout
|
||||
} else {
|
||||
SerialTarget::None
|
||||
})
|
||||
.with_bootrom(args.bootrom.map(RomFile::Path))
|
||||
.with_no_save(args.no_save)
|
||||
.with_verbose(args.verbose)
|
||||
.with_tile_window(tile_window);
|
||||
|
||||
let mut core = EmulatorCore::init(receiver, options);
|
||||
|
||||
match args.step_by {
|
||||
Some(step_size) => loop {
|
||||
core.run_stepped(step_size);
|
||||
},
|
||||
None => loop {
|
||||
core.run();
|
||||
},
|
||||
}
|
||||
}
|
148
gb-emu/src/window.rs
Normal file
148
gb-emu/src/window.rs
Normal file
|
@ -0,0 +1,148 @@
|
|||
use gb_emu_lib::{
|
||||
connect::{JoypadState, Renderer},
|
||||
util::scale_buffer,
|
||||
};
|
||||
use gilrs::{
|
||||
ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks},
|
||||
Button, Gilrs,
|
||||
};
|
||||
use minifb::{Key, Window, WindowOptions};
|
||||
|
||||
pub struct WindowRenderer {
|
||||
window: Option<Window>,
|
||||
scaled_buf: Vec<u32>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
factor: usize,
|
||||
gamepad_handler: Option<Gilrs>,
|
||||
joypad_state: JoypadState,
|
||||
current_rumble: bool,
|
||||
}
|
||||
|
||||
impl WindowRenderer {
|
||||
pub fn new(factor: usize, gamepad_handler: Option<Gilrs>) -> Self {
|
||||
Self {
|
||||
window: None,
|
||||
scaled_buf: vec![],
|
||||
width: 0,
|
||||
height: 0,
|
||||
factor,
|
||||
gamepad_handler,
|
||||
joypad_state: JoypadState::default(),
|
||||
current_rumble: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<u32> for WindowRenderer {
|
||||
fn prepare(&mut self, width: usize, height: usize) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.window = Some(
|
||||
Window::new(
|
||||
"Gameboy",
|
||||
width * self.factor,
|
||||
height * self.factor,
|
||||
WindowOptions::default(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
fn display(&mut self, buffer: &[u32]) {
|
||||
if let Some(ref mut window) = self.window {
|
||||
self.scaled_buf = scale_buffer(buffer, self.width, self.height, self.factor);
|
||||
window
|
||||
.update_with_buffer(
|
||||
&self.scaled_buf,
|
||||
self.width * self.factor,
|
||||
self.height * self.factor,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: String) {
|
||||
if let Some(ref mut window) = self.window {
|
||||
window.set_title(&title);
|
||||
}
|
||||
}
|
||||
|
||||
fn latest_joypad_state(&mut self) -> JoypadState {
|
||||
self.joypad_state.reset();
|
||||
|
||||
if let Some(ref mut gamepad_handler) = self.gamepad_handler {
|
||||
while let Some(event) = gamepad_handler.next_event() {
|
||||
if let gilrs::EventType::ButtonPressed(button, _) = event.event {
|
||||
match button {
|
||||
Button::DPadDown => self.joypad_state.down = true,
|
||||
Button::DPadUp => self.joypad_state.up = true,
|
||||
Button::DPadLeft => self.joypad_state.left = true,
|
||||
Button::DPadRight => self.joypad_state.right = true,
|
||||
Button::Start => self.joypad_state.start = true,
|
||||
Button::Select => self.joypad_state.select = true,
|
||||
Button::East => self.joypad_state.a = true,
|
||||
Button::South => self.joypad_state.b = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (_, pad) in gamepad_handler.gamepads() {
|
||||
self.joypad_state.down |= pad.is_pressed(Button::DPadDown);
|
||||
self.joypad_state.up |= pad.is_pressed(Button::DPadUp);
|
||||
self.joypad_state.left |= pad.is_pressed(Button::DPadLeft);
|
||||
self.joypad_state.right |= pad.is_pressed(Button::DPadRight);
|
||||
self.joypad_state.start |= pad.is_pressed(Button::Start);
|
||||
self.joypad_state.select |= pad.is_pressed(Button::Select);
|
||||
self.joypad_state.a |= pad.is_pressed(Button::East);
|
||||
self.joypad_state.b |= pad.is_pressed(Button::South);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(window) = &self.window {
|
||||
let keys = window.get_keys();
|
||||
|
||||
self.joypad_state.down |= keys.contains(&Key::Down) || keys.contains(&Key::S);
|
||||
self.joypad_state.up |= keys.contains(&Key::Up) || keys.contains(&Key::W);
|
||||
self.joypad_state.left |= keys.contains(&Key::Left) || keys.contains(&Key::A);
|
||||
self.joypad_state.right |= keys.contains(&Key::Right) || keys.contains(&Key::D);
|
||||
self.joypad_state.start |= keys.contains(&Key::Equal);
|
||||
self.joypad_state.select |= keys.contains(&Key::Minus);
|
||||
self.joypad_state.a |= keys.contains(&Key::Apostrophe);
|
||||
self.joypad_state.b |= keys.contains(&Key::Semicolon);
|
||||
}
|
||||
|
||||
self.joypad_state
|
||||
}
|
||||
|
||||
fn set_rumble(&mut self, rumbling: bool) {
|
||||
if rumbling != self.current_rumble && let Some(ref mut gamepad_handler) = self.gamepad_handler {
|
||||
self.current_rumble = rumbling;
|
||||
|
||||
let ids = gamepad_handler
|
||||
.gamepads()
|
||||
.filter_map(|(id, gp)| if gp.is_ff_supported() { Some(id) } else { None })
|
||||
.collect::<Vec<_>>();
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let magnitude = if rumbling { 0xFF } else { 0x0 };
|
||||
|
||||
EffectBuilder::new()
|
||||
.add_effect(BaseEffect {
|
||||
kind: BaseEffectType::Strong { magnitude },
|
||||
scheduling: Replay {
|
||||
after: Ticks::from_ms(0),
|
||||
play_for: Ticks::from_ms(16),
|
||||
with_delay: Ticks::from_ms(0),
|
||||
},
|
||||
envelope: Default::default(),
|
||||
})
|
||||
.gamepads(&ids)
|
||||
.finish(gamepad_handler)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +1,21 @@
|
|||
[package]
|
||||
name = "twinc_emu_vst"
|
||||
version = "0.5.1"
|
||||
name = "vst"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "vst"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["plugin", "wgpu"]
|
||||
pixels = ["gb-emu-lib/pixels-renderer"]
|
||||
vulkan = ["gb-emu-lib/vulkan-renderer"]
|
||||
vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"]
|
||||
wgpu = ["gb-emu-lib/wgpu-renderer"]
|
||||
plugin = [
|
||||
"dep:nih_plug",
|
||||
"dep:baseview",
|
||||
"dep:async-ringbuf",
|
||||
"dep:futures",
|
||||
"dep:keyboard-types",
|
||||
]
|
||||
default = []
|
||||
savestate = []
|
||||
|
||||
[dependencies]
|
||||
gb-emu-lib = { workspace = true }
|
||||
nih_plug = { workspace = true, features = [
|
||||
"standalone",
|
||||
"vst3",
|
||||
], optional = true }
|
||||
baseview = { workspace = true, optional = true }
|
||||
async-ringbuf = { version = "0.2.1", optional = true }
|
||||
futures = { version = "0.3.30", optional = true }
|
||||
keyboard-types = { version = "0.6.2", optional = true }
|
||||
raw-window-handle = { workspace = true }
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
gb-emu-lib = { path = "../lib" }
|
||||
nih_plug = { path = "../vendored/nih-plug", features = ["standalone"] }
|
||||
baseview = { path = "../vendored/baseview" }
|
||||
pixels = "0.11"
|
||||
async-ringbuf = "0.1.2"
|
||||
futures = "0.3"
|
||||
keyboard-types = "0.6.2"
|
BIN
gb-vst/error.gb
BIN
gb-vst/error.gb
Binary file not shown.
|
@ -1,29 +1,337 @@
|
|||
use gb_emu_lib::config::NamedConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use async_ringbuf::AsyncHeapConsumer;
|
||||
use futures::executor;
|
||||
use gb_emu_lib::{
|
||||
connect::{
|
||||
AudioOutput, DownsampleType, EmulatorMessage, EmulatorOptions, JoypadButtons, NoCamera,
|
||||
RomFile, SerialTarget,
|
||||
},
|
||||
EmulatorCore,
|
||||
};
|
||||
use nih_plug::midi::MidiResult::Basic;
|
||||
use nih_plug::prelude::*;
|
||||
use std::sync::{
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use ui::{Emulator, EmulatorRenderer};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
mod plugin;
|
||||
#[cfg(feature = "savestate")]
|
||||
use gb_emu_lib::connect::CpuSaveState;
|
||||
#[cfg(feature = "savestate")]
|
||||
use nih_plug::params::persist::PersistentField;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct VstConfig {
|
||||
pub scale_factor: usize,
|
||||
pub rom: String,
|
||||
pub force_skip_bootrom: bool,
|
||||
mod ui;
|
||||
|
||||
#[cfg(feature = "savestate")]
|
||||
#[derive(Default)]
|
||||
struct SaveStateParam {
|
||||
state: Arc<Mutex<Option<CpuSaveState<[u8; 4]>>>>,
|
||||
}
|
||||
|
||||
impl NamedConfig for VstConfig {
|
||||
fn name() -> String {
|
||||
String::from("vst")
|
||||
#[cfg(feature = "savestate")]
|
||||
impl PersistentField<'_, Option<CpuSaveState<[u8; 4]>>> for SaveStateParam {
|
||||
fn set(&self, new_value: Option<CpuSaveState<[u8; 4]>>) {
|
||||
*self.state.lock().unwrap() = new_value;
|
||||
}
|
||||
|
||||
fn map<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: Fn(&Option<CpuSaveState<[u8; 4]>>) -> R,
|
||||
{
|
||||
f(&self.state.lock().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VstConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scale_factor: 3,
|
||||
rom: String::from(""),
|
||||
force_skip_bootrom: true,
|
||||
#[derive(Params, Default)]
|
||||
struct EmuParams {
|
||||
#[cfg(feature = "savestate")]
|
||||
#[persist = "save_state"]
|
||||
last_save_state: SaveStateParam,
|
||||
}
|
||||
|
||||
struct EmuVars {
|
||||
rx: AsyncHeapConsumer<[f32; 2]>,
|
||||
sender: Sender<EmulatorMessage>,
|
||||
emulator_core: EmulatorCore<[u8; 4], EmulatorRenderer, NoCamera>,
|
||||
serial_tx: Sender<u8>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GameboyEmu {
|
||||
vars: Option<EmuVars>,
|
||||
frame_receiver: Arc<FrameReceiver>,
|
||||
key_handler: Arc<JoypadSender>,
|
||||
params: Arc<EmuParams>,
|
||||
}
|
||||
|
||||
type FrameReceiver = Mutex<Option<Receiver<Vec<[u8; 4]>>>>;
|
||||
type JoypadSender = Mutex<Option<Sender<(JoypadButtons, bool)>>>;
|
||||
|
||||
const FRAMES_TO_BUFFER: usize = 1;
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::ZeroOrderHold;
|
||||
|
||||
const ROM: &[u8; 65536] = include_bytes!("../../test-roms/mGB1_3_0.gb");
|
||||
const BOOTROM: Option<&[u8; 256]> = None;
|
||||
|
||||
impl Plugin for GameboyEmu {
|
||||
const NAME: &'static str = "Gameboy";
|
||||
|
||||
const VENDOR: &'static str = "Alex Janka";
|
||||
|
||||
const URL: &'static str = "alexjanka.com";
|
||||
|
||||
const EMAIL: &'static str = "alex@alexjanka.com";
|
||||
|
||||
const VERSION: &'static str = "0.1";
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(2),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
}];
|
||||
|
||||
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
||||
|
||||
type SysExMessage = ();
|
||||
|
||||
type BackgroundTask = ();
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
nih_warn!("processing audio...");
|
||||
while let Some(event) = context.next_event() {
|
||||
if let Some(Basic(as_bytes)) = event.as_midi() {
|
||||
match event {
|
||||
NoteEvent::NoteOn {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x90 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::NoteOff {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x80 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiPitchBend {
|
||||
timing: _,
|
||||
channel,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xE0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiCC {
|
||||
timing: _,
|
||||
channel,
|
||||
cc: _,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xB0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiProgramChange {
|
||||
timing: _,
|
||||
channel,
|
||||
program: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xC0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
nih_warn!("...finished processing note events");
|
||||
if buffer.channels() != 2 {
|
||||
nih_warn!("literally just panicking because there number of channels != 2 and if this is the problem you are literally stupid");
|
||||
panic!()
|
||||
}
|
||||
nih_warn!("...not stupid");
|
||||
for sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
nih_warn!("...rx empty: running until buffer full");
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
nih_warn!("...buffer full");
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
for (source, dest) in a.iter().zip(sample) {
|
||||
*dest = *source;
|
||||
}
|
||||
} else {
|
||||
nih_warn!("...could not rx audio from emulator")
|
||||
}
|
||||
}
|
||||
nih_warn!("...running emulator until buffer is full again");
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
nih_warn!("...finished with processing audio");
|
||||
} else {
|
||||
nih_warn!("processing audio before emulator init");
|
||||
while context.next_event().is_some() {}
|
||||
}
|
||||
self.update_save_state();
|
||||
ProcessStatus::KeepAlive
|
||||
}
|
||||
|
||||
fn editor(&self, _: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
nih_warn!("creating first editor instance");
|
||||
Some(Box::new(Emulator::new(
|
||||
self.frame_receiver.clone(),
|
||||
self.key_handler.clone(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
nih_warn!("begin initialize");
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
false,
|
||||
FRAMES_TO_BUFFER,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
vars.emulator_core.replace_output(output);
|
||||
vars.rx = rx;
|
||||
} else {
|
||||
let bootrom = BOOTROM.map(|v| RomFile::Raw(v.to_vec()));
|
||||
let rom = RomFile::Raw(ROM.to_vec());
|
||||
|
||||
let (sender, receiver) = channel::<EmulatorMessage>();
|
||||
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
false,
|
||||
FRAMES_TO_BUFFER,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
let (window, frame_receiver, key_handler) = EmulatorRenderer::new();
|
||||
|
||||
*self
|
||||
.frame_receiver
|
||||
.lock()
|
||||
.expect("could not lock frame receiver") = Some(frame_receiver);
|
||||
*self.key_handler.lock().expect("could not lock key handler") = Some(key_handler);
|
||||
|
||||
let (serial_tx, gb_serial_rx) = mpsc::channel::<u8>();
|
||||
let serial_target = SerialTarget::Custom {
|
||||
rx: Some(gb_serial_rx),
|
||||
tx: None,
|
||||
};
|
||||
|
||||
nih_warn!("creating emulator core");
|
||||
|
||||
#[cfg(feature = "savestate")]
|
||||
let mut emulator_core = if let Some(state) =
|
||||
self.params.last_save_state.state.lock().unwrap().take()
|
||||
{
|
||||
EmulatorCore::from_save_state(state, rom, receiver, window, output, serial_target)
|
||||
} else {
|
||||
let options = gb_emu_lib::Options::new(window, rom, output)
|
||||
.with_bootrom(bootrom)
|
||||
.with_serial_target(serial_target)
|
||||
.force_no_save();
|
||||
|
||||
EmulatorCore::init(receiver, options)
|
||||
};
|
||||
#[cfg(not(feature = "savestate"))]
|
||||
let mut emulator_core = {
|
||||
let options = EmulatorOptions::new(window, rom, output)
|
||||
.with_bootrom(bootrom)
|
||||
.with_serial_target(serial_target)
|
||||
.force_no_save();
|
||||
|
||||
EmulatorCore::init(receiver, options)
|
||||
};
|
||||
|
||||
emulator_core.run_until_buffer_full();
|
||||
|
||||
self.vars = Some(EmuVars {
|
||||
rx,
|
||||
sender,
|
||||
emulator_core,
|
||||
serial_tx,
|
||||
});
|
||||
self.update_save_state();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn deactivate(&mut self) {
|
||||
nih_log!("deactivating");
|
||||
self.update_save_state();
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
match vars.sender.send(EmulatorMessage::Stop) {
|
||||
Ok(_) => self.vars = None,
|
||||
Err(e) => nih_log!("error {e} sending message to emulator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameboyEmu {
|
||||
fn update_save_state(&mut self) {
|
||||
#[cfg(feature = "savestate")]
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
*self.params.last_save_state.state.lock().unwrap() =
|
||||
Some(vars.emulator_core.get_save_state());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vst3Plugin for GameboyEmu {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula";
|
||||
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
||||
&[Vst3SubCategory::Instrument, Vst3SubCategory::Synth];
|
||||
}
|
||||
|
||||
nih_export_vst3!(GameboyEmu);
|
||||
|
|
7
gb-vst/src/main.rs
Normal file
7
gb-vst/src/main.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
use nih_plug::prelude::*;
|
||||
|
||||
use vst::GameboyEmu;
|
||||
|
||||
fn main() {
|
||||
nih_export_standalone::<GameboyEmu>();
|
||||
}
|
|
@ -1,364 +0,0 @@
|
|||
use async_ringbuf::{
|
||||
traits::{AsyncConsumer, Observer},
|
||||
AsyncHeapCons,
|
||||
};
|
||||
use baseview::Size;
|
||||
use futures::executor;
|
||||
use gb_emu_lib::{
|
||||
config::CONFIG_MANAGER,
|
||||
connect::{
|
||||
AudioOutput, CgbRomType, DownsampleType, EmulatorCoreTrait, EmulatorMessage,
|
||||
EmulatorOptions, RendererMessage, RomFile, SerialTarget,
|
||||
},
|
||||
EmulatorCore, HEIGHT, WIDTH,
|
||||
};
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
Arc, Mutex, OnceLock, RwLock,
|
||||
},
|
||||
};
|
||||
use ui::TwincEditor;
|
||||
|
||||
use crate::VstConfig;
|
||||
|
||||
mod ui;
|
||||
|
||||
#[derive(Default)]
|
||||
struct SramParam {
|
||||
state: Arc<RwLock<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl PersistentField<'_, Vec<u8>> for SramParam {
|
||||
fn set(&self, new_value: Vec<u8>) {
|
||||
let mut w = self.state.write().unwrap();
|
||||
w.resize(new_value.len(), 0);
|
||||
w.copy_from_slice(&new_value);
|
||||
}
|
||||
|
||||
fn map<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: Fn(&Vec<u8>) -> R,
|
||||
{
|
||||
f(&self.state.read().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Default)]
|
||||
struct EmuParams {
|
||||
#[persist = "sram"]
|
||||
sram_save: SramParam,
|
||||
}
|
||||
|
||||
struct EmuVars {
|
||||
rx: AsyncHeapCons<[f32; 2]>,
|
||||
emulator_core: EmulatorCore<[u8; 4]>,
|
||||
serial_tx: Sender<u8>,
|
||||
}
|
||||
|
||||
struct EmuComms {
|
||||
sender: Sender<EmulatorMessage<[u8; 4]>>,
|
||||
receiver: Receiver<RendererMessage<[u8; 4]>>,
|
||||
}
|
||||
|
||||
struct Configs {
|
||||
vst_config: VstConfig,
|
||||
emu_config: gb_emu_lib::config::Config,
|
||||
config_dir: PathBuf,
|
||||
}
|
||||
|
||||
static CONFIGS: OnceLock<Configs> = OnceLock::new();
|
||||
static IS_CGB: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
fn access_config<'a>() -> &'a Configs {
|
||||
CONFIGS.get_or_init(|| {
|
||||
let emu_config = CONFIG_MANAGER.load_or_create_base_config();
|
||||
let vst_config: VstConfig = CONFIG_MANAGER.load_or_create_config();
|
||||
|
||||
Configs {
|
||||
vst_config,
|
||||
emu_config,
|
||||
config_dir: CONFIG_MANAGER.dir(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GameboyEmu {
|
||||
vars: Option<EmuVars>,
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
params: Arc<EmuParams>,
|
||||
}
|
||||
|
||||
const BUFFERS_PER_FRAME: usize = 1;
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::Linear;
|
||||
|
||||
impl Plugin for GameboyEmu {
|
||||
const NAME: &'static str = "Gameboy";
|
||||
|
||||
const VENDOR: &'static str = "Alex Janka";
|
||||
|
||||
const URL: &'static str = "alexjanka.com";
|
||||
|
||||
const EMAIL: &'static str = "alex@alexjanka.com";
|
||||
|
||||
const VERSION: &'static str = "0.1";
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(2),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(1),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
];
|
||||
|
||||
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
||||
|
||||
type SysExMessage = ();
|
||||
|
||||
type BackgroundTask = ();
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
while let Some(event) = context.next_event() {
|
||||
if let Some(Basic(as_bytes)) = event.as_midi() {
|
||||
match event {
|
||||
NoteEvent::NoteOn {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x90 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::NoteOff {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x80 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiPitchBend {
|
||||
timing: _,
|
||||
channel,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xE0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiCC {
|
||||
timing: _,
|
||||
channel,
|
||||
cc: _,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xB0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiProgramChange {
|
||||
timing: _,
|
||||
channel,
|
||||
program: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xC0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if buffer.channels() != 2 {
|
||||
for mut sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
if let Some(g) = sample.get_mut(0) {
|
||||
*g = (a[0] + a[1]) / 2.;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
for (source, dest) in a.iter().zip(sample) {
|
||||
*dest = *source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
} else {
|
||||
while context.next_event().is_some() {}
|
||||
}
|
||||
ProcessStatus::KeepAlive
|
||||
}
|
||||
|
||||
fn editor(&mut self, _e: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
let configs = access_config();
|
||||
|
||||
let size = Size::new(
|
||||
(WIDTH * configs.vst_config.scale_factor) as f64,
|
||||
(HEIGHT * configs.vst_config.scale_factor) as f64,
|
||||
);
|
||||
|
||||
Some(Box::new(TwincEditor::new(self.emu_comms.clone(), size)))
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
vars.emulator_core.replace_output(output);
|
||||
vars.rx = rx;
|
||||
} else {
|
||||
let configs = access_config();
|
||||
let rom_path = configs.config_dir.join(configs.vst_config.rom.clone());
|
||||
|
||||
let rom = RomFile::Path(rom_path)
|
||||
.load(gb_emu_lib::connect::SramType::None)
|
||||
.unwrap_or_else(|_v| {
|
||||
RomFile::Raw(include_bytes!("../error.gb").to_vec())
|
||||
.load(gb_emu_lib::connect::SramType::None)
|
||||
.expect("Couldn't load built-in fallback rom")
|
||||
});
|
||||
|
||||
let _ =
|
||||
IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb);
|
||||
|
||||
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
|
||||
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
let (emu_sender, renderer_receiver) = mpsc::channel();
|
||||
|
||||
*self.emu_comms.lock().unwrap() = Some(EmuComms {
|
||||
sender,
|
||||
receiver: renderer_receiver,
|
||||
});
|
||||
|
||||
let (serial_tx, gb_serial_rx) = mpsc::channel::<u8>();
|
||||
let serial_target = SerialTarget::Custom {
|
||||
rx: Some(gb_serial_rx),
|
||||
tx: None,
|
||||
};
|
||||
|
||||
let will_skip_bootrom =
|
||||
configs.vst_config.force_skip_bootrom || !configs.emu_config.show_bootrom;
|
||||
|
||||
let mut emulator_core = {
|
||||
let options = EmulatorOptions::new_with_config(
|
||||
configs.emu_config.clone(),
|
||||
configs.config_dir.clone(),
|
||||
Some(emu_sender),
|
||||
rom,
|
||||
output,
|
||||
)
|
||||
.with_serial_target(serial_target)
|
||||
.with_sram_buffer(self.params.sram_save.state.clone())
|
||||
.with_show_bootrom(!will_skip_bootrom);
|
||||
|
||||
EmulatorCore::init(false, receiver, options)
|
||||
.expect("couldn't initialize emulator core!")
|
||||
};
|
||||
|
||||
emulator_core.run_until_buffer_full();
|
||||
|
||||
self.vars = Some(EmuVars {
|
||||
rx,
|
||||
emulator_core,
|
||||
serial_tx,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn deactivate(&mut self) {
|
||||
if let Ok(comms) = self.emu_comms.lock() {
|
||||
if let Some(ref comms) = *comms {
|
||||
match comms.sender.send(EmulatorMessage::Exit) {
|
||||
Ok(_) => self.vars = None,
|
||||
Err(e) => nih_log!("error {e} sending message to emulator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vst3Plugin for GameboyEmu {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula";
|
||||
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
||||
&[Vst3SubCategory::Instrument, Vst3SubCategory::Synth];
|
||||
}
|
||||
|
||||
nih_export_vst3!(GameboyEmu);
|
|
@ -1,239 +0,0 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use baseview::{
|
||||
Event, EventStatus, Size, Window, WindowEvent, WindowHandle, WindowHandler, WindowOpenOptions,
|
||||
};
|
||||
use gb_emu_lib::{
|
||||
connect::{JoypadButtons, JoypadState, RendererMessage, ResolutionData, HEIGHT, WIDTH},
|
||||
renderer::{ActiveBackend, RendererBackend, RendererBackendManager},
|
||||
util::PrintErrors,
|
||||
};
|
||||
use keyboard_types::{Code, KeyState};
|
||||
use nih_plug::prelude::*;
|
||||
use raw_window_handle::HasDisplayHandle;
|
||||
|
||||
use super::{access_config, EmuComms};
|
||||
|
||||
pub struct TwincEditor {
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl TwincEditor {
|
||||
pub fn new(emu_comms: Arc<Mutex<Option<EmuComms>>>, size: Size) -> Self {
|
||||
Self { emu_comms, size }
|
||||
}
|
||||
}
|
||||
|
||||
impl Editor for TwincEditor {
|
||||
fn spawn(
|
||||
&self,
|
||||
parent: ParentWindowHandle,
|
||||
_context: Arc<dyn GuiContext>,
|
||||
) -> Box<dyn std::any::Any + Send> {
|
||||
let rr_cloned = self.emu_comms.clone();
|
||||
|
||||
// let (size, scale) = if cfg!(target_os = "macos") {
|
||||
// (
|
||||
// Size::new((WIDTH * EXTRA_SCALE) as f64, (HEIGHT * EXTRA_SCALE) as f64),
|
||||
// baseview::WindowScalePolicy::SystemScaleFactor,
|
||||
// )
|
||||
// } else {
|
||||
// (
|
||||
// Size::new(WIDTH as f64, HEIGHT as f64),
|
||||
// baseview::WindowScalePolicy::ScaleFactor(EXTRA_SCALE as f64),
|
||||
// )
|
||||
// };
|
||||
|
||||
let config = access_config();
|
||||
|
||||
let shader_path = {
|
||||
if super::IS_CGB.get().is_some_and(|v| *v) {
|
||||
config.emu_config.vulkan_config.cgb_shader_path.as_ref()
|
||||
} else {
|
||||
config.emu_config.vulkan_config.dmg_shader_path.as_ref()
|
||||
}
|
||||
.map(|p| config.config_dir.join(p))
|
||||
};
|
||||
|
||||
let scale_factor = config.vst_config.scale_factor;
|
||||
|
||||
let size = Size::new(
|
||||
(WIDTH * scale_factor) as f64,
|
||||
(HEIGHT * scale_factor) as f64,
|
||||
);
|
||||
|
||||
let window = Window::open_parented(
|
||||
&parent,
|
||||
WindowOpenOptions {
|
||||
title: String::from("gb-emu"),
|
||||
size,
|
||||
scale: baseview::WindowScalePolicy::SystemScaleFactor,
|
||||
gl_config: Default::default(),
|
||||
},
|
||||
move |window| {
|
||||
let manager = Arc::new(
|
||||
<ActiveBackend as RendererBackend>::RendererBackendManager::new(
|
||||
window.display_handle().unwrap(),
|
||||
),
|
||||
);
|
||||
TwincEditorWindow::new(window, rr_cloned, manager, size, shader_path)
|
||||
},
|
||||
);
|
||||
|
||||
Box::new(TwincEditorWindowHandle { window })
|
||||
}
|
||||
|
||||
fn size(&self) -> (u32, u32) {
|
||||
(self.size.width as u32, self.size.height as u32)
|
||||
}
|
||||
|
||||
fn set_scale_factor(&self, _factor: f32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {}
|
||||
|
||||
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {}
|
||||
|
||||
fn param_values_changed(&self) {}
|
||||
}
|
||||
|
||||
struct TwincEditorWindowHandle {
|
||||
window: WindowHandle,
|
||||
}
|
||||
|
||||
unsafe impl Send for TwincEditorWindowHandle {}
|
||||
|
||||
impl Drop for TwincEditorWindowHandle {
|
||||
fn drop(&mut self) {
|
||||
self.window.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TwincEditorWindow {
|
||||
renderer: ActiveBackend,
|
||||
manager: Arc<<ActiveBackend as RendererBackend>::RendererBackendManager>,
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
joypad_state: JoypadState,
|
||||
latest_buf: Vec<[u8; 4]>,
|
||||
current_resolution: ResolutionData,
|
||||
}
|
||||
|
||||
impl TwincEditorWindow {
|
||||
fn new(
|
||||
window: &mut Window,
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
manager: Arc<<ActiveBackend as RendererBackend>::RendererBackendManager>,
|
||||
size: Size,
|
||||
shader_path: Option<std::path::PathBuf>,
|
||||
) -> Self {
|
||||
let current_resolution = ResolutionData {
|
||||
real_width: size.width as u32,
|
||||
real_height: size.height as u32,
|
||||
scaled_width: WIDTH as u32,
|
||||
scaled_height: HEIGHT as u32,
|
||||
};
|
||||
|
||||
let renderer =
|
||||
RendererBackend::new(current_resolution, window, shader_path, manager.clone()).unwrap();
|
||||
|
||||
Self {
|
||||
renderer,
|
||||
manager,
|
||||
emu_comms,
|
||||
joypad_state: Default::default(),
|
||||
latest_buf: Vec::new(),
|
||||
current_resolution,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_events(&mut self) {
|
||||
if let Ok(comms) = self.emu_comms.lock() {
|
||||
if let Some(ref comms) = *comms {
|
||||
while let Ok(e) = comms.receiver.try_recv() {
|
||||
match e {
|
||||
RendererMessage::Prepare {
|
||||
width: _,
|
||||
height: _,
|
||||
} => {}
|
||||
RendererMessage::Resize {
|
||||
width: _,
|
||||
height: _,
|
||||
} => {}
|
||||
RendererMessage::Display { buffer } => self.latest_buf = buffer,
|
||||
RendererMessage::SetTitle { title: _ } => {}
|
||||
RendererMessage::Rumble { rumble: _ } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowHandler for TwincEditorWindow {
|
||||
fn on_frame(&mut self, _window: &mut Window) {
|
||||
self.process_events();
|
||||
|
||||
if self.latest_buf.len()
|
||||
== (self.current_resolution.scaled_height * self.current_resolution.scaled_width)
|
||||
as usize
|
||||
{
|
||||
self.renderer.new_frame(&self.latest_buf).some_or_print();
|
||||
}
|
||||
|
||||
self.renderer
|
||||
.render(self.current_resolution, &self.manager)
|
||||
.some_or_print();
|
||||
}
|
||||
|
||||
fn on_event(&mut self, window: &mut Window, event: baseview::Event) -> EventStatus {
|
||||
match event {
|
||||
Event::Window(WindowEvent::Resized(info)) => {
|
||||
let physical_size = info.physical_size();
|
||||
self.current_resolution = ResolutionData {
|
||||
real_width: physical_size.width,
|
||||
real_height: physical_size.height,
|
||||
scaled_width: WIDTH as u32,
|
||||
scaled_height: HEIGHT as u32,
|
||||
};
|
||||
self.renderer
|
||||
.resize(self.current_resolution, window)
|
||||
.some_or_print();
|
||||
EventStatus::Captured
|
||||
}
|
||||
Event::Keyboard(event) => {
|
||||
let status = event.state == KeyState::Down;
|
||||
if let Some(button) = match event.code {
|
||||
Code::Equal => Some(JoypadButtons::Start),
|
||||
Code::Minus => Some(JoypadButtons::Select),
|
||||
Code::Quote => Some(JoypadButtons::A),
|
||||
Code::Semicolon => Some(JoypadButtons::B),
|
||||
Code::KeyW | Code::ArrowUp => Some(JoypadButtons::Up),
|
||||
Code::KeyA | Code::ArrowLeft => Some(JoypadButtons::Left),
|
||||
Code::KeyS | Code::ArrowDown => Some(JoypadButtons::Down),
|
||||
Code::KeyD | Code::ArrowRight => Some(JoypadButtons::Right),
|
||||
_ => None,
|
||||
} {
|
||||
self.joypad_state.set(button, status);
|
||||
if let Ok(comms) = self.emu_comms.lock() {
|
||||
if let Some(ref comms) = *comms {
|
||||
match comms.sender.send(
|
||||
gb_emu_lib::connect::EmulatorMessage::JoypadUpdate(
|
||||
self.joypad_state,
|
||||
),
|
||||
) {
|
||||
Ok(_) => {}
|
||||
Err(e) => nih_error!("error sending joypad update: {e:#?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
EventStatus::Captured
|
||||
} else {
|
||||
EventStatus::Ignored
|
||||
}
|
||||
}
|
||||
_ => EventStatus::Ignored,
|
||||
}
|
||||
}
|
||||
}
|
265
gb-vst/src/ui.rs
Normal file
265
gb-vst/src/ui.rs
Normal file
|
@ -0,0 +1,265 @@
|
|||
use std::sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use baseview::{
|
||||
Event, EventStatus, Size, Window, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions,
|
||||
};
|
||||
use gb_emu_lib::{
|
||||
connect::{JoypadButtons, JoypadState, Renderer, HEIGHT, WIDTH},
|
||||
util::scale_buffer_in_place,
|
||||
};
|
||||
use keyboard_types::{Code, KeyState};
|
||||
use nih_plug::prelude::*;
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
|
||||
use crate::{FrameReceiver, JoypadSender};
|
||||
|
||||
pub struct Emulator {
|
||||
frame_receiver: Arc<FrameReceiver>,
|
||||
joypad_sender: Arc<JoypadSender>,
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
pub fn new(frame_receiver: Arc<FrameReceiver>, joypad_sender: Arc<JoypadSender>) -> Self {
|
||||
nih_warn!("new emulator rx/tx struct");
|
||||
Self {
|
||||
frame_receiver,
|
||||
joypad_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const EXTRA_SCALE: usize = 3;
|
||||
const S_WIDTH: usize = WIDTH * EXTRA_SCALE;
|
||||
const S_HEIGHT: usize = HEIGHT * EXTRA_SCALE;
|
||||
|
||||
impl Editor for Emulator {
|
||||
fn spawn(
|
||||
&self,
|
||||
parent: ParentWindowHandle,
|
||||
_context: Arc<dyn GuiContext>,
|
||||
) -> Box<dyn std::any::Any + Send> {
|
||||
nih_warn!("spawning editor");
|
||||
|
||||
let fr_cloned = self.frame_receiver.clone();
|
||||
let js_cloned = self.joypad_sender.clone();
|
||||
|
||||
let (size, scale) = if cfg!(target_os = "macos") {
|
||||
(
|
||||
Size::new(S_WIDTH as f64, S_HEIGHT as f64),
|
||||
baseview::WindowScalePolicy::SystemScaleFactor,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Size::new(WIDTH as f64, HEIGHT as f64),
|
||||
baseview::WindowScalePolicy::ScaleFactor(EXTRA_SCALE as f64),
|
||||
)
|
||||
};
|
||||
|
||||
nih_warn!("opening window");
|
||||
Window::open_parented(
|
||||
&parent,
|
||||
WindowOpenOptions {
|
||||
title: String::from("gb-emu"),
|
||||
size,
|
||||
scale,
|
||||
gl_config: None,
|
||||
},
|
||||
|w| EmulatorWindow::new(w, fr_cloned, js_cloned),
|
||||
);
|
||||
Box::new(Self::new(
|
||||
self.frame_receiver.clone(),
|
||||
self.joypad_sender.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
fn size(&self) -> (u32, u32) {
|
||||
nih_warn!("editor size");
|
||||
((WIDTH * EXTRA_SCALE) as u32, (HEIGHT * EXTRA_SCALE) as u32)
|
||||
}
|
||||
|
||||
fn set_scale_factor(&self, _factor: f32) -> bool {
|
||||
nih_warn!("editor scale factor");
|
||||
true
|
||||
}
|
||||
|
||||
fn param_value_changed(&self, _id: &str, _normalized_value: f32) {
|
||||
nih_warn!("editor param value changed");
|
||||
}
|
||||
|
||||
fn param_modulation_changed(&self, _id: &str, _modulation_offset: f32) {
|
||||
nih_warn!("editor param modulator changed");
|
||||
}
|
||||
|
||||
fn param_values_changed(&self) {
|
||||
nih_warn!("editor param valueS changed");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmulatorWindow {
|
||||
pix: Pixels,
|
||||
scale: usize,
|
||||
scaled_buf: Vec<[u8; 4]>,
|
||||
frame_receiver: Arc<FrameReceiver>,
|
||||
joypad_sender: Arc<JoypadSender>,
|
||||
}
|
||||
|
||||
impl EmulatorWindow {
|
||||
fn new(
|
||||
window: &mut Window,
|
||||
frame_receiver: Arc<FrameReceiver>,
|
||||
joypad_sender: Arc<JoypadSender>,
|
||||
) -> Self {
|
||||
nih_warn!("creating emulatorwindow");
|
||||
let info = WindowInfo::from_logical_size(
|
||||
Size::new(WIDTH as f64, HEIGHT as f64),
|
||||
EXTRA_SCALE as f64,
|
||||
);
|
||||
|
||||
let (pix, scale, scaled_buf) = init_pixbuf(info, window);
|
||||
|
||||
Self {
|
||||
pix,
|
||||
scale,
|
||||
scaled_buf,
|
||||
frame_receiver,
|
||||
joypad_sender,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_pixbuf(info: WindowInfo, window: &mut Window) -> (Pixels, usize, Vec<[u8; 4]>) {
|
||||
nih_warn!("initializing pixbuf");
|
||||
let physical_size = info.physical_size();
|
||||
let scale = (physical_size.width as usize / WIDTH).min(physical_size.height as usize / HEIGHT);
|
||||
let scaled_buf = vec![[0, 0, 0, 0xFF]; WIDTH * scale * HEIGHT * scale];
|
||||
(
|
||||
pixels::Pixels::new(
|
||||
physical_size.width,
|
||||
physical_size.height,
|
||||
SurfaceTexture::new(physical_size.width, physical_size.height, window),
|
||||
)
|
||||
.expect("could not init pixbuf"),
|
||||
scale,
|
||||
scaled_buf,
|
||||
)
|
||||
}
|
||||
|
||||
impl WindowHandler for EmulatorWindow {
|
||||
fn on_frame(&mut self, _window: &mut Window) {
|
||||
nih_warn!("rendering window frame");
|
||||
if let Some(ref mut receiver) = *self.frame_receiver.lock().expect("failed to lock mutex") {
|
||||
nih_warn!("...got frame receiver");
|
||||
if let Some(ref buf) = receiver.try_iter().last() {
|
||||
nih_warn!("...got frame");
|
||||
if self.scale != 1 {
|
||||
scale_buffer_in_place(buf, &mut self.scaled_buf, WIDTH, HEIGHT, self.scale);
|
||||
}
|
||||
for (pixel, source) in self
|
||||
.pix
|
||||
.get_frame_mut()
|
||||
.chunks_exact_mut(4)
|
||||
.zip(&self.scaled_buf)
|
||||
{
|
||||
pixel.copy_from_slice(source);
|
||||
}
|
||||
self.pix
|
||||
.render()
|
||||
.expect("could not render pixbuf to window");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_event(&mut self, window: &mut Window, event: baseview::Event) -> EventStatus {
|
||||
nih_warn!("window event");
|
||||
match event {
|
||||
Event::Window(WindowEvent::Resized(info)) => {
|
||||
(self.pix, self.scale, self.scaled_buf) = init_pixbuf(info, window);
|
||||
EventStatus::Captured
|
||||
}
|
||||
Event::Keyboard(event) => {
|
||||
let status = event.state == KeyState::Down;
|
||||
if let Some(button) = match event.code {
|
||||
Code::Equal => Some(JoypadButtons::Start),
|
||||
Code::Minus => Some(JoypadButtons::Select),
|
||||
Code::Quote => Some(JoypadButtons::A),
|
||||
Code::Semicolon => Some(JoypadButtons::B),
|
||||
Code::KeyW | Code::ArrowUp => Some(JoypadButtons::Up),
|
||||
Code::KeyA | Code::ArrowLeft => Some(JoypadButtons::Left),
|
||||
Code::KeyS | Code::ArrowDown => Some(JoypadButtons::Down),
|
||||
Code::KeyD | Code::ArrowRight => Some(JoypadButtons::Right),
|
||||
_ => None,
|
||||
} {
|
||||
if let Some(ref mut sender) =
|
||||
*self.joypad_sender.lock().expect("failed to lock mutex")
|
||||
{
|
||||
sender
|
||||
.send((button, status))
|
||||
.expect("could not send button status");
|
||||
}
|
||||
EventStatus::Captured
|
||||
} else {
|
||||
EventStatus::Ignored
|
||||
}
|
||||
}
|
||||
_ => EventStatus::Ignored,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EmulatorRenderer {
|
||||
tx: Sender<Vec<[u8; 4]>>,
|
||||
joypad: JoypadState,
|
||||
keys: Receiver<(JoypadButtons, bool)>,
|
||||
}
|
||||
|
||||
impl EmulatorRenderer {
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(super) fn new() -> (Self, Receiver<Vec<[u8; 4]>>, Sender<(JoypadButtons, bool)>) {
|
||||
nih_warn!("creating emulator renderer");
|
||||
let (tx, rx) = mpsc::channel::<Vec<[u8; 4]>>();
|
||||
let (keys_tx, keys) = mpsc::channel::<(JoypadButtons, bool)>();
|
||||
(
|
||||
Self {
|
||||
tx,
|
||||
joypad: JoypadState::default(),
|
||||
keys,
|
||||
},
|
||||
rx,
|
||||
keys_tx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Renderer<[u8; 4]> for EmulatorRenderer {
|
||||
fn prepare(&mut self, _width: usize, _height: usize) {
|
||||
nih_warn!("preparing emulator");
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
fn display(&mut self, buffer: &[[u8; 4]]) {
|
||||
nih_warn!("sending frame from emulator thread");
|
||||
self.tx.send(buffer.to_vec());
|
||||
nih_warn!("...finished sending frame");
|
||||
}
|
||||
|
||||
fn latest_joypad_state(&mut self) -> JoypadState {
|
||||
nih_warn!("begin getting latest joypad state");
|
||||
while let Ok((key, state)) = self.keys.try_recv() {
|
||||
match key {
|
||||
JoypadButtons::Down => self.joypad.down = state,
|
||||
JoypadButtons::Up => self.joypad.up = state,
|
||||
JoypadButtons::Left => self.joypad.left = state,
|
||||
JoypadButtons::Right => self.joypad.right = state,
|
||||
JoypadButtons::Start => self.joypad.start = state,
|
||||
JoypadButtons::Select => self.joypad.select = state,
|
||||
JoypadButtons::B => self.joypad.b = state,
|
||||
JoypadButtons::A => self.joypad.a = state,
|
||||
}
|
||||
}
|
||||
nih_warn!("end getting latest joypad state");
|
||||
self.joypad
|
||||
}
|
||||
}
|
10
gb-vst/xtask/Cargo.toml
Normal file
10
gb-vst/xtask/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "xtask"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nih_plug_xtask = { path = "../../vendored/nih-plug/nih_plug_xtask" }
|
||||
|
3
gb-vst/xtask/src/main.rs
Normal file
3
gb-vst/xtask/src/main.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() -> nih_plug_xtask::Result<()> {
|
||||
nih_plug_xtask::main()
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
[package]
|
||||
name = "gui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "TWINC Game Boy (CGB/DMG) emulator GUI"
|
||||
|
||||
[package.metadata.bundle]
|
||||
identifier = "com.alexjanka.TWINC.gui"
|
||||
osx_file_extensions = [[["Game Boy ROM", "Viewer"], ["gb", "gbc"]]]
|
||||
|
||||
[features]
|
||||
default = ["wgpu", "macos-ui", "crossplatform-ui"]
|
||||
macos-ui = ["cacao", "objc", "uuid"]
|
||||
crossplatform-ui = ["gtk", "adw", "glib-build-tools"]
|
||||
force-crossplatform-ui = ["crossplatform-ui"]
|
||||
wgpu = ["frontend-common/wgpu"]
|
||||
pixels = ["frontend-common/pixels"]
|
||||
vulkan = ["frontend-common/vulkan"]
|
||||
|
||||
[dependencies]
|
||||
adw = { version = "0.7.0", package = "libadwaita", features = [
|
||||
"v1_4",
|
||||
"gtk_v4_6",
|
||||
], optional = true }
|
||||
frontend-common = { workspace = true }
|
||||
gb-emu-lib = { workspace = true }
|
||||
gtk = { version = "0.9.0", package = "gtk4", features = [
|
||||
"v4_12",
|
||||
], optional = true }
|
||||
twinc_emu_vst = { path = "../gb-vst", default-features = false }
|
||||
raw-window-handle = { workspace = true }
|
||||
cpal = "0.15.3"
|
||||
log = { workspace = true }
|
||||
env_logger = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
anyhow = "1.0.86"
|
||||
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
cacao = { git = "https://git.alexjanka.com/alex/cacao", optional = true }
|
||||
objc = { version = "=0.3.0-beta.3", package = "objc2", optional = true }
|
||||
uuid = { version = "1.10.0", features = ["v4", "fast-rng"], optional = true }
|
||||
|
||||
[build-dependencies]
|
||||
glib-build-tools = { version = "0.20.0", optional = true }
|
11
gui/build.rs
11
gui/build.rs
|
@ -1,11 +0,0 @@
|
|||
fn main() {
|
||||
#[cfg(not(all(
|
||||
target_os = "macos",
|
||||
all(feature = "macos-ui", not(feature = "force-crossplatform-ui"))
|
||||
)))]
|
||||
glib_build_tools::compile_resources(
|
||||
&["src/crossplatform/resources"],
|
||||
"src/crossplatform/resources/resources.gresource.xml",
|
||||
"crossplatform_templates.gresource",
|
||||
);
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use gb_emu_lib::config::NamedConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GuiConfig {
|
||||
pub tile_window: bool,
|
||||
pub layer_window: bool,
|
||||
pub games_dir: Option<PathBuf>,
|
||||
pub recursive: bool,
|
||||
}
|
||||
|
||||
impl NamedConfig for GuiConfig {
|
||||
fn name() -> String {
|
||||
String::from("gui")
|
||||
}
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
use std::{cell::RefCell, path::PathBuf};
|
||||
|
||||
use glib::Properties;
|
||||
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||
|
||||
#[derive(Properties, Default)]
|
||||
#[properties(wrapper_type = super::GameListEntryObject)]
|
||||
pub struct GameListEntryObject {
|
||||
#[property(get, set)]
|
||||
name: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
path: RefCell<PathBuf>,
|
||||
#[property(get, set)]
|
||||
title: RefCell<String>,
|
||||
#[property(get, set)]
|
||||
mbc: RefCell<String>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for GameListEntryObject {
|
||||
const NAME: &'static str = "MyGtkAppGameListEntryObject";
|
||||
type Type = super::GameListEntryObject;
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for GameListEntryObject {}
|
|
@ -1,21 +0,0 @@
|
|||
mod imp;
|
||||
|
||||
use glib::Object;
|
||||
use gtk::glib;
|
||||
|
||||
use crate::gamelist::GameListEntry;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct GameListEntryObject(ObjectSubclass<imp::GameListEntryObject>);
|
||||
}
|
||||
|
||||
impl GameListEntryObject {
|
||||
pub fn new(entry: GameListEntry) -> Self {
|
||||
Object::builder()
|
||||
.property("name", entry.name)
|
||||
.property("path", entry.path)
|
||||
.property("title", entry.header.title)
|
||||
.property("mbc", entry.header.cartridge_type.to_string())
|
||||
.build()
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use std::cell::RefCell;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use glib::subclass::InitializingObject;
|
||||
use gtk::{
|
||||
gio, glib, ColumnView, ColumnViewColumn, CompositeTemplate, ScrolledWindow, SingleSelection,
|
||||
SortListModel,
|
||||
};
|
||||
|
||||
use crate::crossplatform::{game_list_entry::GameListEntryObject, GameListEntryColumn};
|
||||
|
||||
#[derive(CompositeTemplate, Default)]
|
||||
#[template(resource = "/com/alexjanka/TWINC/game_list_entry_display.ui")]
|
||||
pub struct GameListWindow {
|
||||
#[template_child]
|
||||
pub gamelist: TemplateChild<ScrolledWindow>,
|
||||
pub games: RefCell<Vec<GameListEntryObject>>,
|
||||
#[template_child]
|
||||
pub columnview: TemplateChild<ColumnView>,
|
||||
#[template_child]
|
||||
pub title_column: TemplateChild<ColumnViewColumn>,
|
||||
#[template_child]
|
||||
pub filename_column: TemplateChild<ColumnViewColumn>,
|
||||
#[template_child]
|
||||
pub mbc_column: TemplateChild<ColumnViewColumn>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for GameListWindow {
|
||||
const NAME: &'static str = "TwincGameList";
|
||||
type Type = super::GameListWindow;
|
||||
type ParentType = gtk::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for GameListWindow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
self.obj().set_game_list();
|
||||
|
||||
let model = gio::ListStore::new::<GameListEntryObject>();
|
||||
model.extend_from_slice(&self.games.borrow());
|
||||
|
||||
let f1 = GameListEntryColumn::new("title");
|
||||
let f2 = GameListEntryColumn::new("name");
|
||||
let f3 = GameListEntryColumn::new("mbc");
|
||||
|
||||
self.title_column.set_factory(Some(&f1.factory));
|
||||
self.title_column.set_sorter(Some(&f1.sorter));
|
||||
self.filename_column.set_factory(Some(&f2.factory));
|
||||
self.filename_column.set_sorter(Some(&f2.sorter));
|
||||
self.mbc_column.set_factory(Some(&f3.factory));
|
||||
self.mbc_column.set_sorter(Some(&f3.sorter));
|
||||
|
||||
self.columnview
|
||||
.sort_by_column(Some(&self.filename_column), gtk::SortType::Ascending);
|
||||
|
||||
let sort_model = SortListModel::new(Some(model), Some(self.columnview.sorter().unwrap()));
|
||||
|
||||
let selection_model = SingleSelection::new(Some(sort_model));
|
||||
|
||||
self.columnview.set_model(Some(&selection_model));
|
||||
|
||||
self.columnview.connect_activate(move |val, _| {
|
||||
log::info!(
|
||||
"activated: {:?}",
|
||||
val.model()
|
||||
.unwrap()
|
||||
.downcast_ref::<SingleSelection>()
|
||||
.unwrap()
|
||||
.selected_item()
|
||||
.and_then(move |v| v.downcast::<GameListEntryObject>().ok())
|
||||
.unwrap()
|
||||
.path()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for GameListWindow {}
|
||||
|
||||
impl WindowImpl for GameListWindow {}
|
||||
|
||||
impl ApplicationWindowImpl for GameListWindow {}
|
||||
|
||||
impl AdwApplicationWindowImpl for GameListWindow {}
|
|
@ -1,38 +0,0 @@
|
|||
mod imp;
|
||||
|
||||
use gb_emu_lib::config::CONFIG_MANAGER;
|
||||
use glib::Object;
|
||||
use gtk::{
|
||||
gio,
|
||||
glib::{self, subclass::types::ObjectSubclassIsExt},
|
||||
};
|
||||
|
||||
use crate::{config::GuiConfig, gamelist::load_games};
|
||||
|
||||
use super::game_list_entry::GameListEntryObject;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct GameListWindow(ObjectSubclass<imp::GameListWindow>)
|
||||
@extends gtk::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
impl GameListWindow {
|
||||
pub fn new(app: &adw::Application) -> Self {
|
||||
Object::builder().property("application", app).build()
|
||||
}
|
||||
|
||||
pub fn set_game_list(&self) {
|
||||
self.imp().games.replace(load_and_parse_games());
|
||||
}
|
||||
}
|
||||
|
||||
fn load_and_parse_games() -> Vec<GameListEntryObject> {
|
||||
let config: GuiConfig = CONFIG_MANAGER.load_or_create_config();
|
||||
load_games(config.games_dir.unwrap(), config.recursive)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(GameListEntryObject::new)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use adw::{prelude::*, Application};
|
||||
use gtk::{gio, glib::ExitCode, CustomSorter, Label, ListItem, SignalListItemFactory};
|
||||
use thiserror::Error;
|
||||
|
||||
use self::{game_list_entry::GameListEntryObject, game_list_entry_display::GameListWindow};
|
||||
|
||||
mod game_list_entry;
|
||||
mod game_list_entry_display;
|
||||
|
||||
const APP_ID: &str = "com.alexjanka.TWINC.gui";
|
||||
|
||||
pub fn run() -> Result<(), CrossplatformUiError> {
|
||||
gio::resources_register_include!("crossplatform_templates.gresource")?;
|
||||
let app = Application::builder().application_id(APP_ID).build();
|
||||
|
||||
app.connect_activate(build_ui);
|
||||
|
||||
match app.run().value() {
|
||||
v if v == ExitCode::SUCCESS.value() => Ok(()),
|
||||
val => Err(CrossplatformUiError::GtkError(val)),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_ui(app: &Application) {
|
||||
let window = GameListWindow::new(app);
|
||||
window.present();
|
||||
}
|
||||
|
||||
struct GameListEntryColumn {
|
||||
factory: SignalListItemFactory,
|
||||
sorter: CustomSorter,
|
||||
}
|
||||
|
||||
impl GameListEntryColumn {
|
||||
fn new<T: ToString>(bind_to: T) -> Self {
|
||||
let factory = SignalListItemFactory::new();
|
||||
factory.connect_setup(move |_, list_item| {
|
||||
let label = Label::builder()
|
||||
.margin_top(2)
|
||||
.margin_bottom(2)
|
||||
.margin_start(4)
|
||||
.margin_end(4)
|
||||
.build();
|
||||
|
||||
list_item
|
||||
.downcast_ref::<ListItem>()
|
||||
.unwrap()
|
||||
.set_child(Some(&label));
|
||||
});
|
||||
let bind_to = bind_to.to_string();
|
||||
|
||||
{
|
||||
let bind_to = bind_to.clone();
|
||||
factory.connect_bind(move |_, list_item| {
|
||||
let inner_object = list_item
|
||||
.downcast_ref::<ListItem>()
|
||||
.unwrap()
|
||||
.item()
|
||||
.and_downcast::<GameListEntryObject>()
|
||||
.unwrap();
|
||||
|
||||
let label = list_item
|
||||
.downcast_ref::<ListItem>()
|
||||
.unwrap()
|
||||
.child()
|
||||
.and_downcast::<Label>()
|
||||
.unwrap();
|
||||
|
||||
label.set_label(&inner_object.property::<String>(&bind_to));
|
||||
});
|
||||
}
|
||||
|
||||
let sorter = gtk::CustomSorter::new(move |left, right| {
|
||||
let left = left.downcast_ref::<GameListEntryObject>().unwrap();
|
||||
let right = right.downcast_ref::<GameListEntryObject>().unwrap();
|
||||
let left_val: String = left.property(&bind_to);
|
||||
let right_val: String = right.property(&bind_to);
|
||||
|
||||
left_val
|
||||
.to_lowercase()
|
||||
.cmp(&right_val.to_lowercase())
|
||||
.into()
|
||||
});
|
||||
|
||||
Self { factory, sorter }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CrossplatformUiError {
|
||||
#[error("GTK error")]
|
||||
GtkError(i32),
|
||||
#[error("glib error")]
|
||||
Glib(#[from] adw::glib::Error),
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<template class="TwincGameList" parent="GtkApplicationWindow">
|
||||
<property name="title">TWINC</property>
|
||||
<property name="default-width">1200</property>
|
||||
<property name="default-height">800</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="gamelist">
|
||||
<property name="margin-top">12</property>
|
||||
<property name="margin-bottom">12</property>
|
||||
<property name="margin-start">12</property>
|
||||
<property name="margin-end">12</property>
|
||||
<property name="hscrollbar-policy">GTK_POLICY_NEVER</property>
|
||||
<property name="propagate-natural-height">true</property>
|
||||
<property name="propagate-natural-width">true</property>
|
||||
<child>
|
||||
<object class="GtkColumnView" id="columnview">
|
||||
<child>
|
||||
<object class="GtkColumnViewColumn" id="title_column">
|
||||
<property name="title">Title</property>
|
||||
<property name="fixed-width">150</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkColumnViewColumn" id="filename_column">
|
||||
<property name="title">Filename</property>
|
||||
<property name="expand">true</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkColumnViewColumn" id="mbc_column">
|
||||
<property name="title">MBC</property>
|
||||
<property name="fixed-width">160</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<gresources>
|
||||
<gresource prefix="/com/alexjanka/TWINC/">
|
||||
<file compressed="true" preprocess="xml-stripblanks">game_list_entry_display.ui</file>
|
||||
</gresource>
|
||||
</gresources>
|
|
@ -1,41 +0,0 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
use gb_emu_lib::connect::{RomHeader, RomHeaderError};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GameListEntry {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub header: RomHeader,
|
||||
}
|
||||
|
||||
pub fn load_games<P: AsRef<Path>>(
|
||||
game_dir: P,
|
||||
recursive: bool,
|
||||
) -> Result<Vec<GameListEntry>, GameListError> {
|
||||
let mut games = Vec::new();
|
||||
for entry in std::fs::read_dir(game_dir)?.flatten() {
|
||||
if recursive && entry.file_type()?.is_dir() {
|
||||
if let Ok(mut recursed) = load_games(entry.path(), true) {
|
||||
games.append(&mut recursed);
|
||||
}
|
||||
} else if entry.file_type()?.is_file() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
let path = entry.path();
|
||||
if name.ends_with("gb") || name.ends_with("gbc") {
|
||||
let header = RomHeader::parse(&std::fs::read(path.clone())?)?;
|
||||
games.push(GameListEntry { name, path, header });
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(games)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GameListError {
|
||||
#[error("fs error")]
|
||||
Fs(#[from] std::io::Error),
|
||||
#[error("rom header")]
|
||||
Header(#[from] RomHeaderError),
|
||||
}
|
|
@ -1,550 +0,0 @@
|
|||
use std::{
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{self, Receiver, Sender},
|
||||
Arc,
|
||||
},
|
||||
thread::JoinHandle,
|
||||
};
|
||||
|
||||
use cacao::{
|
||||
appkit::{
|
||||
window::{Window, WindowConfig, WindowDelegate, WindowStyle},
|
||||
Event, EventMask, EventMonitor,
|
||||
},
|
||||
filesystem::ModalResponse,
|
||||
foundation::{NSInteger, NSString},
|
||||
};
|
||||
use cpal::Stream;
|
||||
use frontend_common::{
|
||||
new_layer_window, new_tile_window,
|
||||
window::{RendererChannel, WindowManager, WindowType},
|
||||
};
|
||||
use gb_emu_lib::{
|
||||
connect::{EmulatorMessage, JoypadButtons, JoypadState, RendererMessage, ResolutionData},
|
||||
renderer::{ActiveBackend, RendererBackend, RendererBackendManager},
|
||||
util::PrintErrors,
|
||||
};
|
||||
use objc::{
|
||||
class, msg_send, msg_send_id,
|
||||
rc::{Id, Owned},
|
||||
runtime::Object,
|
||||
};
|
||||
use raw_window_handle::{
|
||||
AppKitDisplayHandle, AppKitWindowHandle, DisplayHandle, HasDisplayHandle, HasWindowHandle,
|
||||
RawDisplayHandle, RawWindowHandle, WindowHandle,
|
||||
};
|
||||
|
||||
use super::{dispatch, AppMessage, CoreMessage};
|
||||
|
||||
pub enum EmuWindowMessage {
|
||||
Closing {
|
||||
window_type: WindowType,
|
||||
},
|
||||
Renderer {
|
||||
window_type: WindowType,
|
||||
renderer_message: RendererMessage<[u8; 4]>,
|
||||
},
|
||||
KeyDown(JoypadButtons),
|
||||
KeyUp(JoypadButtons),
|
||||
}
|
||||
|
||||
pub struct EmulatorHandles {
|
||||
sender: Sender<EmulatorMessage<[u8; 4]>>,
|
||||
emulator_thread: Option<JoinHandle<()>>,
|
||||
_stream: Stream,
|
||||
}
|
||||
|
||||
impl EmulatorHandles {
|
||||
fn new(sender: Sender<EmulatorMessage<[u8; 4]>>, stream: Stream) -> Self {
|
||||
Self {
|
||||
sender,
|
||||
emulator_thread: None,
|
||||
_stream: stream,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_thread_handle(&mut self, handle: JoinHandle<()>) {
|
||||
self.emulator_thread = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for EmulatorHandles {
|
||||
fn drop(&mut self) {
|
||||
if let Some(handle) = self.emulator_thread.take() {
|
||||
self.sender.send(EmulatorMessage::Exit).unwrap();
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ButtonHandler {
|
||||
_down_monitor: EventMonitor,
|
||||
_up_monitor: EventMonitor,
|
||||
}
|
||||
|
||||
impl ButtonHandler {
|
||||
fn new() -> Self {
|
||||
let _down_monitor = Event::local_monitor(EventMask::KeyDown, |v| {
|
||||
if let Some(button) = get_buttons(&v) {
|
||||
dispatch(AppMessage::EmuWindow(EmuWindowMessage::KeyDown(button)));
|
||||
None
|
||||
} else {
|
||||
Some(v)
|
||||
}
|
||||
});
|
||||
let _up_monitor = Event::local_monitor(EventMask::KeyUp, |v| {
|
||||
if let Some(button) = get_buttons(&v) {
|
||||
dispatch(AppMessage::EmuWindow(EmuWindowMessage::KeyUp(button)));
|
||||
None
|
||||
} else {
|
||||
Some(v)
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
_down_monitor,
|
||||
_up_monitor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait GetKeyChar {
|
||||
fn get_key_char(&self) -> char;
|
||||
}
|
||||
|
||||
impl GetKeyChar for JoypadButtons {
|
||||
fn get_key_char(&self) -> char {
|
||||
match self {
|
||||
JoypadButtons::Down => 's',
|
||||
JoypadButtons::Up => 'w',
|
||||
JoypadButtons::Left => 'a',
|
||||
JoypadButtons::Right => 'd',
|
||||
JoypadButtons::Start => '=',
|
||||
JoypadButtons::Select => '-',
|
||||
JoypadButtons::B => ';',
|
||||
JoypadButtons::A => '\'',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ALL_BUTTONS: [JoypadButtons; 8] = [
|
||||
JoypadButtons::Down,
|
||||
JoypadButtons::Up,
|
||||
JoypadButtons::Left,
|
||||
JoypadButtons::Right,
|
||||
JoypadButtons::Start,
|
||||
JoypadButtons::Select,
|
||||
JoypadButtons::B,
|
||||
JoypadButtons::A,
|
||||
];
|
||||
|
||||
fn get_buttons(event: &Event) -> Option<JoypadButtons> {
|
||||
let characters = event.characters();
|
||||
// i have no idea why i left this check in? what was i looking for?
|
||||
// if characters.len() != 1 {
|
||||
// panic!("ok that assumption was wrong lol. event characters CAN be != 1");
|
||||
// }
|
||||
|
||||
if event.current_modifier_flags().is_empty() {
|
||||
for button in ALL_BUTTONS {
|
||||
if characters.contains(button.get_key_char()) {
|
||||
return Some(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub struct CacaoWindowManager {
|
||||
backend_manager: Arc<<ActiveBackend as RendererBackend>::RendererBackendManager>,
|
||||
windows: HashMap<WindowType, Window<CacaoWindow>>,
|
||||
handles: Option<EmulatorHandles>,
|
||||
joypad_state: JoypadState,
|
||||
_button_handler: ButtonHandler,
|
||||
}
|
||||
|
||||
impl CacaoWindowManager {
|
||||
pub fn new(display_handle: DisplayHandle) -> Self {
|
||||
let _button_handler = ButtonHandler::new();
|
||||
|
||||
Self {
|
||||
backend_manager: Arc::new(
|
||||
<ActiveBackend as RendererBackend>::RendererBackendManager::new(display_handle),
|
||||
),
|
||||
windows: HashMap::new(),
|
||||
handles: None,
|
||||
joypad_state: Default::default(),
|
||||
_button_handler,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_emulator_running(&self) -> bool {
|
||||
self.handles.is_some()
|
||||
}
|
||||
|
||||
pub fn update_handles(&mut self, sender: Sender<EmulatorMessage<[u8; 4]>>, stream: Stream) {
|
||||
self.handles = Some(EmulatorHandles::new(sender, stream));
|
||||
}
|
||||
|
||||
pub fn set_thread_handle(&mut self, handle: JoinHandle<()>) {
|
||||
if let Some(ref mut handles) = self.handles {
|
||||
handles.set_thread_handle(handle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_open_new_rom(&mut self) -> bool {
|
||||
if if let Some(handles) = &self.handles {
|
||||
handles.sender.send(EmulatorMessage::Pause).unwrap();
|
||||
let title = NSString::new("Emulator running!");
|
||||
let message = NSString::new("Stopping may result in lost data");
|
||||
let close = NSString::new("Continue");
|
||||
let continue_ = NSString::new("Stop");
|
||||
let escape_keycode = NSString::new("\x1b");
|
||||
|
||||
let out: ModalResponse = unsafe {
|
||||
let mut alert: Id<Object, Owned> = msg_send_id![class!(NSAlert), new];
|
||||
let _: () = msg_send![&mut alert, setMessageText: &*title];
|
||||
let _: () = msg_send![&mut alert, setInformativeText: &*message];
|
||||
let _: () = msg_send![&mut alert, addButtonWithTitle: &*continue_];
|
||||
let close_button: cacao::foundation::id =
|
||||
msg_send![&mut alert, addButtonWithTitle: &*close];
|
||||
let _: () = msg_send![&mut *close_button, setKeyEquivalent: &*escape_keycode];
|
||||
|
||||
let out: NSInteger = msg_send![&*alert, runModal];
|
||||
out.into()
|
||||
};
|
||||
|
||||
handles.sender.send(EmulatorMessage::Start).unwrap();
|
||||
matches!(out, ModalResponse::FirstButtonReturned)
|
||||
} else {
|
||||
true
|
||||
} {
|
||||
let _ = self.handles.take();
|
||||
for (_, w) in self.windows.drain() {
|
||||
w.close();
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn message(&mut self, message: EmuWindowMessage) {
|
||||
match message {
|
||||
EmuWindowMessage::Closing { window_type } => {
|
||||
self.windows.remove(&window_type);
|
||||
match window_type {
|
||||
WindowType::Main => {
|
||||
let _ = self.handles.take();
|
||||
for (_, w) in self.windows.drain() {
|
||||
w.close();
|
||||
}
|
||||
}
|
||||
WindowType::Tile => {
|
||||
if self.is_emulator_running() {
|
||||
dispatch(AppMessage::Core(CoreMessage::SetTileWindow(false)))
|
||||
}
|
||||
}
|
||||
WindowType::Layer => {
|
||||
if self.is_emulator_running() {
|
||||
dispatch(AppMessage::Core(CoreMessage::SetLayerWindow(false)))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
EmuWindowMessage::Renderer {
|
||||
window_type,
|
||||
renderer_message,
|
||||
} => {
|
||||
if let Some(window) = self.windows.get_mut(&window_type) {
|
||||
if let Some(delegate) = window.delegate.as_mut() {
|
||||
delegate.message(
|
||||
renderer_message,
|
||||
Window {
|
||||
delegate: None,
|
||||
objc: window.objc.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
EmuWindowMessage::KeyDown(key) => {
|
||||
if let Some(handles) = self.handles.as_ref() {
|
||||
let old_state = self.joypad_state;
|
||||
self.joypad_state.set(key, true);
|
||||
if self.joypad_state != old_state {
|
||||
handles
|
||||
.sender
|
||||
.send(EmulatorMessage::JoypadUpdate(self.joypad_state))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
EmuWindowMessage::KeyUp(key) => {
|
||||
if let Some(handles) = self.handles.as_ref() {
|
||||
let old_state = self.joypad_state;
|
||||
self.joypad_state.set(key, false);
|
||||
if self.joypad_state != old_state {
|
||||
handles
|
||||
.sender
|
||||
.send(EmulatorMessage::JoypadUpdate(self.joypad_state))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_layer_window(&mut self, state: bool) {
|
||||
if state {
|
||||
let is_running = self.is_emulator_running();
|
||||
if is_running {
|
||||
let new_layer_window: Sender<RendererMessage<[u8; 4]>> =
|
||||
match new_layer_window(self) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
log::error!("couldn't create tile window: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(ref handles) = self.handles {
|
||||
handles
|
||||
.sender
|
||||
.send(EmulatorMessage::NewLayerWindow(new_layer_window))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tile_window(&mut self, state: bool) {
|
||||
if state {
|
||||
let is_running = self.is_emulator_running();
|
||||
if is_running {
|
||||
let new_tile_window = match new_tile_window(self) {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
log::error!("couldn't create tile window: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(ref handles) = self.handles {
|
||||
handles
|
||||
.sender
|
||||
.send(EmulatorMessage::NewTileWindow(new_tile_window))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowManager for CacaoWindowManager {
|
||||
fn add(
|
||||
&mut self,
|
||||
window_type: WindowType,
|
||||
factor: usize,
|
||||
shader_path: Option<std::path::PathBuf>,
|
||||
resizable: bool,
|
||||
) -> anyhow::Result<RendererChannel> {
|
||||
let (w, receiver) = CacaoWindow::new(
|
||||
window_type,
|
||||
factor,
|
||||
shader_path,
|
||||
self.backend_manager.clone(),
|
||||
);
|
||||
let window = Window::with(
|
||||
{
|
||||
let mut config = WindowConfig::default();
|
||||
config.set_initial_dimensions(0., 0., 800., 800.);
|
||||
|
||||
let mut styles = vec![
|
||||
WindowStyle::Miniaturizable,
|
||||
WindowStyle::Closable,
|
||||
WindowStyle::Titled,
|
||||
];
|
||||
if resizable {
|
||||
styles.push(WindowStyle::Resizable);
|
||||
}
|
||||
|
||||
config.set_styles(&styles);
|
||||
config
|
||||
},
|
||||
w,
|
||||
);
|
||||
window.show();
|
||||
self.windows
|
||||
.insert(window.delegate.as_ref().unwrap().window_type, window);
|
||||
Ok(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MonitorThread {
|
||||
state: MonitorThreadState,
|
||||
}
|
||||
|
||||
enum MonitorThreadState {
|
||||
Live {
|
||||
thread: JoinHandle<()>,
|
||||
destroy: Sender<bool>,
|
||||
},
|
||||
Destroyed,
|
||||
}
|
||||
|
||||
impl MonitorThread {
|
||||
fn new(receiver: Receiver<RendererMessage<[u8; 4]>>, window_type: WindowType) -> Self {
|
||||
let (destroy, destroy_recv) = mpsc::channel();
|
||||
let thread = std::thread::spawn(move || loop {
|
||||
if let Ok(renderer_message) = receiver.recv() {
|
||||
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Renderer {
|
||||
window_type,
|
||||
renderer_message,
|
||||
}));
|
||||
}
|
||||
if let Ok(true) = destroy_recv.try_recv() {
|
||||
return;
|
||||
}
|
||||
});
|
||||
Self {
|
||||
state: MonitorThreadState::Live { thread, destroy },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorThreadState {
|
||||
fn destroy(&mut self) {
|
||||
if let Self::Live { thread, destroy } = std::mem::replace(self, Self::Destroyed) {
|
||||
destroy.send(true).expect("Failed to kill monitor thread");
|
||||
thread.join().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MonitorThread {
|
||||
fn drop(&mut self) {
|
||||
self.state.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
struct CacaoWindow {
|
||||
window_type: WindowType,
|
||||
backend_manager: Arc<<ActiveBackend as RendererBackend>::RendererBackendManager>,
|
||||
backend: Option<ActiveBackend>,
|
||||
scale_factor: usize,
|
||||
shader_path: Option<PathBuf>,
|
||||
resolutions: ResolutionData,
|
||||
_channel_thread: MonitorThread,
|
||||
}
|
||||
|
||||
impl CacaoWindow {
|
||||
fn new(
|
||||
window_type: WindowType,
|
||||
scale_factor: usize,
|
||||
shader_path: Option<std::path::PathBuf>,
|
||||
backend_manager: Arc<<ActiveBackend as RendererBackend>::RendererBackendManager>,
|
||||
) -> (Self, RendererChannel) {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let channel_thread = MonitorThread::new(receiver, window_type);
|
||||
(
|
||||
Self {
|
||||
window_type,
|
||||
backend_manager,
|
||||
backend: None,
|
||||
scale_factor,
|
||||
shader_path,
|
||||
resolutions: ResolutionData {
|
||||
real_width: 1,
|
||||
real_height: 1,
|
||||
scaled_width: 1,
|
||||
scaled_height: 1,
|
||||
},
|
||||
_channel_thread: channel_thread,
|
||||
},
|
||||
sender,
|
||||
)
|
||||
}
|
||||
|
||||
fn message(&mut self, message: RendererMessage<[u8; 4]>, window: Window) {
|
||||
match message {
|
||||
RendererMessage::Prepare { width, height } => self.resize(width, height, window),
|
||||
RendererMessage::Resize { width, height } => self.resize(width, height, window),
|
||||
RendererMessage::Display { buffer } => self.display(buffer),
|
||||
RendererMessage::SetTitle { title } => window.set_title(&title),
|
||||
RendererMessage::Rumble { rumble: _ } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(&mut self, width: usize, height: usize, window: Window) {
|
||||
window.set_content_size(
|
||||
(width * self.scale_factor) as f64,
|
||||
(height * self.scale_factor) as f64,
|
||||
);
|
||||
let real_factor = (window.backing_scale_factor() * self.scale_factor as f64) as u32;
|
||||
let (width, height) = (width as u32, height as u32);
|
||||
self.resolutions = ResolutionData {
|
||||
real_width: width * real_factor,
|
||||
real_height: height * real_factor,
|
||||
scaled_width: width,
|
||||
scaled_height: height,
|
||||
};
|
||||
if let Some(backend) = self.backend.as_mut() {
|
||||
backend
|
||||
.resize(self.resolutions, &WindowHandleWrapper(&window))
|
||||
.some_or_print();
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&mut self, buffer: Vec<[u8; 4]>) {
|
||||
if let Some(backend) = self.backend.as_mut() {
|
||||
backend.new_frame(&buffer).some_or_print();
|
||||
backend
|
||||
.render(self.resolutions, &self.backend_manager)
|
||||
.some_or_print();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowDelegate for CacaoWindow {
|
||||
const NAME: &'static str = "EmulatorWindow";
|
||||
|
||||
fn did_load(&mut self, window: Window) {
|
||||
self.backend = RendererBackend::new(
|
||||
self.resolutions,
|
||||
&WindowHandleWrapper(&window),
|
||||
self.shader_path.clone(),
|
||||
self.backend_manager.clone(),
|
||||
)
|
||||
.some_or_print();
|
||||
}
|
||||
|
||||
fn will_close(&self) {
|
||||
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Closing {
|
||||
window_type: self.window_type,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
struct WindowHandleWrapper<'a, T>(&'a Window<T>);
|
||||
|
||||
impl<'a, T> HasDisplayHandle for WindowHandleWrapper<'a, T> {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||
Ok(unsafe {
|
||||
DisplayHandle::borrow_raw(RawDisplayHandle::AppKit(AppKitDisplayHandle::new()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> HasWindowHandle for WindowHandleWrapper<'a, T> {
|
||||
fn window_handle(&self) -> Result<WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||
let Self(w) = self;
|
||||
|
||||
Ok(unsafe {
|
||||
WindowHandle::borrow_raw(RawWindowHandle::AppKit(AppKitWindowHandle::new(
|
||||
w.content_view_ptr()
|
||||
.ok_or(raw_window_handle::HandleError::Unavailable)?,
|
||||
)))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,253 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::channel;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use cacao::appkit::menu::{Menu, MenuItem};
|
||||
use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle};
|
||||
use cacao::appkit::{App, AppDelegate};
|
||||
use cacao::filesystem::FileSelectPanel;
|
||||
use cacao::notification_center::Dispatcher;
|
||||
use frontend_common::audio;
|
||||
use gb_emu_lib::config::CONFIG_MANAGER;
|
||||
use gb_emu_lib::connect::{EmulatorCoreTrait, EmulatorMessage};
|
||||
use raw_window_handle::{AppKitDisplayHandle, DisplayHandle, RawDisplayHandle};
|
||||
|
||||
use crate::config::GuiConfig;
|
||||
|
||||
use self::cacao_window_manager::{CacaoWindowManager, EmuWindowMessage};
|
||||
use self::preferences::{PreferencesMessage, PreferencesUi};
|
||||
|
||||
mod cacao_window_manager;
|
||||
mod preferences;
|
||||
|
||||
pub(crate) enum AppMessage {
|
||||
Core(CoreMessage),
|
||||
Preferences(PreferencesMessage),
|
||||
EmuWindow(EmuWindowMessage),
|
||||
}
|
||||
|
||||
pub(crate) enum CoreMessage {
|
||||
ShowOpenDialog,
|
||||
ToggleTileWindow,
|
||||
ToggleLayerWindow,
|
||||
SetTileWindow(bool),
|
||||
SetLayerWindow(bool),
|
||||
OpenPreferences,
|
||||
OpenRom(PathBuf),
|
||||
}
|
||||
|
||||
pub(crate) struct TwincUiApp {
|
||||
preferences: RwLock<Window<PreferencesUi>>,
|
||||
current_game: RwLock<CacaoWindowManager>,
|
||||
gui_config: RwLock<GuiConfig>,
|
||||
}
|
||||
|
||||
impl TwincUiApp {
|
||||
fn open_dialog(&self) {
|
||||
if !self.current_game.write().unwrap().can_open_new_rom() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut file_select_panel = FileSelectPanel::new();
|
||||
file_select_panel.set_can_choose_directories(false);
|
||||
file_select_panel.set_can_choose_files(true);
|
||||
file_select_panel.set_allows_multiple_selection(false);
|
||||
file_select_panel.show(move |v| {
|
||||
if let Some(path) = v.first() {
|
||||
dispatch(AppMessage::Core(CoreMessage::OpenRom(path.pathbuf())));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TwincUiApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
preferences: RwLock::new(Window::with(
|
||||
{
|
||||
let mut config = WindowConfig::default();
|
||||
config.set_initial_dimensions(0., 0., 800., 800.);
|
||||
|
||||
config.set_styles(&[
|
||||
WindowStyle::Resizable,
|
||||
WindowStyle::Miniaturizable,
|
||||
WindowStyle::Closable,
|
||||
WindowStyle::Titled,
|
||||
]);
|
||||
|
||||
config.toolbar_style = WindowToolbarStyle::Preferences;
|
||||
config
|
||||
},
|
||||
PreferencesUi::new(),
|
||||
)),
|
||||
current_game: RwLock::new(CacaoWindowManager::new(unsafe {
|
||||
DisplayHandle::borrow_raw(RawDisplayHandle::AppKit(AppKitDisplayHandle::new()))
|
||||
})),
|
||||
gui_config: RwLock::new(CONFIG_MANAGER.load_or_create_config()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AppDelegate for TwincUiApp {
|
||||
fn did_finish_launching(&self) {
|
||||
App::set_menu(menu(&self.gui_config.read().unwrap()));
|
||||
App::activate();
|
||||
}
|
||||
|
||||
fn open_urls(&self, urls: Vec<cacao::url::Url>) {
|
||||
if let Some(url) = urls.first() {
|
||||
if url.scheme() == "file" {
|
||||
if let Ok(path) = url.to_file_path() {
|
||||
if self.current_game.write().unwrap().can_open_new_rom() {
|
||||
dispatch(AppMessage::Core(CoreMessage::OpenRom(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatcher for TwincUiApp {
|
||||
type Message = AppMessage;
|
||||
|
||||
fn on_ui_message(&self, message: Self::Message) {
|
||||
match message {
|
||||
AppMessage::Core(CoreMessage::ShowOpenDialog) => self.open_dialog(),
|
||||
AppMessage::Core(CoreMessage::OpenPreferences) => {
|
||||
self.preferences.read().unwrap().show();
|
||||
}
|
||||
AppMessage::Core(CoreMessage::ToggleLayerWindow) => {
|
||||
if let Ok(mut config) = self.gui_config.write() {
|
||||
config.layer_window = !config.layer_window;
|
||||
App::set_menu(menu(&config));
|
||||
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
|
||||
if let Ok(mut current_game) = self.current_game.write() {
|
||||
current_game.set_layer_window(config.layer_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMessage::Core(CoreMessage::ToggleTileWindow) => {
|
||||
if let Ok(mut config) = self.gui_config.write() {
|
||||
config.tile_window = !config.tile_window;
|
||||
App::set_menu(menu(&config));
|
||||
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
|
||||
if let Ok(mut current_game) = self.current_game.write() {
|
||||
current_game.set_tile_window(config.tile_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMessage::Core(CoreMessage::SetLayerWindow(val)) => {
|
||||
if let Ok(mut config) = self.gui_config.write() {
|
||||
config.layer_window = val;
|
||||
App::set_menu(menu(&config));
|
||||
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
|
||||
if let Ok(mut current_game) = self.current_game.write() {
|
||||
current_game.set_layer_window(config.layer_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMessage::Core(CoreMessage::SetTileWindow(val)) => {
|
||||
if let Ok(mut config) = self.gui_config.write() {
|
||||
config.tile_window = val;
|
||||
App::set_menu(menu(&config));
|
||||
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
|
||||
if let Ok(mut current_game) = self.current_game.write() {
|
||||
current_game.set_tile_window(config.tile_window);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMessage::Core(CoreMessage::OpenRom(path)) => {
|
||||
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
|
||||
sender.send(EmulatorMessage::Start).unwrap();
|
||||
|
||||
let mut options = frontend_common::RunOptions::new(path);
|
||||
if let Ok(config) = self.gui_config.read() {
|
||||
options.layer_window = config.layer_window;
|
||||
options.tile_window = config.tile_window
|
||||
}
|
||||
|
||||
let prepared = frontend_common::prepare(options, receiver);
|
||||
let (output, stream) = audio::create_output(false);
|
||||
let mut window_manager = self.current_game.write().unwrap();
|
||||
window_manager.update_handles(sender, stream);
|
||||
let mut core = match frontend_common::run(prepared, &mut *window_manager, output) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::error!("couldn't create emulator core: {e:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let handle = std::thread::Builder::new()
|
||||
.name(String::from("EmuCore"))
|
||||
.spawn(move || loop {
|
||||
if core.run(100) {
|
||||
break;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
window_manager.set_thread_handle(handle);
|
||||
}
|
||||
AppMessage::Preferences(prefs_message) => {
|
||||
if let Ok(mut prefs) = self.preferences.write() {
|
||||
if let Some(ref mut delegate) = prefs.delegate {
|
||||
delegate.message(prefs_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
AppMessage::EmuWindow(window_message) => {
|
||||
if let Ok(mut window_manager) = self.current_game.write() {
|
||||
window_manager.message(window_message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn menu(config: &GuiConfig) -> Vec<Menu> {
|
||||
vec![
|
||||
Menu::new(
|
||||
"",
|
||||
vec![
|
||||
MenuItem::About("Cacao Test".to_string()),
|
||||
MenuItem::Separator,
|
||||
MenuItem::new("Preferences")
|
||||
.key(",")
|
||||
.action(|| dispatch(AppMessage::Core(CoreMessage::OpenPreferences))),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Services,
|
||||
MenuItem::Separator,
|
||||
MenuItem::Hide,
|
||||
MenuItem::HideOthers,
|
||||
MenuItem::ShowAll,
|
||||
MenuItem::Separator,
|
||||
MenuItem::Quit,
|
||||
],
|
||||
),
|
||||
Menu::new(
|
||||
"File",
|
||||
vec![MenuItem::new("Open")
|
||||
.key("o")
|
||||
.action(|| dispatch(AppMessage::Core(CoreMessage::ShowOpenDialog)))],
|
||||
),
|
||||
Menu::new(
|
||||
"Window",
|
||||
vec![
|
||||
MenuItem::new("Tiles")
|
||||
.checkmark(config.tile_window)
|
||||
.action(|| dispatch(AppMessage::Core(CoreMessage::ToggleTileWindow))),
|
||||
MenuItem::new("Layers")
|
||||
.checkmark(config.layer_window)
|
||||
.action(|| dispatch(AppMessage::Core(CoreMessage::ToggleLayerWindow))),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Minimize,
|
||||
MenuItem::Separator,
|
||||
MenuItem::new("Bring All to Front"),
|
||||
],
|
||||
),
|
||||
Menu::new("Help", vec![]),
|
||||
]
|
||||
}
|
||||
|
||||
fn dispatch(message: AppMessage) {
|
||||
App::<TwincUiApp, AppMessage>::dispatch_main(message);
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
use cacao::{
|
||||
appkit::{
|
||||
toolbar::Toolbar,
|
||||
window::{Window, WindowDelegate},
|
||||
},
|
||||
view::ViewController,
|
||||
};
|
||||
|
||||
use self::{
|
||||
toolbar::PreferencesToolbar,
|
||||
views::{
|
||||
CorePreferencesContentView, CorePreferencesUpdates, StandalonePreferencesContentView,
|
||||
StandalonePreferencesUpdates, VstPreferencesContentView, VstPreferencesUpdates,
|
||||
},
|
||||
};
|
||||
|
||||
mod toolbar;
|
||||
mod views;
|
||||
|
||||
pub(crate) enum PreferencesMessage {
|
||||
SwitchPane(PreferencesPane),
|
||||
UpdateCore(CorePreferencesUpdates),
|
||||
UpdateStandalone(StandalonePreferencesUpdates),
|
||||
UpdateVst(VstPreferencesUpdates),
|
||||
}
|
||||
|
||||
pub(crate) enum PreferencesPane {
|
||||
Core,
|
||||
Standalone,
|
||||
Vst,
|
||||
}
|
||||
|
||||
pub(crate) struct PreferencesUi {
|
||||
pub(crate) toolbar: Toolbar<PreferencesToolbar>,
|
||||
pub(crate) core_prefs: ViewController<CorePreferencesContentView>,
|
||||
pub(crate) standalone_prefs: ViewController<StandalonePreferencesContentView>,
|
||||
pub(crate) vst_prefs: ViewController<VstPreferencesContentView>,
|
||||
window: Option<Window>,
|
||||
}
|
||||
|
||||
impl PreferencesUi {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()),
|
||||
core_prefs: ViewController::new(CorePreferencesContentView::new()),
|
||||
standalone_prefs: ViewController::new(StandalonePreferencesContentView::new()),
|
||||
vst_prefs: ViewController::new(VstPreferencesContentView::new()),
|
||||
window: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn message(&mut self, message: PreferencesMessage) {
|
||||
let window = self.window.as_ref().unwrap();
|
||||
|
||||
match message {
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Core) => {
|
||||
window.set_content_view_controller(&self.core_prefs);
|
||||
}
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => {
|
||||
window.set_content_view_controller(&self.standalone_prefs);
|
||||
}
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Vst) => {
|
||||
window.set_content_view_controller(&self.vst_prefs);
|
||||
}
|
||||
PreferencesMessage::UpdateCore(update) => {
|
||||
if let Some(ref mut delegate) = self.core_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
PreferencesMessage::UpdateStandalone(update) => {
|
||||
if let Some(ref mut delegate) = self.standalone_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
PreferencesMessage::UpdateVst(update) => {
|
||||
if let Some(ref mut delegate) = self.vst_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WindowDelegate for PreferencesUi {
|
||||
const NAME: &'static str = "PreferencesUi";
|
||||
|
||||
fn did_load(&mut self, window: Window) {
|
||||
window.set_autosave_name("PreferencesWindow");
|
||||
window.set_movable_by_background(true);
|
||||
window.set_toolbar(&self.toolbar);
|
||||
window.set_title("Preferences");
|
||||
|
||||
self.window = Some(window);
|
||||
|
||||
self.message(PreferencesMessage::SwitchPane(PreferencesPane::Core));
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
use cacao::{
|
||||
appkit::toolbar::{ItemIdentifier, ToolbarDelegate, ToolbarItem},
|
||||
image::{Image, MacSystemIcon},
|
||||
};
|
||||
|
||||
use crate::macos::{dispatch, AppMessage};
|
||||
|
||||
use super::{PreferencesMessage, PreferencesPane};
|
||||
|
||||
pub(crate) struct PreferencesToolbar {
|
||||
core: ToolbarItem,
|
||||
standalone: ToolbarItem,
|
||||
vst: ToolbarItem,
|
||||
}
|
||||
|
||||
impl Default for PreferencesToolbar {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
core: {
|
||||
let mut item = ToolbarItem::new("core");
|
||||
item.set_title("Core");
|
||||
|
||||
let icon = Image::toolbar_icon(MacSystemIcon::PreferencesGeneral, "Core");
|
||||
item.set_image(icon);
|
||||
|
||||
item.set_action(|_| {
|
||||
dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane(
|
||||
PreferencesPane::Core,
|
||||
)));
|
||||
});
|
||||
|
||||
item
|
||||
},
|
||||
standalone: {
|
||||
let mut item = ToolbarItem::new("standalone");
|
||||
item.set_title("Standalone");
|
||||
|
||||
let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "Standalone");
|
||||
item.set_image(icon);
|
||||
|
||||
item.set_action(|_| {
|
||||
dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane(
|
||||
PreferencesPane::Standalone,
|
||||
)));
|
||||
});
|
||||
|
||||
item
|
||||
},
|
||||
vst: {
|
||||
let mut item = ToolbarItem::new("vst");
|
||||
item.set_title("VST");
|
||||
|
||||
let icon = Image::toolbar_icon(MacSystemIcon::PreferencesAdvanced, "VST");
|
||||
item.set_image(icon);
|
||||
|
||||
item.set_action(|_| {
|
||||
dispatch(AppMessage::Preferences(PreferencesMessage::SwitchPane(
|
||||
PreferencesPane::Vst,
|
||||
)));
|
||||
});
|
||||
|
||||
item
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolbarDelegate for PreferencesToolbar {
|
||||
const NAME: &'static str = "PreferencesToolbar";
|
||||
|
||||
fn did_load(&mut self, toolbar: cacao::appkit::toolbar::Toolbar) {
|
||||
toolbar.set_selected("core")
|
||||
}
|
||||
|
||||
fn allowed_item_identifiers(&self) -> Vec<cacao::appkit::toolbar::ItemIdentifier> {
|
||||
vec![
|
||||
ItemIdentifier::Custom("core"),
|
||||
ItemIdentifier::Custom("standalone"),
|
||||
ItemIdentifier::Custom("vst"),
|
||||
]
|
||||
}
|
||||
|
||||
fn default_item_identifiers(&self) -> Vec<cacao::appkit::toolbar::ItemIdentifier> {
|
||||
vec![
|
||||
ItemIdentifier::Custom("core"),
|
||||
ItemIdentifier::Custom("standalone"),
|
||||
ItemIdentifier::Custom("vst"),
|
||||
]
|
||||
}
|
||||
|
||||
fn selectable_item_identifiers(&self) -> Vec<ItemIdentifier> {
|
||||
vec![
|
||||
ItemIdentifier::Custom("core"),
|
||||
ItemIdentifier::Custom("standalone"),
|
||||
ItemIdentifier::Custom("vst"),
|
||||
]
|
||||
}
|
||||
|
||||
fn item_for(&self, identifier: &str) -> &cacao::appkit::toolbar::ToolbarItem {
|
||||
match identifier {
|
||||
"core" => &self.core,
|
||||
"standalone" => &self.standalone,
|
||||
"vst" => &self.vst,
|
||||
_ => {
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,533 +0,0 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use cacao::{
|
||||
layout::Layout,
|
||||
view::{View, ViewDelegate},
|
||||
};
|
||||
use frontend_common::StandaloneConfig;
|
||||
use gb_emu_lib::config::{Config, ResolutionOverride, CONFIG_MANAGER};
|
||||
|
||||
use crate::macos::dispatch;
|
||||
|
||||
use self::widgets::{PathView, StepperView, StepperViewToggle, ToggleView};
|
||||
|
||||
mod widgets;
|
||||
|
||||
fn make_relative_path(path: PathBuf, base_dir: PathBuf) -> String {
|
||||
let path = path.canonicalize().unwrap_or(path);
|
||||
let base_dir = base_dir.canonicalize().unwrap_or(base_dir);
|
||||
if path.starts_with(&base_dir) {
|
||||
path.strip_prefix(base_dir).unwrap_or(&path)
|
||||
} else {
|
||||
&path
|
||||
}
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) struct CorePreferencesContentView {
|
||||
config: Config,
|
||||
dmg_bootrom: PathView,
|
||||
cgb_bootrom: PathView,
|
||||
show_bootrom: ToggleView,
|
||||
prefer_cgb: ToggleView,
|
||||
dmg_shader: PathView,
|
||||
dmg_resizable: ToggleView,
|
||||
dmg_resolution: StepperViewToggle,
|
||||
cgb_shader: PathView,
|
||||
cgb_resizable: ToggleView,
|
||||
cgb_resolution: StepperViewToggle,
|
||||
}
|
||||
|
||||
impl CorePreferencesContentView {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
config: CONFIG_MANAGER.load_or_create_base_config(),
|
||||
dmg_bootrom: Default::default(),
|
||||
cgb_bootrom: Default::default(),
|
||||
show_bootrom: Default::default(),
|
||||
prefer_cgb: Default::default(),
|
||||
dmg_shader: Default::default(),
|
||||
dmg_resizable: Default::default(),
|
||||
dmg_resolution: Default::default(),
|
||||
cgb_shader: Default::default(),
|
||||
cgb_resizable: Default::default(),
|
||||
cgb_resolution: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: CorePreferencesUpdates) {
|
||||
match update {
|
||||
CorePreferencesUpdates::ShowBootrom => {
|
||||
self.config.show_bootrom = !self.config.show_bootrom
|
||||
}
|
||||
CorePreferencesUpdates::PreferCGB => self.config.prefer_cgb = !self.config.prefer_cgb,
|
||||
CorePreferencesUpdates::DmgResolution => {
|
||||
if let Some(val) = self.dmg_resolution.update() {
|
||||
self.config.vulkan_config.dmg_resolution_override =
|
||||
ResolutionOverride::Scale(val.round() as usize)
|
||||
}
|
||||
}
|
||||
CorePreferencesUpdates::CgbResolution => {
|
||||
if let Some(val) = self.cgb_resolution.update() {
|
||||
self.config.vulkan_config.cgb_resolution_override =
|
||||
ResolutionOverride::Scale(val.round() as usize)
|
||||
}
|
||||
}
|
||||
CorePreferencesUpdates::DmgResizable => {
|
||||
self.config.vulkan_config.dmg_shader_resizable =
|
||||
!self.config.vulkan_config.dmg_shader_resizable
|
||||
}
|
||||
CorePreferencesUpdates::CgbResizable => {
|
||||
self.config.vulkan_config.cgb_shader_resizable =
|
||||
!self.config.vulkan_config.cgb_shader_resizable
|
||||
}
|
||||
CorePreferencesUpdates::DmgResolutionEnabled => {
|
||||
self.dmg_resolution.flip();
|
||||
self.config.vulkan_config.dmg_resolution_override = self
|
||||
.dmg_resolution
|
||||
.update()
|
||||
.map(|v| v.round() as usize)
|
||||
.into();
|
||||
}
|
||||
CorePreferencesUpdates::CgbResolutionEnabled => {
|
||||
self.cgb_resolution.flip();
|
||||
self.config.vulkan_config.cgb_resolution_override = self
|
||||
.cgb_resolution
|
||||
.update()
|
||||
.map(|v| v.round() as usize)
|
||||
.into();
|
||||
}
|
||||
CorePreferencesUpdates::DmgBootrom(path) => {
|
||||
self.config.dmg_bootrom = path.map(|v| make_relative_path(v, CONFIG_MANAGER.dir()));
|
||||
self.dmg_bootrom.update(self.config.dmg_bootrom.clone());
|
||||
}
|
||||
CorePreferencesUpdates::CgbBootrom(path) => {
|
||||
self.config.cgb_bootrom = path.map(|v| make_relative_path(v, CONFIG_MANAGER.dir()));
|
||||
self.cgb_bootrom.update(self.config.cgb_bootrom.clone());
|
||||
}
|
||||
CorePreferencesUpdates::DmgShader(path) => {
|
||||
self.config.vulkan_config.dmg_shader_path =
|
||||
path.map(|v| make_relative_path(v, CONFIG_MANAGER.dir()));
|
||||
self.dmg_shader
|
||||
.update(self.config.vulkan_config.dmg_shader_path.clone());
|
||||
}
|
||||
CorePreferencesUpdates::CgbShader(path) => {
|
||||
self.config.vulkan_config.cgb_shader_path =
|
||||
path.map(|v| make_relative_path(v, CONFIG_MANAGER.dir()));
|
||||
self.cgb_shader
|
||||
.update(self.config.vulkan_config.cgb_shader_path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
CONFIG_MANAGER
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum CorePreferencesUpdates {
|
||||
ShowBootrom,
|
||||
PreferCGB,
|
||||
DmgResolution,
|
||||
DmgResolutionEnabled,
|
||||
CgbResolution,
|
||||
CgbResolutionEnabled,
|
||||
DmgResizable,
|
||||
CgbResizable,
|
||||
DmgBootrom(Option<PathBuf>),
|
||||
CgbBootrom(Option<PathBuf>),
|
||||
DmgShader(Option<PathBuf>),
|
||||
CgbShader(Option<PathBuf>),
|
||||
}
|
||||
|
||||
impl ViewDelegate for CorePreferencesContentView {
|
||||
const NAME: &'static str = "CorePreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.dmg_bootrom
|
||||
.configure("DMG bootrom", "", self.config.dmg_bootrom.clone(), |v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgBootrom(v)),
|
||||
))
|
||||
});
|
||||
view.add_subview(&self.dmg_bootrom.view);
|
||||
|
||||
self.cgb_bootrom
|
||||
.configure("CGB bootrom", "", self.config.cgb_bootrom.clone(), |v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbBootrom(v)),
|
||||
))
|
||||
});
|
||||
view.add_subview(&self.cgb_bootrom.view);
|
||||
|
||||
self.show_bootrom
|
||||
.configure("Show BootROM", self.config.show_bootrom, |_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::ShowBootrom),
|
||||
))
|
||||
});
|
||||
view.add_subview(&self.show_bootrom.view);
|
||||
|
||||
self.prefer_cgb.configure(
|
||||
"Prefer Game Boy Colour mode",
|
||||
self.config.prefer_cgb,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::PreferCGB),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.prefer_cgb.view);
|
||||
|
||||
let dmg_resolution_override = match self.config.vulkan_config.dmg_resolution_override {
|
||||
ResolutionOverride::Scale(v) => v,
|
||||
ResolutionOverride::Default => 4,
|
||||
};
|
||||
|
||||
self.dmg_resolution.set_suffix(String::from("x"));
|
||||
self.dmg_resolution.configure(
|
||||
"DMG scale override",
|
||||
1.,
|
||||
10.,
|
||||
1.0,
|
||||
0,
|
||||
Some(dmg_resolution_override as f64),
|
||||
self.config.vulkan_config.dmg_resolution_override != ResolutionOverride::Default,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgResolution),
|
||||
));
|
||||
},
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(
|
||||
CorePreferencesUpdates::DmgResolutionEnabled,
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.dmg_resolution.view);
|
||||
|
||||
self.dmg_shader.configure(
|
||||
"DMG shader",
|
||||
"",
|
||||
self.config.vulkan_config.dmg_shader_path.clone(),
|
||||
|v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgShader(v)),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.dmg_shader.view);
|
||||
|
||||
self.dmg_resizable.configure(
|
||||
"Resizable",
|
||||
self.config.vulkan_config.dmg_shader_resizable,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::DmgResizable),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.dmg_resizable.view);
|
||||
|
||||
let cgb_resolution_override = match self.config.vulkan_config.cgb_resolution_override {
|
||||
ResolutionOverride::Scale(v) => v,
|
||||
ResolutionOverride::Default => 4,
|
||||
};
|
||||
|
||||
self.cgb_resolution.set_suffix(String::from("x"));
|
||||
self.cgb_resolution.configure(
|
||||
"CGB scale override",
|
||||
1.,
|
||||
10.,
|
||||
1.0,
|
||||
0,
|
||||
Some(cgb_resolution_override as f64),
|
||||
self.config.vulkan_config.cgb_resolution_override != ResolutionOverride::Default,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbResolution),
|
||||
));
|
||||
},
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(
|
||||
CorePreferencesUpdates::CgbResolutionEnabled,
|
||||
),
|
||||
));
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.cgb_resolution.view);
|
||||
|
||||
self.cgb_shader.configure(
|
||||
"CGB shader",
|
||||
"",
|
||||
self.config.vulkan_config.cgb_shader_path.clone(),
|
||||
|v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbShader(v)),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.cgb_shader.view);
|
||||
|
||||
self.cgb_resizable.configure(
|
||||
"Resizable",
|
||||
self.config.vulkan_config.cgb_shader_resizable,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::CgbResizable),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.cgb_resizable.view);
|
||||
|
||||
widgets::auto_layout(
|
||||
&view,
|
||||
vec![
|
||||
&self.dmg_bootrom.view,
|
||||
&self.cgb_bootrom.view,
|
||||
&self.show_bootrom.view,
|
||||
&self.prefer_cgb.view,
|
||||
&self.dmg_resolution.view,
|
||||
&self.dmg_shader.view,
|
||||
&self.dmg_resizable.view,
|
||||
&self.cgb_resolution.view,
|
||||
&self.cgb_shader.view,
|
||||
&self.cgb_resizable.view,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct StandalonePreferencesContentView {
|
||||
config: StandaloneConfig,
|
||||
scale_factor: StepperView,
|
||||
group_screenshots_by_rom: ToggleView,
|
||||
buffers_per_frame: StepperView,
|
||||
output_buffer_size: StepperView,
|
||||
}
|
||||
|
||||
impl StandalonePreferencesContentView {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
config: CONFIG_MANAGER.load_or_create_config(),
|
||||
|
||||
group_screenshots_by_rom: Default::default(),
|
||||
scale_factor: Default::default(),
|
||||
buffers_per_frame: Default::default(),
|
||||
output_buffer_size: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: StandalonePreferencesUpdates) {
|
||||
match update {
|
||||
StandalonePreferencesUpdates::GroupScreenshotsByRom => {
|
||||
self.config.group_screenshots_by_rom = !self.config.group_screenshots_by_rom
|
||||
}
|
||||
StandalonePreferencesUpdates::ScaleFactor => {
|
||||
self.config.scale_factor = self.scale_factor.update().round() as usize
|
||||
}
|
||||
StandalonePreferencesUpdates::BuffersPerFrame => {
|
||||
self.config.buffers_per_frame = self.buffers_per_frame.update().round() as usize
|
||||
}
|
||||
StandalonePreferencesUpdates::OutputBufferSize => {
|
||||
self.config.output_buffer_size = self.output_buffer_size.update_map(|v| {
|
||||
let i = v.round() as u32;
|
||||
2_u32.pow(i)
|
||||
});
|
||||
}
|
||||
}
|
||||
CONFIG_MANAGER
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum StandalonePreferencesUpdates {
|
||||
GroupScreenshotsByRom,
|
||||
ScaleFactor,
|
||||
BuffersPerFrame,
|
||||
OutputBufferSize,
|
||||
}
|
||||
|
||||
impl ViewDelegate for StandalonePreferencesContentView {
|
||||
const NAME: &'static str = "StandalonePreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.scale_factor.configure(
|
||||
"Scale factor",
|
||||
1.,
|
||||
16.,
|
||||
1.,
|
||||
0,
|
||||
Some(self.config.scale_factor as f64),
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateStandalone(
|
||||
StandalonePreferencesUpdates::ScaleFactor,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.scale_factor.view);
|
||||
|
||||
self.group_screenshots_by_rom.configure(
|
||||
"Group screenshots by ROM",
|
||||
self.config.group_screenshots_by_rom,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateStandalone(
|
||||
StandalonePreferencesUpdates::GroupScreenshotsByRom,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.group_screenshots_by_rom.view);
|
||||
|
||||
self.buffers_per_frame.configure(
|
||||
"Buffers per frame",
|
||||
1.,
|
||||
10.,
|
||||
1.,
|
||||
0,
|
||||
Some(self.config.buffers_per_frame as f64),
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateStandalone(
|
||||
StandalonePreferencesUpdates::BuffersPerFrame,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.buffers_per_frame.view);
|
||||
|
||||
self.output_buffer_size.configure(
|
||||
"Output buffer size",
|
||||
1.,
|
||||
100.,
|
||||
1.,
|
||||
0,
|
||||
Some((self.config.output_buffer_size as f64).log2()),
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateStandalone(
|
||||
StandalonePreferencesUpdates::OutputBufferSize,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
self.output_buffer_size.update_map(|v| {
|
||||
let i = v.round() as u32;
|
||||
2_u32.pow(i)
|
||||
});
|
||||
view.add_subview(&self.output_buffer_size.view);
|
||||
|
||||
widgets::auto_layout(
|
||||
&view,
|
||||
vec![
|
||||
&self.scale_factor.view,
|
||||
&self.group_screenshots_by_rom.view,
|
||||
&self.buffers_per_frame.view,
|
||||
&self.output_buffer_size.view,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct VstPreferencesContentView {
|
||||
config: twinc_emu_vst::VstConfig,
|
||||
scale_factor: StepperView,
|
||||
rom: PathView,
|
||||
force_skip_bootrom: ToggleView,
|
||||
}
|
||||
|
||||
impl VstPreferencesContentView {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
config: CONFIG_MANAGER.load_or_create_config(),
|
||||
|
||||
force_skip_bootrom: Default::default(),
|
||||
scale_factor: Default::default(),
|
||||
rom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: VstPreferencesUpdates) {
|
||||
match update {
|
||||
VstPreferencesUpdates::ForceSkipBootrom => {
|
||||
self.config.force_skip_bootrom = !self.config.force_skip_bootrom
|
||||
}
|
||||
VstPreferencesUpdates::ScaleFactor => {
|
||||
self.config.scale_factor = self.scale_factor.update().round() as usize
|
||||
}
|
||||
VstPreferencesUpdates::Rom(path) => {
|
||||
if let Some(path) = path {
|
||||
self.config.rom = make_relative_path(path, CONFIG_MANAGER.dir());
|
||||
}
|
||||
self.rom.update(Some(self.config.rom.clone()));
|
||||
}
|
||||
}
|
||||
CONFIG_MANAGER
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum VstPreferencesUpdates {
|
||||
ForceSkipBootrom,
|
||||
ScaleFactor,
|
||||
Rom(Option<PathBuf>),
|
||||
}
|
||||
|
||||
impl ViewDelegate for VstPreferencesContentView {
|
||||
const NAME: &'static str = "VstPreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.scale_factor.configure(
|
||||
"Scale factor",
|
||||
1.,
|
||||
16.,
|
||||
1.,
|
||||
0,
|
||||
Some(self.config.scale_factor as f64),
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::ScaleFactor),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.scale_factor.view);
|
||||
|
||||
self.rom
|
||||
.configure("ROM", "", Some(self.config.rom.clone()), |path| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::Rom(path)),
|
||||
))
|
||||
});
|
||||
view.add_subview(&self.rom.view);
|
||||
|
||||
self.force_skip_bootrom.configure(
|
||||
"Force skip bootrom",
|
||||
self.config.force_skip_bootrom,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::ForceSkipBootrom),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.force_skip_bootrom.view);
|
||||
|
||||
widgets::auto_layout(
|
||||
&view,
|
||||
vec![
|
||||
&self.scale_factor.view,
|
||||
&self.rom.view,
|
||||
&self.force_skip_bootrom.view,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,528 +0,0 @@
|
|||
use std::fmt::Display;
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cacao::button::Button;
|
||||
use cacao::filesystem::FileSelectPanel;
|
||||
use cacao::image::{Image, ImageView};
|
||||
use cacao::input::TextField;
|
||||
use cacao::layout::{Layout, LayoutConstraint};
|
||||
use cacao::select::Select;
|
||||
use cacao::stepper::Stepper;
|
||||
use cacao::switch::Switch;
|
||||
use cacao::text::Label;
|
||||
use cacao::view::View;
|
||||
use objc::runtime::Object;
|
||||
|
||||
const LEFT_MARGIN: f64 = 150.;
|
||||
|
||||
pub(crate) fn auto_layout(container: &View, items: Vec<&View>) {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
LayoutConstraint::activate(&[
|
||||
if i > 0 {
|
||||
item.top
|
||||
.constraint_equal_to(&items[i - 1].bottom)
|
||||
.offset(11.)
|
||||
} else {
|
||||
item.top.constraint_equal_to(&container.top).offset(22.)
|
||||
},
|
||||
item.leading
|
||||
.constraint_equal_to(&container.leading)
|
||||
.offset(22.),
|
||||
item.trailing
|
||||
.constraint_equal_to(&container.trailing)
|
||||
.offset(-22.),
|
||||
]);
|
||||
}
|
||||
LayoutConstraint::activate(&[items
|
||||
.last()
|
||||
.unwrap()
|
||||
.bottom
|
||||
.constraint_equal_to(&container.bottom)
|
||||
.offset(-22.)]);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleView {
|
||||
pub view: View,
|
||||
pub switch: Switch,
|
||||
pub title: Label,
|
||||
}
|
||||
|
||||
impl Default for ToggleView {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
let switch = Switch::new("");
|
||||
view.add_subview(&switch);
|
||||
|
||||
let title = Label::new();
|
||||
view.add_subview(&title);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
switch.center_y.constraint_equal_to(&view.center_y),
|
||||
switch
|
||||
.leading
|
||||
.constraint_equal_to(&view.leading)
|
||||
.offset(LEFT_MARGIN),
|
||||
switch.width.constraint_equal_to_constant(24.),
|
||||
title
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(200.),
|
||||
title.top.constraint_equal_to(&view.top),
|
||||
title.leading.constraint_equal_to(&switch.trailing),
|
||||
title.trailing.constraint_equal_to(&view.trailing),
|
||||
title.bottom.constraint_equal_to(&view.bottom),
|
||||
]);
|
||||
|
||||
ToggleView {
|
||||
view,
|
||||
switch,
|
||||
title,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToggleView {
|
||||
pub fn configure<F>(&mut self, text: &str, state: bool, handler: F)
|
||||
where
|
||||
F: Fn(*const Object) + Send + Sync + 'static,
|
||||
{
|
||||
self.title.set_text(text);
|
||||
self.switch.set_action(handler);
|
||||
self.switch.set_checked(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathView {
|
||||
pub view: View,
|
||||
pub field: TextField,
|
||||
pub browse_button: Button,
|
||||
pub clear_button: Button,
|
||||
pub title: Label,
|
||||
}
|
||||
|
||||
const PATH_VIEW_BUTTON_WIDTH: f64 = 80.;
|
||||
|
||||
impl Default for PathView {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
let field = TextField::new();
|
||||
field.set_uses_single_line(true);
|
||||
field.set_truncates_last_visible_line(false);
|
||||
field.set_line_break_mode(cacao::text::LineBreakMode::TruncateMiddle);
|
||||
view.add_subview(&field);
|
||||
|
||||
let browse_button = Button::new("Browse");
|
||||
view.add_subview(&browse_button);
|
||||
|
||||
let clear_button = Button::new("Clear");
|
||||
view.add_subview(&clear_button);
|
||||
|
||||
let title = Label::new();
|
||||
view.add_subview(&title);
|
||||
title.set_text_alignment(cacao::text::TextAlign::Right);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
title.center_y.constraint_equal_to(&view.center_y),
|
||||
title.leading.constraint_equal_to(&view.leading),
|
||||
title.width.constraint_equal_to_constant(LEFT_MARGIN - 10.),
|
||||
field.top.constraint_equal_to(&view.top),
|
||||
field
|
||||
.leading
|
||||
.constraint_equal_to(&title.trailing)
|
||||
.offset(10.),
|
||||
field.bottom.constraint_equal_to(&view.bottom),
|
||||
field
|
||||
.trailing
|
||||
.constraint_equal_to(&browse_button.leading)
|
||||
.offset(-10.),
|
||||
field
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(200.),
|
||||
browse_button.center_y.constraint_equal_to(&view.center_y),
|
||||
browse_button
|
||||
.trailing
|
||||
.constraint_equal_to(&clear_button.leading)
|
||||
.offset(-10.),
|
||||
browse_button
|
||||
.width
|
||||
.constraint_equal_to_constant(PATH_VIEW_BUTTON_WIDTH),
|
||||
clear_button.center_y.constraint_equal_to(&view.center_y),
|
||||
clear_button.trailing.constraint_equal_to(&view.trailing),
|
||||
clear_button
|
||||
.width
|
||||
.constraint_equal_to_constant(PATH_VIEW_BUTTON_WIDTH),
|
||||
]);
|
||||
|
||||
Self {
|
||||
view,
|
||||
field,
|
||||
browse_button,
|
||||
clear_button,
|
||||
title,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PathView {
|
||||
pub fn configure<F>(
|
||||
&mut self,
|
||||
title: &str,
|
||||
placeholder: &str,
|
||||
state: Option<String>,
|
||||
handler: F,
|
||||
) where
|
||||
F: Fn(Option<PathBuf>) + Copy + Send + Sync + 'static,
|
||||
{
|
||||
self.browse_button.set_action(move |_v| {
|
||||
let mut file_select_panel = FileSelectPanel::new();
|
||||
file_select_panel.set_can_choose_directories(false);
|
||||
file_select_panel.set_can_choose_files(true);
|
||||
file_select_panel.set_allows_multiple_selection(false);
|
||||
file_select_panel.show(move |v| {
|
||||
if let Some(path) = v.first() {
|
||||
handler(Some(path.pathbuf()));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.clear_button.set_action(move |_v| handler(None));
|
||||
|
||||
self.title.set_text(title);
|
||||
self.field.set_placeholder_text(placeholder);
|
||||
if let Some(state) = state {
|
||||
self.field.set_text(&state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, state: Option<String>) {
|
||||
self.field.set_text(&state.unwrap_or(String::from("")));
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PickerView<T>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
pub _view: View,
|
||||
pub select: Select,
|
||||
pub title: Label,
|
||||
_p: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for PickerView<T>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
let select = Select::new();
|
||||
|
||||
view.add_subview(&select);
|
||||
|
||||
let title = Label::new();
|
||||
view.add_subview(&title);
|
||||
title.set_text_alignment(cacao::text::TextAlign::Right);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
title.center_y.constraint_equal_to(&view.center_y),
|
||||
title.leading.constraint_equal_to(&view.leading),
|
||||
title.width.constraint_equal_to_constant(LEFT_MARGIN - 10.),
|
||||
select.top.constraint_equal_to(&view.top),
|
||||
select
|
||||
.leading
|
||||
.constraint_equal_to(&title.trailing)
|
||||
.offset(10.),
|
||||
select.bottom.constraint_equal_to(&view.bottom),
|
||||
select.trailing.constraint_equal_to(&view.trailing),
|
||||
select
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(200.),
|
||||
]);
|
||||
|
||||
Self {
|
||||
_view: view,
|
||||
select,
|
||||
title,
|
||||
_p: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PickerView<T>
|
||||
where
|
||||
T: ToString,
|
||||
{
|
||||
#[allow(dead_code)]
|
||||
pub fn configure(&mut self, title: &str, values: Vec<T>) {
|
||||
self.title.set_text(title);
|
||||
for val in &values {
|
||||
self.select.add_item(val.to_string().as_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StepperView {
|
||||
pub view: View,
|
||||
pub field: TextField,
|
||||
pub stepper: Stepper,
|
||||
pub title: Label,
|
||||
pub decimal_places: usize,
|
||||
}
|
||||
|
||||
impl Default for StepperView {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
let stepper = Stepper::new();
|
||||
stepper.set_wraps(false);
|
||||
view.add_subview(&stepper);
|
||||
|
||||
let field = TextField::new();
|
||||
field.set_uses_single_line(true);
|
||||
field.set_editable(false);
|
||||
view.add_subview(&field);
|
||||
|
||||
let title = Label::new();
|
||||
view.add_subview(&title);
|
||||
title.set_text_alignment(cacao::text::TextAlign::Right);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
title.center_y.constraint_equal_to(&view.center_y),
|
||||
title.leading.constraint_equal_to(&view.leading),
|
||||
title.width.constraint_equal_to_constant(LEFT_MARGIN - 10.),
|
||||
field.top.constraint_equal_to(&view.top),
|
||||
field
|
||||
.leading
|
||||
.constraint_equal_to(&title.trailing)
|
||||
.offset(10.),
|
||||
stepper.center_y.constraint_equal_to(&view.center_y),
|
||||
stepper
|
||||
.leading
|
||||
.constraint_equal_to(&field.trailing)
|
||||
.offset(5.),
|
||||
field.bottom.constraint_equal_to(&view.bottom),
|
||||
field
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(80.),
|
||||
]);
|
||||
|
||||
Self {
|
||||
view,
|
||||
field,
|
||||
stepper,
|
||||
title,
|
||||
decimal_places: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StepperView {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn configure<F>(
|
||||
&mut self,
|
||||
title: &str,
|
||||
min: f64,
|
||||
max: f64,
|
||||
increment: f64,
|
||||
decimal_places: usize,
|
||||
initial_value: Option<f64>,
|
||||
handler: F,
|
||||
) where
|
||||
F: Fn(*const Object) + Send + Sync + 'static,
|
||||
{
|
||||
self.title.set_text(title);
|
||||
self.decimal_places = decimal_places;
|
||||
if let Some(val) = initial_value {
|
||||
self.stepper.set_value(val);
|
||||
}
|
||||
self.stepper.set_min_value(min);
|
||||
self.stepper.set_max_value(max);
|
||||
self.stepper.set_increment(increment);
|
||||
self.stepper.set_action(handler);
|
||||
self.update();
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> f64 {
|
||||
let val = self.stepper.get_value();
|
||||
self.field
|
||||
.set_text(format!("{:.1$}", val, self.decimal_places).as_str());
|
||||
val
|
||||
}
|
||||
|
||||
pub fn update_map<F, T>(&mut self, map: F) -> T
|
||||
where
|
||||
F: Fn(f64) -> T,
|
||||
T: Display,
|
||||
{
|
||||
let mapped = map(self.stepper.get_value());
|
||||
self.field.set_text(format!("{}", mapped).as_str());
|
||||
mapped
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StepperViewToggle {
|
||||
pub view: View,
|
||||
pub switch: Switch,
|
||||
pub field: TextField,
|
||||
pub stepper: Stepper,
|
||||
pub title: Label,
|
||||
pub decimal_places: usize,
|
||||
pub enabled: bool,
|
||||
pub suffix: String,
|
||||
}
|
||||
|
||||
impl Default for StepperViewToggle {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
let stepper = Stepper::new();
|
||||
stepper.set_wraps(false);
|
||||
view.add_subview(&stepper);
|
||||
|
||||
let field = TextField::new();
|
||||
field.set_uses_single_line(true);
|
||||
field.set_editable(false);
|
||||
view.add_subview(&field);
|
||||
|
||||
let title = Label::new();
|
||||
view.add_subview(&title);
|
||||
title.set_text_alignment(cacao::text::TextAlign::Right);
|
||||
|
||||
let switch = Switch::new("");
|
||||
view.add_subview(&switch);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
title.center_y.constraint_equal_to(&view.center_y),
|
||||
title.leading.constraint_equal_to(&view.leading),
|
||||
title.width.constraint_equal_to_constant(LEFT_MARGIN - 10.),
|
||||
switch.center_y.constraint_equal_to(&view.center_y),
|
||||
switch
|
||||
.leading
|
||||
.constraint_equal_to(&title.trailing)
|
||||
.offset(10.),
|
||||
field.top.constraint_equal_to(&view.top),
|
||||
field
|
||||
.leading
|
||||
.constraint_equal_to(&switch.trailing)
|
||||
.offset(10.),
|
||||
stepper.center_y.constraint_equal_to(&view.center_y),
|
||||
stepper
|
||||
.leading
|
||||
.constraint_equal_to(&field.trailing)
|
||||
.offset(5.),
|
||||
field.bottom.constraint_equal_to(&view.bottom),
|
||||
field
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(80.),
|
||||
]);
|
||||
|
||||
Self {
|
||||
view,
|
||||
switch,
|
||||
field,
|
||||
stepper,
|
||||
title,
|
||||
decimal_places: 0,
|
||||
enabled: false,
|
||||
suffix: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StepperViewToggle {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn configure<F, G>(
|
||||
&mut self,
|
||||
title: &str,
|
||||
min: f64,
|
||||
max: f64,
|
||||
increment: f64,
|
||||
decimal_places: usize,
|
||||
initial_value: Option<f64>,
|
||||
initial_enabled: bool,
|
||||
stepper_handler: F,
|
||||
switch_handler: G,
|
||||
) where
|
||||
F: Fn(*const Object) + Send + Sync + 'static,
|
||||
G: Fn(*const Object) + Send + Sync + 'static,
|
||||
{
|
||||
self.title.set_text(title);
|
||||
self.decimal_places = decimal_places;
|
||||
if let Some(val) = initial_value {
|
||||
self.stepper.set_value(val);
|
||||
}
|
||||
self.stepper.set_min_value(min);
|
||||
self.stepper.set_max_value(max);
|
||||
self.stepper.set_increment(increment);
|
||||
self.stepper.set_action(stepper_handler);
|
||||
self.switch.set_action(switch_handler);
|
||||
self.enabled = initial_enabled;
|
||||
self.update();
|
||||
}
|
||||
|
||||
pub fn set_suffix(&mut self, suffix: String) {
|
||||
self.suffix = suffix;
|
||||
}
|
||||
|
||||
pub fn update(&mut self) -> Option<f64> {
|
||||
self.switch.set_checked(self.enabled);
|
||||
self.field.set_enabled(self.enabled);
|
||||
if self.enabled {
|
||||
let val = self.stepper.get_value();
|
||||
self.field
|
||||
.set_text(format!("{:.1$}{2}", val, self.decimal_places, self.suffix,).as_str());
|
||||
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flip(&mut self) {
|
||||
self.enabled = !self.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ImageViewWrapper {
|
||||
view: View,
|
||||
image_view: ImageView,
|
||||
_image: Image,
|
||||
}
|
||||
|
||||
impl ImageViewWrapper {
|
||||
pub fn new(image: Image) -> Self {
|
||||
let view = View::new();
|
||||
let image_view = ImageView::new();
|
||||
image_view.set_image(&image);
|
||||
view.add_subview(&image_view);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
image_view
|
||||
.leading
|
||||
.constraint_equal_to(&view.leading)
|
||||
.offset(10.),
|
||||
image_view.top.constraint_equal_to(&view.top).offset(10.),
|
||||
image_view
|
||||
.bottom
|
||||
.constraint_equal_to(&view.bottom)
|
||||
.offset(-10.),
|
||||
]);
|
||||
Self {
|
||||
view,
|
||||
image_view,
|
||||
_image: image,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(&self) -> &View {
|
||||
&self.view
|
||||
}
|
||||
|
||||
pub fn image_view(&self) -> &ImageView {
|
||||
&self.image_view
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
#[cfg(not(all(
|
||||
target_os = "macos",
|
||||
all(feature = "macos-ui", not(feature = "force-crossplatform-ui"))
|
||||
)))]
|
||||
mod crossplatform;
|
||||
#[cfg(all(target_os = "macos", feature = "macos-ui",))]
|
||||
mod macos;
|
||||
|
||||
mod config;
|
||||
mod gamelist;
|
||||
|
||||
fn main() {
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
env_logger::init();
|
||||
#[cfg(not(all(
|
||||
target_os = "macos",
|
||||
all(feature = "macos-ui", not(feature = "force-crossplatform-ui"))
|
||||
)))]
|
||||
{
|
||||
crossplatform::run().unwrap();
|
||||
}
|
||||
#[cfg(all(
|
||||
target_os = "macos",
|
||||
all(feature = "macos-ui", not(feature = "force-crossplatform-ui"))
|
||||
))]
|
||||
{
|
||||
cacao::appkit::App::new("com.alexjanka.cacao-test", macos::TwincUiApp::default()).run();
|
||||
}
|
||||
}
|
|
@ -1,66 +1,17 @@
|
|||
[package]
|
||||
name = "gb-emu-lib"
|
||||
version = "0.5.1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["config"]
|
||||
default = []
|
||||
clocked-serial = []
|
||||
librashader = [
|
||||
"dep:librashader",
|
||||
"dep:librashader-presets",
|
||||
"dep:librashader-common",
|
||||
]
|
||||
vulkan-renderer = [
|
||||
"renderer",
|
||||
"librashader",
|
||||
"dep:ash",
|
||||
"dep:ash-window",
|
||||
"dep:naga",
|
||||
"librashader/runtime-vk",
|
||||
]
|
||||
vulkan-static = ["dep:ash-molten", "vulkan-renderer"]
|
||||
vulkan-debug = []
|
||||
pixels-renderer = ["renderer", "dep:pixels"]
|
||||
wgpu-renderer = [
|
||||
"renderer",
|
||||
"librashader",
|
||||
"librashader/runtime-wgpu",
|
||||
"dep:wgpu",
|
||||
]
|
||||
renderer = []
|
||||
config = ["dep:directories", "dep:ron"]
|
||||
error-colour = []
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.5"
|
||||
async-ringbuf = "0.2.1"
|
||||
futures = "0.3.30"
|
||||
itertools = "0.13.0"
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_with = "3.9.0"
|
||||
bytemuck = "1.16.3"
|
||||
num-traits = "0.2.19"
|
||||
pixels = { git = "https://git.alexjanka.com/alex/pixels", optional = true }
|
||||
ash = { workspace = true, features = ["linked"], optional = true }
|
||||
ash-window = { workspace = true, optional = true }
|
||||
raw-window-handle = { workspace = true }
|
||||
librashader = { workspace = true, optional = true }
|
||||
librashader-presets = { workspace = true, optional = true }
|
||||
librashader-common = { workspace = true, optional = true }
|
||||
directories = { version = "5.0.1", optional = true }
|
||||
ron = { version = "0.8.1", optional = true }
|
||||
lazy_static = "1.5.0"
|
||||
wgpu = { version = "22.1.0", optional = true }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
anyhow = "1.0.86"
|
||||
|
||||
[build-dependencies]
|
||||
naga = { version = "22.1.0", optional = true, features = [
|
||||
"wgsl-in",
|
||||
"spv-out",
|
||||
] }
|
||||
|
||||
[target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies]
|
||||
ash-molten = { version = "0.19.0", optional = true }
|
||||
async-ringbuf = "0.1.2"
|
||||
futures = "0.3"
|
||||
once_cell = "1.17.1"
|
||||
itertools = "0.10.5"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_with = "2.3.1"
|
||||
|
|
|
@ -1,164 +0,0 @@
|
|||
use std::{
|
||||
fs,
|
||||
io::{BufReader, BufWriter},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CONFIG_MANAGER: ConfigManager =
|
||||
ConfigManager::get().expect("Error loading configmanager!");
|
||||
}
|
||||
|
||||
pub trait NamedConfig {
|
||||
fn name() -> String;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigManager {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl ConfigManager {
|
||||
fn get() -> Option<Self> {
|
||||
directories::ProjectDirs::from("com", "alexjanka", "TWINC")
|
||||
.map(|v| v.config_dir().to_path_buf())
|
||||
.map(|path| {
|
||||
if let Ok(false) = path.try_exists() {
|
||||
fs::create_dir_all(path.clone()).expect("Failed to create config dir");
|
||||
};
|
||||
Self { path }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dir(&self) -> PathBuf {
|
||||
self.path.clone()
|
||||
}
|
||||
|
||||
pub fn load_or_create_base_config(&self) -> Config {
|
||||
self.load_or_create_config()
|
||||
}
|
||||
|
||||
pub fn load_or_create_config<C>(&self) -> C
|
||||
where
|
||||
C: NamedConfig + Serialize + DeserializeOwned + Default + Clone,
|
||||
{
|
||||
match self.load_custom_config::<C>() {
|
||||
Some(v) => {
|
||||
let _ = self.save_custom_config(v.clone());
|
||||
v
|
||||
}
|
||||
None => {
|
||||
let config = C::default();
|
||||
if let Ok(true) = self.path.join(C::name()).try_exists() {
|
||||
log::error!(
|
||||
"Failed to load \"{}\" config, but it exists on disk",
|
||||
C::name()
|
||||
);
|
||||
} else {
|
||||
let result = self.save_custom_config(config.clone());
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to save \"{}\" config: {e:#?}", C::name());
|
||||
}
|
||||
}
|
||||
config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_custom_config<C>(&self) -> Option<C>
|
||||
where
|
||||
C: NamedConfig + DeserializeOwned + Default,
|
||||
{
|
||||
let path = self.path.join(C::name());
|
||||
ron::de::from_reader(BufReader::new(fs::File::open(path).ok()?)).ok()
|
||||
}
|
||||
|
||||
pub fn save_custom_config<C>(&self, config: C) -> Result<(), ron::Error>
|
||||
where
|
||||
C: NamedConfig + Serialize,
|
||||
{
|
||||
let path = self.path.join(C::name());
|
||||
ron::ser::to_writer_pretty(
|
||||
BufWriter::new(fs::File::create(path)?),
|
||||
&config,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_custom_config_string<C>(config: C) -> Result<String, ron::Error>
|
||||
where
|
||||
C: Serialize,
|
||||
{
|
||||
ron::ser::to_string_pretty(&config, Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct Config {
|
||||
pub dmg_bootrom: Option<String>,
|
||||
pub cgb_bootrom: Option<String>,
|
||||
pub show_bootrom: bool,
|
||||
pub prefer_cgb: bool,
|
||||
pub vulkan_config: VulkanRendererConfig,
|
||||
}
|
||||
|
||||
impl NamedConfig for Config {
|
||||
fn name() -> String {
|
||||
String::from("base")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct VulkanRendererConfig {
|
||||
pub dmg_shader_path: Option<String>,
|
||||
pub dmg_shader_resizable: bool,
|
||||
pub dmg_resolution_override: ResolutionOverride,
|
||||
pub cgb_shader_path: Option<String>,
|
||||
pub cgb_shader_resizable: bool,
|
||||
pub cgb_resolution_override: ResolutionOverride,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
pub enum ResolutionOverride {
|
||||
Scale(usize),
|
||||
Default,
|
||||
}
|
||||
|
||||
impl From<Option<usize>> for ResolutionOverride {
|
||||
fn from(value: Option<usize>) -> Self {
|
||||
match value {
|
||||
Some(scale) => Self::Scale(scale),
|
||||
None => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dmg_bootrom: None,
|
||||
cgb_bootrom: None,
|
||||
show_bootrom: false,
|
||||
prefer_cgb: true,
|
||||
vulkan_config: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VulkanRendererConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dmg_shader_path: None,
|
||||
dmg_shader_resizable: false,
|
||||
dmg_resolution_override: ResolutionOverride::Default,
|
||||
cgb_shader_path: None,
|
||||
cgb_shader_resizable: false,
|
||||
cgb_resolution_override: ResolutionOverride::Default,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +1,15 @@
|
|||
use async_ringbuf::{traits::Split, AsyncHeapCons, AsyncHeapProd, AsyncHeapRb};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub use async_ringbuf::traits::consumer::AsyncConsumer;
|
||||
|
||||
pub use crate::error::RomHeaderError;
|
||||
pub use crate::processor::memory::mmio::gpu::Colour;
|
||||
use crate::processor::memory::mmio::gpu::Colour;
|
||||
pub use crate::processor::memory::mmio::joypad::{JoypadButtons, JoypadState};
|
||||
pub use crate::processor::memory::mmio::serial::{SerialTarget, StdoutType};
|
||||
use crate::processor::memory::rom::sram_save::SaveDataLocation;
|
||||
pub use crate::processor::memory::rom::{
|
||||
licensee::LicenseeCode, CartridgeType, CgbRomType, RamSize, RomHeader, RomSize,
|
||||
};
|
||||
pub use crate::processor::memory::Rom;
|
||||
pub use crate::processor::memory::mmio::serial::SerialTarget;
|
||||
pub use crate::processor::CpuSaveState;
|
||||
pub use crate::{HEIGHT, WIDTH};
|
||||
use async_ringbuf::{AsyncHeapConsumer, AsyncHeapProducer, AsyncHeapRb};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EmulatorMessage<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
Start,
|
||||
Pause,
|
||||
Exit,
|
||||
JoypadUpdate(JoypadState),
|
||||
NewLayerWindow(Sender<RendererMessage<ColourFormat>>),
|
||||
NewTileWindow(Sender<RendererMessage<ColourFormat>>),
|
||||
pub enum EmulatorMessage {
|
||||
Stop,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -37,80 +19,37 @@ pub enum DownsampleType {
|
|||
}
|
||||
|
||||
pub enum RomFile {
|
||||
Path(PathBuf),
|
||||
Path(String),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl RomFile {
|
||||
pub fn load(self, save: SramType) -> Result<Rom, std::io::Error> {
|
||||
match self {
|
||||
RomFile::Path(path) => {
|
||||
let save_location = match save {
|
||||
SramType::File(path) => Some(SaveDataLocation::File(path)),
|
||||
SramType::RawBuffer(buf) => Some(SaveDataLocation::Raw(buf)),
|
||||
SramType::Auto => Some(SaveDataLocation::File(path.with_extension("sav"))),
|
||||
SramType::None => None,
|
||||
};
|
||||
pub trait Renderer<Format: From<Colour>> {
|
||||
fn prepare(&mut self, width: usize, height: usize);
|
||||
|
||||
fs::read(path).map(|data| Rom::load(data, save_location))
|
||||
}
|
||||
RomFile::Raw(data) => {
|
||||
let save_location = match save {
|
||||
SramType::File(path) => Some(SaveDataLocation::File(path)),
|
||||
SramType::RawBuffer(buf) => Some(SaveDataLocation::Raw(buf)),
|
||||
SramType::Auto => None,
|
||||
SramType::None => None,
|
||||
};
|
||||
Ok(Rom::load(data, save_location))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn display(&mut self, buffer: &[Format]);
|
||||
|
||||
pub fn load_data(self) -> Result<Vec<u8>, std::io::Error> {
|
||||
match self {
|
||||
RomFile::Path(path) => std::fs::read(path),
|
||||
RomFile::Raw(data) => Ok(data),
|
||||
}
|
||||
}
|
||||
}
|
||||
fn set_title(&mut self, _title: String) {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RendererMessage<Format: From<Colour>> {
|
||||
Prepare { width: usize, height: usize },
|
||||
Resize { width: usize, height: usize },
|
||||
Display { buffer: Vec<Format> },
|
||||
SetTitle { title: String },
|
||||
Rumble { rumble: bool },
|
||||
}
|
||||
fn latest_joypad_state(&mut self) -> JoypadState;
|
||||
|
||||
impl<Format: From<Colour>> RendererMessage<Format> {
|
||||
pub fn display_message(buffer: Vec<Format>) -> Self {
|
||||
Self::Display { buffer }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "renderer")]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ResolutionData {
|
||||
pub real_width: u32,
|
||||
pub real_height: u32,
|
||||
pub scaled_width: u32,
|
||||
pub scaled_height: u32,
|
||||
fn set_rumble(&mut self, _rumbling: bool) {}
|
||||
}
|
||||
|
||||
pub struct AudioOutput {
|
||||
pub sample_rate: f32,
|
||||
pub send_rb: AsyncHeapProd<[f32; 2]>,
|
||||
pub send_rb: AsyncHeapProducer<[f32; 2]>,
|
||||
pub wait_for_output: bool,
|
||||
pub downsample_type: DownsampleType,
|
||||
}
|
||||
|
||||
impl AudioOutput {
|
||||
pub fn new(
|
||||
sample_rate: f32,
|
||||
buffers_per_frame: usize,
|
||||
wait_for_output: bool,
|
||||
frames_to_buffer: usize,
|
||||
downsample_type: DownsampleType,
|
||||
) -> (Self, AsyncHeapCons<[f32; 2]>) {
|
||||
let rb_len = (sample_rate as usize / 60) / buffers_per_frame;
|
||||
) -> (Self, AsyncHeapConsumer<[f32; 2]>) {
|
||||
let rb_len = (sample_rate as usize / 60) * frames_to_buffer;
|
||||
|
||||
let rb = AsyncHeapRb::<[f32; 2]>::new(rb_len);
|
||||
let (send_rb, rx) = rb.split();
|
||||
|
@ -119,6 +58,7 @@ impl AudioOutput {
|
|||
Self {
|
||||
sample_rate,
|
||||
send_rb,
|
||||
wait_for_output,
|
||||
downsample_type,
|
||||
},
|
||||
rx,
|
||||
|
@ -161,7 +101,9 @@ impl PocketCamera for NoCamera {
|
|||
fn init(&mut self) {}
|
||||
}
|
||||
|
||||
pub struct CameraWrapper<C>
|
||||
pub(crate) type CameraWrapperRef<C> = Arc<Mutex<CameraWrapper<C>>>;
|
||||
|
||||
pub(crate) struct CameraWrapper<C>
|
||||
where
|
||||
C: PocketCamera,
|
||||
{
|
||||
|
@ -174,7 +116,6 @@ impl<C> CameraWrapper<C>
|
|||
where
|
||||
C: PocketCamera,
|
||||
{
|
||||
#[allow(unused)]
|
||||
pub(crate) fn new(camera: C) -> Self {
|
||||
Self {
|
||||
inner: camera,
|
||||
|
@ -187,7 +128,6 @@ where
|
|||
self.counter > 0
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn tick(&mut self, steps: usize) {
|
||||
if self.counter > 0 {
|
||||
self.counter = match self.counter.checked_sub(steps) {
|
||||
|
@ -210,116 +150,97 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SramType {
|
||||
File(PathBuf),
|
||||
RawBuffer(Arc<RwLock<Vec<u8>>>),
|
||||
Auto,
|
||||
None,
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
pub struct EmulatorOptions<ColourFormat>
|
||||
pub struct EmulatorOptions<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub(crate) window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
pub(crate) tile_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
pub(crate) layer_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
pub(crate) rom: Rom,
|
||||
pub(crate) window: R,
|
||||
pub(crate) tile_window: Option<R>,
|
||||
pub(crate) rom: RomFile,
|
||||
pub(crate) output: AudioOutput,
|
||||
pub(crate) save: Option<SramType>,
|
||||
|
||||
pub(crate) dmg_bootrom: Option<RomFile>,
|
||||
pub(crate) cgb_bootrom: Option<RomFile>,
|
||||
pub(crate) show_bootrom: bool,
|
||||
pub(crate) no_output: bool,
|
||||
pub(crate) save_path: Option<String>,
|
||||
pub(crate) camera: C,
|
||||
pub(crate) no_save: bool,
|
||||
pub(crate) bootrom: Option<RomFile>,
|
||||
pub(crate) serial_target: SerialTarget,
|
||||
pub(crate) cgb_mode: bool,
|
||||
pub(crate) verbose: bool,
|
||||
spooky: PhantomData<ColourFormat>,
|
||||
}
|
||||
|
||||
impl<ColourFormat> EmulatorOptions<ColourFormat>
|
||||
impl<ColourFormat, R> EmulatorOptions<ColourFormat, R, NoCamera>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn new(
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
rom: Rom,
|
||||
output: AudioOutput,
|
||||
) -> Self {
|
||||
pub fn new(window: R, rom: RomFile, output: AudioOutput) -> Self {
|
||||
Self {
|
||||
window,
|
||||
tile_window: None,
|
||||
layer_window: None,
|
||||
rom,
|
||||
output,
|
||||
save: None,
|
||||
dmg_bootrom: None,
|
||||
cgb_bootrom: None,
|
||||
show_bootrom: false,
|
||||
no_output: false,
|
||||
save_path: None,
|
||||
camera: NoCamera::default(),
|
||||
no_save: false,
|
||||
bootrom: None,
|
||||
serial_target: SerialTarget::None,
|
||||
cgb_mode: true,
|
||||
verbose: false,
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
pub fn new_with_config(
|
||||
config: crate::config::Config,
|
||||
config_dir: PathBuf,
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
rom: Rom,
|
||||
output: AudioOutput,
|
||||
) -> Self {
|
||||
impl<ColourFormat, R, C> EmulatorOptions<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub fn new_with_camera(window: R, rom: RomFile, output: AudioOutput, camera: C) -> Self {
|
||||
Self {
|
||||
window,
|
||||
tile_window: None,
|
||||
layer_window: None,
|
||||
rom,
|
||||
output,
|
||||
save: None,
|
||||
dmg_bootrom: config
|
||||
.dmg_bootrom
|
||||
.map(|v| config_dir.join(v))
|
||||
.map(RomFile::Path),
|
||||
cgb_bootrom: config
|
||||
.cgb_bootrom
|
||||
.map(|v| config_dir.join(v))
|
||||
.map(RomFile::Path),
|
||||
show_bootrom: config.show_bootrom,
|
||||
no_output: false,
|
||||
save_path: None,
|
||||
camera,
|
||||
no_save: false,
|
||||
bootrom: None,
|
||||
serial_target: SerialTarget::None,
|
||||
cgb_mode: config.prefer_cgb,
|
||||
verbose: false,
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_sram_buffer(mut self, buffer: Arc<RwLock<Vec<u8>>>) -> Self {
|
||||
self.save = Some(SramType::RawBuffer(buffer));
|
||||
pub fn with_save_path(mut self, path: Option<String>) -> Self {
|
||||
self.save_path = path;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_dmg_bootrom(mut self, dmg_bootrom: Option<RomFile>) -> Self {
|
||||
self.dmg_bootrom = dmg_bootrom;
|
||||
pub fn force_no_save(mut self) -> Self {
|
||||
self.no_save = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cgb_bootrom(mut self, cgb_bootrom: Option<RomFile>) -> Self {
|
||||
self.cgb_bootrom = cgb_bootrom;
|
||||
pub fn with_no_save(mut self, no_save: bool) -> Self {
|
||||
self.no_save = no_save;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_show_bootrom(mut self, show_bootrom: bool) -> Self {
|
||||
self.show_bootrom = show_bootrom;
|
||||
pub fn with_verbose(mut self, verbose: bool) -> Self {
|
||||
self.verbose = verbose;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_no_output(mut self, no_output: bool) -> Self {
|
||||
self.no_output = no_output;
|
||||
pub fn with_bootrom(mut self, bootrom: Option<RomFile>) -> Self {
|
||||
self.bootrom = bootrom;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_stdout(mut self) -> Self {
|
||||
self.serial_target = SerialTarget::Stdout(StdoutType::Ascii);
|
||||
self.serial_target = SerialTarget::Stdout;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -328,35 +249,13 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_tile_window(
|
||||
mut self,
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
) -> Self {
|
||||
pub fn with_tile_window(mut self, window: Option<R>) -> Self {
|
||||
self.tile_window = window;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_layer_window(
|
||||
mut self,
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
) -> Self {
|
||||
self.layer_window = window;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cgb_mode(mut self, cgb_mode: bool) -> Self {
|
||||
self.cgb_mode = cgb_mode;
|
||||
pub fn verbose(mut self) -> Self {
|
||||
self.verbose = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EmulatorCoreTrait {
|
||||
fn replace_output(&mut self, new: AudioOutput);
|
||||
fn cycle_count(&self) -> usize;
|
||||
fn pc(&self) -> u16;
|
||||
fn print_reg(&self) -> String;
|
||||
fn get_memory(&self, address: u16) -> u8;
|
||||
fn run(&mut self, cycles: usize) -> bool;
|
||||
fn run_until_buffer_full(&mut self);
|
||||
fn process_messages(&mut self) -> bool;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,2 @@
|
|||
use crate::connect::Colour;
|
||||
|
||||
// Hz
|
||||
pub const CLOCK_SPEED: usize = 4194304;
|
||||
|
||||
pub(crate) const ERROR_COLOUR: Colour = Colour(0xFF, 0x00, 0x00);
|
||||
|
||||
pub(crate) mod dmg_colours {
|
||||
use crate::connect::Colour;
|
||||
// validation b&w (dmg-acid2 etc.)
|
||||
pub(crate) const ZERO: Colour = Colour(0xFF, 0xFF, 0xFF);
|
||||
pub(crate) const ONE: Colour = Colour(0xAA, 0xAA, 0xAA);
|
||||
pub(crate) const TWO: Colour = Colour(0x55, 0x55, 0x55);
|
||||
pub(crate) const THREE: Colour = Colour(0x00, 0x00, 0x00);
|
||||
|
||||
// from https://www.designpieces.com/palette/game-boy-original-color-palette-hex-and-rgb/
|
||||
// pub(crate) const ZERO: Colour = Colour(0x9B, 0xBC, 0x0F);
|
||||
// pub(crate) const ONE: Colour = Colour(0x8B, 0xAC, 0x0F);
|
||||
// pub(crate) const TWO: Colour = Colour(0x30, 0x62, 0x30);
|
||||
// pub(crate) const THREE: Colour = Colour(0x0F, 0x38, 0x0F);
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RomHeaderError {
|
||||
#[error("slice not long enough for rom file")]
|
||||
SliceLength,
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
#[error("invalid ROM size")]
|
||||
InvalidRomSize,
|
||||
#[error("invalid RAM size")]
|
||||
InvalidRamSize,
|
||||
#[error("invalid MBC")]
|
||||
InvalidMBC,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AddressError {
|
||||
OutOfBounds,
|
||||
}
|
||||
|
||||
#[cfg(feature = "pixels-renderer")]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PixelsError {
|
||||
#[error("pixels error")]
|
||||
Pixels(#[from] pixels::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu-renderer")]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WgpuError {
|
||||
#[error("no adapter")]
|
||||
NoAdapter,
|
||||
#[error("no texture format")]
|
||||
NoTextureFormat,
|
||||
#[error("rwh error")]
|
||||
RawWindowHandle(#[from] raw_window_handle::HandleError),
|
||||
#[error("create surface error")]
|
||||
CreateSurface(#[from] wgpu::CreateSurfaceError),
|
||||
#[error("wgpu surface error")]
|
||||
Surface(#[from] wgpu::SurfaceError),
|
||||
#[error("request device error")]
|
||||
RequestDevice(#[from] wgpu::RequestDeviceError),
|
||||
#[error("librashader filterchain error")]
|
||||
FilterChain(#[from] librashader::runtime::wgpu::error::FilterChainError),
|
||||
#[error("couldn't load")]
|
||||
CouldntLoad,
|
||||
}
|
||||
|
||||
#[cfg(feature = "vulkan-renderer")]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VulkanError {
|
||||
#[error("vulkan error")]
|
||||
Vulkan, //(#[from] vk::Error),
|
||||
}
|
361
lib/src/lib.rs
361
lib/src/lib.rs
|
@ -1,238 +1,225 @@
|
|||
#![feature(let_chains, bigint_helper_methods)]
|
||||
#![feature(exclusive_range_pattern, let_chains, bigint_helper_methods)]
|
||||
|
||||
use crate::processor::{memory::Memory, Flags};
|
||||
use anyhow::Context;
|
||||
use connect::{AudioOutput, EmulatorCoreTrait, EmulatorMessage, EmulatorOptions, RomFile};
|
||||
use processor::{
|
||||
memory::{mmio::gpu::Colour, rom::CgbRomType, OutputTargets},
|
||||
Cpu,
|
||||
use crate::{processor::memory::Memory, util::pause};
|
||||
use connect::{
|
||||
AudioOutput, CameraWrapper, CameraWrapperRef, EmulatorMessage, EmulatorOptions, NoCamera,
|
||||
PocketCamera, Renderer, RomFile, SerialTarget,
|
||||
};
|
||||
use std::sync::mpsc::Receiver;
|
||||
use once_cell::sync::OnceCell;
|
||||
use processor::{
|
||||
memory::{mmio::gpu::Colour, Rom},
|
||||
Cpu, CpuSaveState,
|
||||
};
|
||||
use std::{
|
||||
fs::{self},
|
||||
io::{stdout, Write},
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
str::FromStr,
|
||||
sync::{mpsc::Receiver, Arc, Mutex},
|
||||
};
|
||||
use util::pause_then_step;
|
||||
|
||||
pub mod error;
|
||||
pub mod renderer;
|
||||
|
||||
#[cfg(feature = "config")]
|
||||
pub mod config;
|
||||
pub mod connect;
|
||||
mod constants;
|
||||
mod processor;
|
||||
pub mod util;
|
||||
|
||||
static mut PAUSE_ENABLED: bool = false;
|
||||
static mut PAUSE_QUEUED: bool = false;
|
||||
|
||||
static VERBOSE: OnceCell<bool> = OnceCell::new();
|
||||
|
||||
pub const WIDTH: usize = 160;
|
||||
pub const HEIGHT: usize = 144;
|
||||
|
||||
pub struct EmulatorCore<ColourFormat>
|
||||
pub struct EmulatorCore<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
receiver: Receiver<EmulatorMessage<ColourFormat>>,
|
||||
cpu: Cpu<ColourFormat>,
|
||||
paused: bool,
|
||||
receiver: Receiver<EmulatorMessage>,
|
||||
cpu: Cpu<ColourFormat, R, C>,
|
||||
spooky: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<ColourFormat> EmulatorCore<ColourFormat>
|
||||
impl<ColourFormat, R, C> EmulatorCore<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy + Sync + Send + 'static,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub fn init(
|
||||
paused: bool,
|
||||
receiver: Receiver<EmulatorMessage<ColourFormat>>,
|
||||
options: EmulatorOptions<ColourFormat>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let rom = options.rom;
|
||||
|
||||
let is_cgb_mode = rom.rom_type == CgbRomType::CgbOnly || options.cgb_mode;
|
||||
let bootrom = if is_cgb_mode {
|
||||
options.cgb_bootrom.unwrap_or(RomFile::Raw(
|
||||
include_bytes!("../../sameboy-bootroms/cgb_boot.bin").to_vec(),
|
||||
))
|
||||
} else {
|
||||
options.dmg_bootrom.unwrap_or(RomFile::Raw(
|
||||
include_bytes!("../../sameboy-bootroms/dmg_boot.bin").to_vec(),
|
||||
))
|
||||
}
|
||||
.load_data()
|
||||
.context("couldn't load bootrom")?;
|
||||
|
||||
if let Some(window) = &options.window {
|
||||
window.send(connect::RendererMessage::Prepare {
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
})?;
|
||||
window.send(connect::RendererMessage::SetTitle {
|
||||
title: format!(
|
||||
"{} on {} on {}",
|
||||
rom.get_title(),
|
||||
rom.mbc_type(),
|
||||
if is_cgb_mode { "CGB" } else { "DMG" }
|
||||
),
|
||||
})?
|
||||
receiver: Receiver<EmulatorMessage>,
|
||||
mut options: EmulatorOptions<ColourFormat, R, C>,
|
||||
) -> Self {
|
||||
if options.verbose {
|
||||
VERBOSE.set(true).unwrap();
|
||||
}
|
||||
|
||||
Ok(Self::new(
|
||||
paused,
|
||||
let camera: CameraWrapperRef<C> = Arc::new(Mutex::new(CameraWrapper::new(options.camera)));
|
||||
|
||||
let rom = match options.rom {
|
||||
RomFile::Path(path) => {
|
||||
let maybe_save = if options.no_save {
|
||||
None
|
||||
} else {
|
||||
Some(if let Some(path) = options.save_path {
|
||||
PathBuf::from_str(&path).unwrap()
|
||||
} else {
|
||||
PathBuf::from_str(&path).unwrap().with_extension("sav")
|
||||
})
|
||||
};
|
||||
|
||||
match fs::read(path) {
|
||||
Ok(data) => Rom::load(data, maybe_save, camera.clone()),
|
||||
Err(e) => {
|
||||
println!("Error reading ROM: {e}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
RomFile::Raw(data) => Rom::load(data, None, camera.clone()),
|
||||
};
|
||||
|
||||
options.window.prepare(WIDTH, HEIGHT);
|
||||
options
|
||||
.window
|
||||
.set_title(format!("{} on {}", rom.get_title(), rom.mbc_type()));
|
||||
|
||||
let bootrom_enabled = options.bootrom.is_some();
|
||||
let bootrom: Option<Vec<u8>> = options.bootrom.map(|v| match v {
|
||||
RomFile::Path(path) => match fs::read(path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("Error reading bootROM: {e}");
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
RomFile::Raw(data) => data,
|
||||
});
|
||||
|
||||
Self::new(
|
||||
receiver,
|
||||
Cpu::new(
|
||||
Memory::init(
|
||||
is_cgb_mode,
|
||||
bootrom,
|
||||
rom,
|
||||
OutputTargets::new(
|
||||
options.window,
|
||||
options.output,
|
||||
options.serial_target,
|
||||
options.tile_window,
|
||||
options.layer_window,
|
||||
),
|
||||
options.window,
|
||||
options.output,
|
||||
options.serial_target,
|
||||
options.tile_window,
|
||||
camera,
|
||||
),
|
||||
options.show_bootrom,
|
||||
options.no_output,
|
||||
bootrom_enabled,
|
||||
),
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> EmulatorCore<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
fn new(
|
||||
paused: bool,
|
||||
receiver: Receiver<EmulatorMessage<ColourFormat>>,
|
||||
cpu: Cpu<ColourFormat>,
|
||||
) -> Self {
|
||||
fn new(receiver: Receiver<EmulatorMessage>, cpu: Cpu<ColourFormat, R, C>) -> Self {
|
||||
Self {
|
||||
receiver,
|
||||
cpu,
|
||||
paused,
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_flags(&self) -> String {
|
||||
format!(
|
||||
"{}{}{}{}",
|
||||
if self.cpu.is_flag(Flags::Zero) {
|
||||
"Z"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if self.cpu.is_flag(Flags::NSubtract) {
|
||||
"N"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if self.cpu.is_flag(Flags::HalfCarry) {
|
||||
"H"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
if self.cpu.is_flag(Flags::Carry) {
|
||||
"C"
|
||||
} else {
|
||||
"-"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn run_cycle(&mut self) {
|
||||
self.cpu.exec_next();
|
||||
}
|
||||
|
||||
fn process_messages(&mut self) -> bool {
|
||||
while let Ok(msg) = self.receiver.try_recv() {
|
||||
if self.process_message(msg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
while self.paused {
|
||||
match self.receiver.recv() {
|
||||
Ok(msg) => {
|
||||
if self.process_message(msg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("no message sender! error {e:#?}"),
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn process_message(&mut self, msg: EmulatorMessage<ColourFormat>) -> bool {
|
||||
match msg {
|
||||
EmulatorMessage::Exit => {
|
||||
self.cpu.memory.flush_rom();
|
||||
return true;
|
||||
}
|
||||
EmulatorMessage::Start => self.paused = false,
|
||||
EmulatorMessage::Pause => self.paused = true,
|
||||
EmulatorMessage::JoypadUpdate(new_state) => {
|
||||
self.cpu.next_joypad_state = Some(new_state)
|
||||
}
|
||||
EmulatorMessage::NewLayerWindow(new) => self.cpu.memory.gpu.set_layer_window(new),
|
||||
EmulatorMessage::NewTileWindow(new) => self.cpu.memory.gpu.set_tile_window(new),
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> EmulatorCoreTrait for EmulatorCore<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
fn replace_output(&mut self, new: AudioOutput) {
|
||||
pub fn replace_output(&mut self, new: AudioOutput) {
|
||||
self.cpu.memory.replace_output(new);
|
||||
}
|
||||
|
||||
fn cycle_count(&self) -> usize {
|
||||
self.cpu.cycle_count
|
||||
pub fn run(&mut self) {
|
||||
self.process_messages();
|
||||
self.run_cycle();
|
||||
}
|
||||
|
||||
fn pc(&self) -> u16 {
|
||||
self.cpu.reg.pc
|
||||
}
|
||||
|
||||
fn print_reg(&self) -> String {
|
||||
format!(
|
||||
"A:{:0>2X}, F:{}, BC:{:0>4X}, DE:{:0>4X}, HL:{:0>4X}, SP:{:0>4X}, PC:{:0>4X}\nLast instruction: {:0>2X} from 0x{:0>4X}",
|
||||
self.cpu.reg.get_8(processor::Reg8::A),
|
||||
self.print_flags(),
|
||||
self.cpu.reg.bc,
|
||||
self.cpu.reg.de,
|
||||
self.cpu.reg.hl,
|
||||
self.cpu.reg.sp,
|
||||
self.cpu.reg.pc,
|
||||
self.cpu.last_instruction,
|
||||
self.cpu.last_instruction_addr
|
||||
)
|
||||
}
|
||||
|
||||
fn get_memory(&self, address: u16) -> u8 {
|
||||
self.cpu.memory.get(address)
|
||||
}
|
||||
|
||||
fn run(&mut self, cycles: usize) -> bool {
|
||||
if self.process_messages() {
|
||||
return true;
|
||||
}
|
||||
if !self.paused {
|
||||
for _ in 0..cycles {
|
||||
pub fn run_stepped(&mut self, step_size: usize) {
|
||||
loop {
|
||||
self.process_messages();
|
||||
for _ in 0..step_size {
|
||||
self.run_cycle();
|
||||
}
|
||||
stdout().flush().unwrap();
|
||||
pause();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn run_until_buffer_full(&mut self) {
|
||||
if self.process_messages() {
|
||||
return;
|
||||
}
|
||||
pub fn run_until_buffer_full(&mut self) {
|
||||
println!("hello from gameboy");
|
||||
while !self.cpu.memory.is_audio_buffer_full() {
|
||||
self.run_cycle();
|
||||
self.run();
|
||||
}
|
||||
println!("gooby from gameboy");
|
||||
}
|
||||
|
||||
fn run_cycle(&mut self) {
|
||||
// let will_pause = unsafe { PAUSE_QUEUED };
|
||||
// let pause_enabled = unsafe { PAUSE_ENABLED };
|
||||
self.cpu.exec_next();
|
||||
// if !pause_enabled && self.cpu.reg.pc >= 0x100 {
|
||||
// unsafe { PAUSE_ENABLED = true };
|
||||
// }
|
||||
// if will_pause {
|
||||
// pause_then_step();
|
||||
// }
|
||||
}
|
||||
|
||||
fn process_messages(&mut self) {
|
||||
while let Ok(msg) = self.receiver.try_recv() {
|
||||
match msg {
|
||||
EmulatorMessage::Stop => {
|
||||
self.cpu.memory.flush_rom();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_messages(&mut self) -> bool {
|
||||
self.process_messages()
|
||||
pub fn get_save_state(&self) -> CpuSaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
CpuSaveState::create(&self.cpu)
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat, R> EmulatorCore<ColourFormat, R, NoCamera>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn from_save_state(
|
||||
state: CpuSaveState<ColourFormat, R>,
|
||||
rom: RomFile,
|
||||
receiver: Receiver<EmulatorMessage>,
|
||||
window: R,
|
||||
output: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
) -> Self {
|
||||
let data = match rom {
|
||||
RomFile::Path(path) => match fs::read(path) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("Error reading ROM: {e}");
|
||||
exit(1);
|
||||
}
|
||||
},
|
||||
RomFile::Raw(data) => data,
|
||||
};
|
||||
Self {
|
||||
receiver,
|
||||
cpu: Cpu::from_save_state(
|
||||
state,
|
||||
data,
|
||||
window,
|
||||
output,
|
||||
serial_target,
|
||||
Arc::new(Mutex::new(CameraWrapper::new(NoCamera::default()))),
|
||||
),
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
152
lib/src/processor/instructions/instructions.rs
Normal file
152
lib/src/processor/instructions/instructions.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
use crate::{
|
||||
connect::{PocketCamera, Renderer},
|
||||
processor::{memory::mmio::gpu::Colour, Cpu, Direction, Flags, Reg8, SplitRegister},
|
||||
util::{clear_bit, get_bit, set_bit},
|
||||
};
|
||||
|
||||
impl<ColourFormat, R, C> Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub(crate) fn and(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first & second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.set_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn xor(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first ^ second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn or(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first | second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn cp(&mut self, first: u8, second: u8) {
|
||||
self.sub_u8s(first, second, false);
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.add_u8s(first, second, false)
|
||||
}
|
||||
|
||||
pub(crate) fn adc(&mut self, first: u8, second: u8) -> u8 {
|
||||
let carry = self.is_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.add_u8s(first, second, carry)
|
||||
}
|
||||
|
||||
pub(crate) fn sub(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.sub_u8s(first, second, false)
|
||||
}
|
||||
|
||||
pub(crate) fn sbc(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.sub_u8s(first, second, self.is_flag(Flags::Carry))
|
||||
}
|
||||
|
||||
pub(crate) fn inc(&mut self, reg: Reg8) {
|
||||
let result = self.reg.get_8(reg);
|
||||
let val = self.inc_raw(result);
|
||||
self.reg.set_8(reg, val);
|
||||
}
|
||||
|
||||
pub(crate) fn dec(&mut self, reg: Reg8) {
|
||||
let result = self.reg.get_8(reg);
|
||||
let val = self.dec_flags(result);
|
||||
self.reg.set_8(reg, val);
|
||||
}
|
||||
|
||||
pub(crate) fn inc_pair(&mut self, val: u16) -> u16 {
|
||||
val.wrapping_add(0x1)
|
||||
}
|
||||
|
||||
pub(crate) fn dec_pair(&mut self, val: u16) -> u16 {
|
||||
val.wrapping_sub(0x1)
|
||||
}
|
||||
|
||||
pub(crate) fn rlc(&mut self, byte: u8) -> u8 {
|
||||
self.rotate_with_carry(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn rrc(&mut self, byte: u8) -> u8 {
|
||||
self.rotate_with_carry(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn rl(&mut self, byte: u8) -> u8 {
|
||||
self.rotate(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn rr(&mut self, byte: u8) -> u8 {
|
||||
self.rotate(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn sla(&mut self, byte: u8) -> u8 {
|
||||
self.shift(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn sra(&mut self, byte: u8) -> u8 {
|
||||
let b = get_bit(byte, 7);
|
||||
let val = self.shift(byte, Direction::Right);
|
||||
if b {
|
||||
val + 0b10000000
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn srl(&mut self, byte: u8) -> u8 {
|
||||
self.shift(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn swap(&mut self, byte: u8) -> u8 {
|
||||
let swapped = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
|
||||
self.set_or_clear_flag(Flags::Zero, swapped == 0x0);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
swapped
|
||||
}
|
||||
|
||||
pub(crate) fn bit(&mut self, byte: u8, bit: u8) {
|
||||
self.set_or_clear_flag(Flags::Zero, !get_bit(byte, bit));
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.set_flag(Flags::HalfCarry);
|
||||
}
|
||||
|
||||
pub(crate) fn rst(&mut self, address: u8) {
|
||||
self.push(self.reg.pc);
|
||||
self.reg.pc.set_high(0x0);
|
||||
self.reg.pc.set_low(address);
|
||||
}
|
||||
|
||||
pub(crate) fn ret(&mut self) {
|
||||
self.reg.pc = self.pop_word();
|
||||
}
|
||||
|
||||
pub(crate) fn jr(&mut self, jump: i8) {
|
||||
self.reg.pc = self.reg.pc.wrapping_add_signed(jump.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn res(byte: u8, bit: u8) -> u8 {
|
||||
clear_bit(byte, bit)
|
||||
}
|
||||
|
||||
pub(crate) fn set(byte: u8, bit: u8) -> u8 {
|
||||
set_bit(byte, bit)
|
||||
}
|
|
@ -1,151 +1,3 @@
|
|||
#[allow(clippy::module_inception)]
|
||||
pub mod instructions;
|
||||
pub mod primitives;
|
||||
|
||||
use crate::{
|
||||
processor::{memory::mmio::gpu::Colour, Cpu, Direction, Flags, Reg8, SplitRegister},
|
||||
util::{clear_bit, get_bit, set_bit},
|
||||
};
|
||||
|
||||
impl<ColourFormat> Cpu<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(crate) fn and(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first & second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.set_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn xor(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first ^ second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn or(&mut self, first: u8, second: u8) -> u8 {
|
||||
let result = first | second;
|
||||
self.set_or_clear_flag(Flags::Zero, result == 0x0);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn cp(&mut self, first: u8, second: u8) {
|
||||
self.sub_u8s(first, second, false);
|
||||
}
|
||||
|
||||
pub(crate) fn add(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.add_u8s(first, second, false)
|
||||
}
|
||||
|
||||
pub(crate) fn adc(&mut self, first: u8, second: u8) -> u8 {
|
||||
let carry = self.is_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.add_u8s(first, second, carry)
|
||||
}
|
||||
|
||||
pub(crate) fn sub(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.sub_u8s(first, second, false)
|
||||
}
|
||||
|
||||
pub(crate) fn sbc(&mut self, first: u8, second: u8) -> u8 {
|
||||
self.sub_u8s(first, second, self.is_flag(Flags::Carry))
|
||||
}
|
||||
|
||||
pub(crate) fn inc(&mut self, reg: Reg8) {
|
||||
let result = self.reg.get_8(reg);
|
||||
let val = self.inc_raw(result);
|
||||
self.reg.set_8(reg, val);
|
||||
}
|
||||
|
||||
pub(crate) fn dec(&mut self, reg: Reg8) {
|
||||
let result = self.reg.get_8(reg);
|
||||
let val = self.dec_flags(result);
|
||||
self.reg.set_8(reg, val);
|
||||
}
|
||||
|
||||
pub(crate) fn inc_pair(&mut self, val: u16) -> u16 {
|
||||
val.wrapping_add(0x1)
|
||||
}
|
||||
|
||||
pub(crate) fn dec_pair(&mut self, val: u16) -> u16 {
|
||||
val.wrapping_sub(0x1)
|
||||
}
|
||||
|
||||
pub(crate) fn rlc(&mut self, byte: u8) -> u8 {
|
||||
self.rotate_with_carry(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn rrc(&mut self, byte: u8) -> u8 {
|
||||
self.rotate_with_carry(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn rl(&mut self, byte: u8) -> u8 {
|
||||
self.rotate(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn rr(&mut self, byte: u8) -> u8 {
|
||||
self.rotate(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn sla(&mut self, byte: u8) -> u8 {
|
||||
self.shift(byte, Direction::Left)
|
||||
}
|
||||
|
||||
pub(crate) fn sra(&mut self, byte: u8) -> u8 {
|
||||
let b = get_bit(byte, 7);
|
||||
let val = self.shift(byte, Direction::Right);
|
||||
if b {
|
||||
val + 0b10000000
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn srl(&mut self, byte: u8) -> u8 {
|
||||
self.shift(byte, Direction::Right)
|
||||
}
|
||||
|
||||
pub(crate) fn swap(&mut self, byte: u8) -> u8 {
|
||||
let swapped = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
|
||||
self.set_or_clear_flag(Flags::Zero, swapped == 0x0);
|
||||
self.clear_flag(Flags::Carry);
|
||||
self.clear_flag(Flags::HalfCarry);
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
swapped
|
||||
}
|
||||
|
||||
pub(crate) fn bit(&mut self, byte: u8, bit: u8) {
|
||||
self.set_or_clear_flag(Flags::Zero, !get_bit(byte, bit));
|
||||
self.clear_flag(Flags::NSubtract);
|
||||
self.set_flag(Flags::HalfCarry);
|
||||
}
|
||||
|
||||
pub(crate) fn rst(&mut self, address: u8) {
|
||||
self.push(self.reg.pc);
|
||||
self.reg.pc.set_high(0x0);
|
||||
self.reg.pc.set_low(address);
|
||||
}
|
||||
|
||||
pub(crate) fn ret(&mut self) {
|
||||
self.reg.pc = self.pop_word();
|
||||
}
|
||||
|
||||
pub(crate) fn jr(&mut self, jump: i8) {
|
||||
self.reg.pc = self.reg.pc.wrapping_add_signed(jump.into());
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn res(byte: u8, bit: u8) -> u8 {
|
||||
clear_bit(byte, bit)
|
||||
}
|
||||
|
||||
pub(crate) fn set(byte: u8, bit: u8) -> u8 {
|
||||
set_bit(byte, bit)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
use crate::{
|
||||
connect::{PocketCamera, Renderer},
|
||||
processor::{memory::mmio::gpu::Colour, Cpu, Direction, Flags, SplitRegister},
|
||||
util::{as_signed, get_bit, get_rotation_carry, rotate, Nibbles},
|
||||
};
|
||||
use std::ops::{BitAnd, BitOr};
|
||||
|
||||
impl<ColourFormat> Cpu<ColourFormat>
|
||||
impl<ColourFormat, R, C> Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub(crate) fn pop_word(&mut self) -> u16 {
|
||||
let mut word: u16 = 0x0;
|
||||
|
|
|
@ -1,400 +1,255 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub use self::rom::Rom;
|
||||
use self::{
|
||||
addresses::{Address, AddressMarker, CgbIoAddress, IoAddress},
|
||||
mmio::{
|
||||
cgb::{DoubleSpeed, Infrared, VramDma},
|
||||
gpu::Colour,
|
||||
Apu, Gpu, Joypad, OamDma, Serial, Timer,
|
||||
apu::ApuSaveState,
|
||||
gpu::{Colour, GpuSaveState},
|
||||
serial::SerialSaveState,
|
||||
Apu, Gpu, Joypad, Serial, Timer,
|
||||
},
|
||||
rom::RomSaveState,
|
||||
};
|
||||
use crate::{
|
||||
connect::{AudioOutput, JoypadState, RendererMessage, SerialTarget},
|
||||
Cpu,
|
||||
connect::{AudioOutput, CameraWrapperRef, JoypadState, PocketCamera, Renderer, SerialTarget},
|
||||
processor::SplitRegister,
|
||||
verbose_println, Cpu,
|
||||
};
|
||||
|
||||
mod interrupts;
|
||||
pub use interrupts::{Interrupt, Interrupts};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub(crate) mod addresses;
|
||||
pub mod mmio;
|
||||
pub(crate) mod rom;
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Wram {
|
||||
#[serde_as(as = "Box<[_; 4096]>")]
|
||||
bank_0: Box<[u8; 4096]>,
|
||||
banks: WramBanks,
|
||||
}
|
||||
pub(crate) type Address = u16;
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
enum WramBanks {
|
||||
Dmg {
|
||||
#[serde_as(as = "Box<[_; 4096]>")]
|
||||
bank: Box<[u8; 4096]>,
|
||||
},
|
||||
Cgb {
|
||||
#[serde_as(as = "Box<[[_; 4096]; 8]>")]
|
||||
banks: Box<[[u8; 4096]; 8]>,
|
||||
selected: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Wram {
|
||||
fn new(cgb: bool) -> Self {
|
||||
Self {
|
||||
bank_0: Box::new([0; 4096]),
|
||||
banks: if cgb {
|
||||
WramBanks::Cgb {
|
||||
banks: Box::new([[0; 4096]; 8]),
|
||||
selected: 0,
|
||||
}
|
||||
} else {
|
||||
WramBanks::Dmg {
|
||||
bank: Box::new([0; 4096]),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_banked(&self, address: usize) -> u8 {
|
||||
match &self.banks {
|
||||
WramBanks::Dmg { bank } => bank[address],
|
||||
WramBanks::Cgb { banks, selected } => banks[*selected][address],
|
||||
}
|
||||
}
|
||||
|
||||
fn set_banked(&mut self, address: usize, data: u8) {
|
||||
match self.banks {
|
||||
WramBanks::Dmg { ref mut bank } => bank[address] = data,
|
||||
WramBanks::Cgb {
|
||||
ref mut banks,
|
||||
selected,
|
||||
} => banks[selected][address] = data,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Default)]
|
||||
struct CgbPeripherals {
|
||||
vram_dma: VramDma,
|
||||
infrared: Infrared,
|
||||
double_speed: DoubleSpeed,
|
||||
}
|
||||
|
||||
pub struct Memory<ColourFormat>
|
||||
pub struct Memory<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
bootrom: Option<Vec<u8>>,
|
||||
rom: Rom,
|
||||
ram: Wram,
|
||||
rom: Rom<C>,
|
||||
ram: [u8; 8192],
|
||||
cpu_ram: [u8; 128],
|
||||
pub(super) interrupts: Interrupts,
|
||||
pub(super) ime: bool,
|
||||
pub(super) ime_scheduled: u8,
|
||||
pub(super) user_mode: bool,
|
||||
oam_dma: OamDma,
|
||||
dma_addr: u8,
|
||||
joypad: Joypad,
|
||||
pub gpu: Gpu<ColourFormat>,
|
||||
gpu: Gpu<ColourFormat, R>,
|
||||
apu: Apu,
|
||||
serial: Serial,
|
||||
timers: Timer,
|
||||
cgb_peripherals: Option<CgbPeripherals>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
}
|
||||
|
||||
pub(crate) struct OutputTargets<ColourFormat>
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MemorySaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
audio: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
tile_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
layer_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
rom: RomSaveState,
|
||||
#[serde_as(as = "[_; 8192]")]
|
||||
ram: [u8; 8192],
|
||||
#[serde_as(as = "[_; 128]")]
|
||||
cpu_ram: [u8; 128],
|
||||
pub(super) interrupts: Interrupts,
|
||||
pub(super) ime: bool,
|
||||
pub(super) ime_scheduled: u8,
|
||||
dma_addr: u8,
|
||||
joypad: Joypad,
|
||||
gpu: GpuSaveState<ColourFormat, R>,
|
||||
apu: ApuSaveState,
|
||||
serial: SerialSaveState,
|
||||
timers: Timer,
|
||||
}
|
||||
|
||||
impl<ColourFormat> OutputTargets<ColourFormat>
|
||||
impl<ColourFormat, R> MemorySaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
// C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
audio: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
tile_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
layer_window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
) -> Self {
|
||||
pub fn create<C: PocketCamera + Send + 'static>(memory: &Memory<ColourFormat, R, C>) -> Self {
|
||||
Self {
|
||||
window,
|
||||
audio,
|
||||
serial_target,
|
||||
tile_window,
|
||||
layer_window,
|
||||
rom: RomSaveState::create(&memory.rom),
|
||||
ram: memory.ram,
|
||||
cpu_ram: memory.cpu_ram,
|
||||
interrupts: memory.interrupts,
|
||||
ime: memory.ime,
|
||||
ime_scheduled: memory.ime_scheduled,
|
||||
dma_addr: memory.dma_addr,
|
||||
joypad: memory.joypad,
|
||||
gpu: GpuSaveState::create(&memory.gpu),
|
||||
apu: ApuSaveState::create(&memory.apu),
|
||||
serial: SerialSaveState::create(&memory.serial),
|
||||
timers: memory.timers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Memory<ColourFormat>
|
||||
impl<ColourFormat, R, C> Memory<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub(crate) fn init(
|
||||
cgb: bool,
|
||||
bootrom: Vec<u8>,
|
||||
rom: Rom,
|
||||
output: OutputTargets<ColourFormat>,
|
||||
bootrom: Option<Vec<u8>>,
|
||||
rom: Rom<C>,
|
||||
window: R,
|
||||
output: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
tile_window: Option<R>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bootrom: Some(bootrom),
|
||||
bootrom,
|
||||
rom,
|
||||
ram: Wram::new(cgb),
|
||||
ram: [0x0; 8192],
|
||||
cpu_ram: [0x0; 128],
|
||||
interrupts: Interrupts::default(),
|
||||
ime: false,
|
||||
ime_scheduled: 0x0,
|
||||
user_mode: false,
|
||||
oam_dma: OamDma::default(),
|
||||
dma_addr: 0xFF,
|
||||
joypad: Joypad::default(),
|
||||
gpu: Gpu::new(cgb, output.window, output.tile_window, output.layer_window),
|
||||
apu: Apu::new(output.audio),
|
||||
serial: Serial::new(output.serial_target),
|
||||
gpu: Gpu::new(window, tile_window),
|
||||
apu: Apu::new(output),
|
||||
serial: Serial::new(serial_target),
|
||||
timers: Timer::init(),
|
||||
cgb_peripherals: if cgb {
|
||||
Some(CgbPeripherals::default())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
camera,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_bootrom(&self) -> bool {
|
||||
self.bootrom.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, address: impl Into<Address>) -> u8 {
|
||||
let address: Address = address.into();
|
||||
if self.oam_dma.is_active() && self.user_mode {
|
||||
if let Address::Hram(_) = address {
|
||||
} else if let Address::Io(IoAddress::Video(v)) = address {
|
||||
if v.inner() == 0xFF46 {
|
||||
} else {
|
||||
return 0xFF;
|
||||
}
|
||||
} else {
|
||||
return 0xFF;
|
||||
}
|
||||
}
|
||||
pub fn get(&self, address: Address) -> u8 {
|
||||
match address {
|
||||
Address::Rom(address) => {
|
||||
0x0..0x8000 => {
|
||||
// rom access
|
||||
// todo - switchable rom banks
|
||||
if let Some(bootrom) = &self.bootrom {
|
||||
if self.cgb_peripherals.is_some() {
|
||||
if address.inner() < 0x100
|
||||
|| (address.inner() >= 0x200
|
||||
&& ((address.inner()) as usize) < bootrom.len())
|
||||
{
|
||||
return bootrom[address.inner() as usize];
|
||||
}
|
||||
} else if (address.inner() as usize) < bootrom.len() {
|
||||
return bootrom[address.inner() as usize];
|
||||
}
|
||||
if let Some(bootrom) = &self.bootrom && (address as usize) < bootrom.len() {
|
||||
bootrom[address as usize]
|
||||
} else {
|
||||
self.rom.get(address)
|
||||
}
|
||||
self.rom.get(address)
|
||||
}
|
||||
Address::Vram(address) => self.gpu.get_vram(address),
|
||||
Address::CartRam(address) => self.rom.get_ram(address),
|
||||
Address::WorkRam(address) => self.ram.bank_0[address.get_local() as usize],
|
||||
Address::BankedWorkRam(address) => self.ram.get_banked((address.get_local()) as usize),
|
||||
Address::MirroredWorkRam(address) => self.ram.bank_0[address.get_local() as usize],
|
||||
Address::MirroredBankedWorkRam(address) => {
|
||||
self.ram.get_banked((address.get_local()) as usize)
|
||||
0x8000..0xA000 => self.gpu.vram.get(address),
|
||||
0xA000..0xC000 => {
|
||||
// cart ram
|
||||
self.rom.get_ram(address)
|
||||
}
|
||||
Address::Oam(address) => self.gpu.get_oam(address),
|
||||
Address::Prohibited(_) => 0xFF,
|
||||
Address::Io(address) => self.get_io(address),
|
||||
Address::Hram(address) => self.cpu_ram[address.get_local() as usize],
|
||||
Address::InterruptEnable(_) => self.interrupts.get_enable_register(),
|
||||
0xC000..0xE000 => self.ram[(address - 0xC000) as usize],
|
||||
0xE000..0xFE00 => self.ram[(address - 0xE000) as usize],
|
||||
0xFE00..0xFEA0 => self.gpu.oam.get(address),
|
||||
0xFEA0..0xFF00 => 0xFF,
|
||||
0xFF00..0xFF4C => self.get_io(address),
|
||||
0xFF4C..0xFF80 => 0xFF,
|
||||
0xFF80..0xFFFF => self.cpu_ram[(address - 0xFF80) as usize],
|
||||
0xFFFF => self.interrupts.get_enable_register(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, address: impl Into<Address>, data: u8) {
|
||||
let address: Address = address.into();
|
||||
if self.oam_dma.is_active() && self.user_mode {
|
||||
if let Address::Hram(_) = address {
|
||||
} else if let Address::Io(IoAddress::Video(v)) = address {
|
||||
if v.inner() == 0xFF46 {
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, address: Address, data: u8) {
|
||||
match address {
|
||||
Address::Rom(address) => {
|
||||
0x0..0x8000 => {
|
||||
// change this with MBC code...
|
||||
self.rom.set(address, data);
|
||||
if self.rom.can_rumble() {
|
||||
// rumble
|
||||
if let Some(window) = &self.gpu.window {
|
||||
window
|
||||
.send(RendererMessage::Rumble {
|
||||
rumble: self.rom.is_rumbling(),
|
||||
})
|
||||
.expect("message error");
|
||||
}
|
||||
self.gpu.window.set_rumble(self.rom.is_rumbling())
|
||||
}
|
||||
}
|
||||
Address::Vram(address) => self.gpu.set_vram(address, data),
|
||||
Address::CartRam(address) => self.rom.set_ram(address, data),
|
||||
Address::WorkRam(address) => self.ram.bank_0[address.get_local() as usize] = data,
|
||||
Address::BankedWorkRam(address) => {
|
||||
self.ram.set_banked(address.get_local() as usize, data)
|
||||
0x8000..0xA000 => self.gpu.vram.set(address, data),
|
||||
0xA000..0xC000 => self.rom.set_ram(address, data),
|
||||
0xC000..0xE000 => self.ram[(address - 0xC000) as usize] = data,
|
||||
0xE000..0xFE00 => self.ram[(address - 0xE000) as usize] = data,
|
||||
0xFE00..0xFEA0 => self.gpu.oam.set(address, data),
|
||||
0xFEA0..0xFF00 => {}
|
||||
0xFF00..0xFF4C => self.set_io(address, data),
|
||||
0xFF50 => self.bootrom = None,
|
||||
0xFF4C..0xFF50 | 0xFF51..0xFF80 => {}
|
||||
0xFF80..0xFFFF => self.cpu_ram[(address - 0xFF80) as usize] = data,
|
||||
0xFFFF => {
|
||||
verbose_println!("interrupts set to {:#b}", data);
|
||||
verbose_println!(" / {:#X}", data);
|
||||
self.interrupts.set_enable_register(data);
|
||||
}
|
||||
Address::MirroredWorkRam(address) => {
|
||||
self.ram.bank_0[address.get_local() as usize] = data
|
||||
}
|
||||
Address::MirroredBankedWorkRam(address) => {
|
||||
self.ram.set_banked(address.get_local() as usize, data)
|
||||
}
|
||||
Address::Oam(address) => self.gpu.set_oam(address, data),
|
||||
Address::Prohibited(_) => {}
|
||||
Address::Io(address) => {
|
||||
if address.inner() == 0xFF50 {
|
||||
self.bootrom = None
|
||||
} else {
|
||||
self.set_io(address, data)
|
||||
}
|
||||
}
|
||||
Address::Hram(address) => self.cpu_ram[address.get_local() as usize] = data,
|
||||
Address::InterruptEnable(_) => self.interrupts.set_enable_register(data),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_io(&self, address: IoAddress) -> u8 {
|
||||
fn get_io(&self, address: Address) -> u8 {
|
||||
// range: 0xFF00 - 0xFF4B inclusive
|
||||
match address {
|
||||
IoAddress::Joypad => self.joypad.as_register(),
|
||||
IoAddress::Serial(address) => match address.inner() {
|
||||
0xFF01 => self.serial.get_queued(),
|
||||
0xFF02 => self.serial.get_control(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
IoAddress::Timer(address) => match address.inner() {
|
||||
0xFF04 => self.timers.get_div(),
|
||||
0xFF05 => self.timers.get_tima(),
|
||||
0xFF06 => self.timers.get_tma(),
|
||||
0xFF07 => self.timers.get_timer_control(),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
IoAddress::InterruptFlag => self.interrupts.get_flag_register(),
|
||||
IoAddress::Audio(address) => self.apu.get_register(address),
|
||||
IoAddress::WaveRam(address) => self.apu.get_wave_ram_register(address),
|
||||
IoAddress::Video(address) => match address.inner() {
|
||||
0xFF40 => self.gpu.get_lcdc(),
|
||||
0xFF41 => self.gpu.get_lcd_status(),
|
||||
0xFF42 => self.gpu.get_scy(),
|
||||
0xFF43 => self.gpu.get_scx(),
|
||||
0xFF44 => self.gpu.get_ly(),
|
||||
0xFF45 => self.gpu.get_lyc(),
|
||||
0xFF46 => self.oam_dma.get_register(),
|
||||
0xFF47 => self.gpu.get_bg_palette(),
|
||||
0xFF48 => self.gpu.get_obj_palette_0(),
|
||||
0xFF49 => self.gpu.get_obj_palette_1(),
|
||||
0xFF4A => self.gpu.get_wy(),
|
||||
0xFF4B => self.gpu.get_wx(),
|
||||
0x0..0xFF40 | 0xFF4C..=0xFFFF => unreachable!(),
|
||||
},
|
||||
IoAddress::Cgb(address) => {
|
||||
if let WramBanks::Cgb { banks: _, selected } = &self.ram.banks
|
||||
&& let Some(cgb_peripherals) = &self.cgb_peripherals
|
||||
{
|
||||
match address {
|
||||
CgbIoAddress::CompatMode => self.gpu.get_compat_byte(),
|
||||
CgbIoAddress::PrepareSpeed => cgb_peripherals.double_speed.get(),
|
||||
CgbIoAddress::VramBank => self.gpu.vram.get_vram_bank(),
|
||||
CgbIoAddress::VramDma(address) => {
|
||||
cgb_peripherals.vram_dma.get_register(address)
|
||||
}
|
||||
CgbIoAddress::Infrared => cgb_peripherals.infrared.get(),
|
||||
CgbIoAddress::Palette(address) => self.gpu.get_cgb_palette(address),
|
||||
CgbIoAddress::ObjPriority => self.gpu.get_obj_priority(),
|
||||
CgbIoAddress::WramBank => ((*selected) & 0b111) as u8,
|
||||
CgbIoAddress::Pcm12 => self.apu.get_pcm_1_2(),
|
||||
CgbIoAddress::Pcm34 => self.apu.get_pcm_3_4(),
|
||||
CgbIoAddress::Unused(v) => {
|
||||
log::warn!("attempt to get unused address 0x{v:0>4X}");
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
IoAddress::Unused(_) => 0xFF,
|
||||
0xFF00 => self.joypad.as_register(),
|
||||
0xFF01 => self.serial.get_queued(),
|
||||
0xFF02 => self.serial.get_control(),
|
||||
0xFF04 => self.timers.get_div(),
|
||||
0xFF05 => self.timers.get_tima(),
|
||||
0xFF06 => self.timers.get_tma(),
|
||||
0xFF07 => self.timers.get_timer_control(),
|
||||
0xFF0F => self.interrupts.get_flag_register(),
|
||||
0xFF10..0xFF40 => self.apu.get_register(address),
|
||||
0xFF40 => self.gpu.get_lcdc(),
|
||||
0xFF41 => self.gpu.get_lcd_status(),
|
||||
0xFF42 => self.gpu.get_scy(),
|
||||
0xFF43 => self.gpu.get_scx(),
|
||||
0xFF44 => self.gpu.get_ly(),
|
||||
0xFF45 => self.gpu.get_lyc(),
|
||||
0xFF46 => self.dma_addr,
|
||||
0xFF47 => self.gpu.get_bg_palette(),
|
||||
0xFF48 => self.gpu.get_obj_palette_0(),
|
||||
0xFF49 => self.gpu.get_obj_palette_1(),
|
||||
0xFF4A => self.gpu.get_wy(),
|
||||
0xFF4B => self.gpu.get_wx(),
|
||||
0xFF03 | 0xFF08..0xFF0F => 0xFF,
|
||||
0x0..0xFF00 | 0xFF4C..=0xFFFF => panic!("passed wrong address to get_io"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_io(&mut self, address: IoAddress, data: u8) {
|
||||
fn set_io(&mut self, address: Address, data: u8) {
|
||||
// range: 0xFF00 - 0xFF4B inclusive
|
||||
match address {
|
||||
IoAddress::Joypad => self.joypad.mmio_write(data),
|
||||
IoAddress::Serial(address) => match address.inner() {
|
||||
0xFF01 => self.serial.update_queued(data),
|
||||
0xFF02 => self.serial.update_control(data),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
IoAddress::Timer(address) => match address.inner() {
|
||||
0xFF04 => self.timers.update_div(),
|
||||
0xFF05 => self.timers.update_tima(data),
|
||||
0xFF06 => self.timers.update_tma(data),
|
||||
0xFF07 => self.timers.update_timer_control(data),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
IoAddress::InterruptFlag => self.interrupts.set_flag_register(data),
|
||||
IoAddress::Audio(address) => self.apu.mmio_write(address, data),
|
||||
IoAddress::WaveRam(address) => self.apu.mmio_write_wave_ram(address, data),
|
||||
IoAddress::Video(address) => match address.inner() {
|
||||
0xFF40 => self.gpu.update_lcdc(data),
|
||||
0xFF41 => self.gpu.update_lcd_status(data),
|
||||
0xFF42 => self.gpu.update_scy(data),
|
||||
0xFF43 => self.gpu.update_scx(data),
|
||||
0xFF44 => {}
|
||||
0xFF45 => self.gpu.update_lyc(data),
|
||||
0xFF46 => self.oam_dma.set_register(data),
|
||||
0xFF47 => self.gpu.update_bg_palette(data),
|
||||
0xFF48 => self.gpu.update_obj_palette_0(data),
|
||||
0xFF49 => self.gpu.update_obj_palette_1(data),
|
||||
0xFF4A => self.gpu.update_wy(data),
|
||||
0xFF4B => self.gpu.update_wx(data),
|
||||
0x0..0xFF40 | 0xFF4C..=0xFFFF => unreachable!(),
|
||||
},
|
||||
IoAddress::Cgb(address) => {
|
||||
if let WramBanks::Cgb { banks: _, selected } = &mut self.ram.banks
|
||||
&& let Some(cgb_peripherals) = &mut self.cgb_peripherals
|
||||
{
|
||||
match address {
|
||||
CgbIoAddress::CompatMode => self.gpu.set_compat_byte(data),
|
||||
CgbIoAddress::PrepareSpeed => cgb_peripherals.double_speed.set(data),
|
||||
CgbIoAddress::VramBank => self.gpu.vram.set_vram_bank(data),
|
||||
CgbIoAddress::VramDma(address) => {
|
||||
cgb_peripherals.vram_dma.set_register(address, data)
|
||||
}
|
||||
CgbIoAddress::Infrared => cgb_peripherals.infrared.set(data),
|
||||
CgbIoAddress::Palette(address) => self.gpu.set_cgb_palette(address, data),
|
||||
CgbIoAddress::ObjPriority => self.gpu.set_obj_priority(data),
|
||||
CgbIoAddress::WramBank => *selected = (data & 0b111).max(1) as usize,
|
||||
CgbIoAddress::Pcm12 => {}
|
||||
CgbIoAddress::Pcm34 => {}
|
||||
CgbIoAddress::Unused(v) => {
|
||||
log::error!("attempt to set unused address 0x{v:0>4X} to 0x{data:0>2X}")
|
||||
}
|
||||
}
|
||||
0xFF00 => {
|
||||
// joypad
|
||||
self.joypad.mmio_write(data);
|
||||
}
|
||||
0xFF01 => self.serial.update_queued(data),
|
||||
0xFF02 => self.serial.update_control(data),
|
||||
0xFF04 => self.timers.update_div(),
|
||||
0xFF05 => self.timers.update_tima(data),
|
||||
0xFF06 => self.timers.update_tma(data),
|
||||
0xFF07 => self.timers.update_timer_control(data),
|
||||
0xFF0F => self.interrupts.set_flag_register(data),
|
||||
0xFF10..0xFF40 => self.apu.mmio_write(address, data),
|
||||
0xFF40 => self.gpu.update_lcdc(data),
|
||||
0xFF41 => self.gpu.update_lcd_status(data),
|
||||
0xFF42 => self.gpu.update_scy(data),
|
||||
0xFF43 => self.gpu.update_scx(data),
|
||||
0xFF45 => self.gpu.update_lyc(data),
|
||||
0xFF46 => {
|
||||
if data > 0xDF {
|
||||
panic!("dma transfer out of bounds: {data:#X}");
|
||||
}
|
||||
self.dma_addr = data;
|
||||
let mut addr: u16 = 0x0;
|
||||
addr.set_high(data);
|
||||
for l in 0x0..0xA0 {
|
||||
addr.set_low(l);
|
||||
self.gpu.oam.data[l as usize] = self.get(addr);
|
||||
}
|
||||
}
|
||||
IoAddress::Unused(_) => {}
|
||||
0xFF47 => self.gpu.update_bg_palette(data),
|
||||
0xFF48 => self.gpu.update_obj_palette_0(data),
|
||||
0xFF49 => self.gpu.update_obj_palette_1(data),
|
||||
0xFF4A => self.gpu.update_wy(data),
|
||||
0xFF4B => self.gpu.update_wx(data),
|
||||
0xFF03 | 0xFF08..0xFF0F | 0xFF44 => {
|
||||
// read-only addresses
|
||||
verbose_println!("BANNED write: {data:#X} to {address:#X}");
|
||||
}
|
||||
0x0..0xFF00 | 0xFF4C..=u16::MAX => panic!("passed wrong address to set_io"),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -402,6 +257,32 @@ where
|
|||
self.joypad.update_pressed_keys(latest_state)
|
||||
}
|
||||
|
||||
pub(super) fn cpu_ram_init(&mut self) {
|
||||
self.set(0xFF04, 0xAD);
|
||||
// self.set(0xFF10, 0x80);
|
||||
// self.set(0xFF11, 0xBF);
|
||||
// self.set(0xFF12, 0xF3);
|
||||
// self.set(0xFF14, 0xBF);
|
||||
// self.set(0xFF16, 0x3F);
|
||||
// self.set(0xFF19, 0xBF);
|
||||
// self.set(0xFF1A, 0x7F);
|
||||
// self.set(0xFF1B, 0xFF);
|
||||
// self.set(0xFF1C, 0x9F);
|
||||
// self.set(0xFF1E, 0xBF);
|
||||
// self.set(0xFF20, 0xFF);
|
||||
// self.set(0xFF23, 0xBF);
|
||||
// self.set(0xFF24, 0x77);
|
||||
// self.set(0xFF25, 0xF3);
|
||||
// self.set(0xFF26, 0xF1);
|
||||
self.set(0xFF40, 0x91);
|
||||
self.set(0xFF47, 0xFC);
|
||||
self.set(0xFF48, 0xFF);
|
||||
self.set(0xFF49, 0xFF);
|
||||
for i in 0xC000..0xE000 {
|
||||
self.set(i, if rand::random() { 0xFF } else { 0x00 });
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush_rom(&mut self) {
|
||||
self.rom.flush();
|
||||
}
|
||||
|
@ -413,31 +294,46 @@ where
|
|||
pub fn replace_output(&mut self, new: AudioOutput) {
|
||||
self.apu.replace_output(new);
|
||||
}
|
||||
|
||||
pub(crate) fn from_save_state(
|
||||
state: MemorySaveState<ColourFormat, R>,
|
||||
data: Vec<u8>,
|
||||
window: R,
|
||||
output: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
Self {
|
||||
bootrom: None,
|
||||
rom: Rom::from_save_state(state.rom, data, camera.clone()),
|
||||
ram: state.ram,
|
||||
cpu_ram: state.cpu_ram,
|
||||
interrupts: state.interrupts,
|
||||
ime: state.ime,
|
||||
ime_scheduled: state.ime_scheduled,
|
||||
dma_addr: state.dma_addr,
|
||||
joypad: state.joypad,
|
||||
gpu: Gpu::from_save_state(state.gpu, window, None),
|
||||
apu: Apu::from_save_state(state.apu, output),
|
||||
serial: Serial::from_save_state(state.serial, serial_target),
|
||||
timers: state.timers,
|
||||
camera,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Cpu<ColourFormat>
|
||||
impl<ColourFormat, R, C> Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub fn increment_timers(&mut self, machine_cycles: usize) {
|
||||
self.increment_timers_div_optional(machine_cycles, true);
|
||||
}
|
||||
pub fn increment_timers(&mut self, machine_cycles: u8) {
|
||||
let steps = (machine_cycles as usize) * 4;
|
||||
|
||||
pub fn increment_timers_div_optional(&mut self, machine_cycles: usize, div: bool) {
|
||||
let steps = machine_cycles * 4;
|
||||
let logical_steps = if self.memory.is_double_speed() {
|
||||
steps / 2
|
||||
} else {
|
||||
steps
|
||||
};
|
||||
self.cycle_count += steps;
|
||||
// self.memory.camera.lock().unwrap().tick(steps);
|
||||
|
||||
self.memory.oam_dma_tick(steps);
|
||||
|
||||
let timer_return = self
|
||||
.memory
|
||||
.timers
|
||||
.tick(steps, div, self.memory.is_double_speed());
|
||||
let timer_return = self.memory.timers.tick(steps);
|
||||
|
||||
for _ in 0..timer_return.num_apu_ticks {
|
||||
self.memory.apu.div_apu_tick();
|
||||
|
@ -447,35 +343,28 @@ where
|
|||
self.memory.interrupts.set_interrupt(Interrupt::Timer, true);
|
||||
}
|
||||
|
||||
self.memory
|
||||
.apu
|
||||
.tick(logical_steps, !(self.is_skipping || self.no_output));
|
||||
self.memory.apu.tick(steps);
|
||||
|
||||
let serial_interrupt = self.memory.serial.tick(steps, self.memory.ime);
|
||||
self.memory
|
||||
.interrupts
|
||||
.set_interrupt(Interrupt::Serial, serial_interrupt);
|
||||
|
||||
let gpu_interrupts = self
|
||||
.memory
|
||||
.gpu
|
||||
.tick(logical_steps, !(self.is_skipping || self.no_output));
|
||||
let gpu_interrupts = self.memory.gpu.tick(steps);
|
||||
|
||||
self.memory
|
||||
.interrupts
|
||||
.set_interrupt(Interrupt::Vblank, gpu_interrupts.vblank);
|
||||
self.memory
|
||||
.interrupts
|
||||
.set_interrupt(Interrupt::LcdStat, gpu_interrupts.lcd_stat);
|
||||
|
||||
if let Some(next_joypad_state) = self.next_joypad_state.take() {
|
||||
let joypad_interrupt = self.memory.update_pressed_keys(next_joypad_state);
|
||||
self.memory
|
||||
.interrupts
|
||||
.set_interrupt(Interrupt::Joypad, joypad_interrupt);
|
||||
}
|
||||
|
||||
if gpu_interrupts.vblank {
|
||||
self.memory
|
||||
.interrupts
|
||||
.set_interrupt(Interrupt::Vblank, true);
|
||||
}
|
||||
// if gpu_interrupts.vblank {
|
||||
// let latest_state = self.memory.gpu.window.latest_joypad_state();
|
||||
// let joypad_interrupt = self.memory.update_pressed_keys(latest_state);
|
||||
// self.memory
|
||||
// .interrupts
|
||||
// .set_interrupt(Interrupt::Joypad, joypad_interrupt);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
use std::{
|
||||
fmt::UpperHex,
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
use crate::error::AddressError;
|
||||
|
||||
pub(crate) use self::types::*;
|
||||
|
||||
mod types;
|
||||
|
||||
pub(crate) type VramAddress = BoundedAddress<0x8000, 0xA000>;
|
||||
pub(crate) type CartRamAddress = BoundedAddress<0xA000, 0xC000>;
|
||||
pub(crate) type WorkRamAddress = BoundedAddress<0xC000, 0xD000>;
|
||||
pub(crate) type BankedWorkRamAddress = BoundedAddress<0xD000, 0xE000>;
|
||||
pub(crate) type MirroredWorkRamAddress = BoundedAddress<0xE000, 0xF000>;
|
||||
pub(crate) type MirroredBankedWorkRamAddress = BoundedAddress<0xF000, 0xFE00>;
|
||||
pub(crate) type OamAddress = BoundedAddress<0xFE00, 0xFEA0>;
|
||||
pub(crate) type ProhibitedAddress = BoundedAddress<0xFEA0, 0xFF00>;
|
||||
pub(crate) type HramAddress = BoundedAddress<0xFF80, 0xFFFF>;
|
||||
pub(crate) type InterruptEnable = ();
|
||||
|
||||
pub(crate) type Bank0Address = BoundedAddress<0x0, 0x4000>;
|
||||
pub(crate) type MappedBankAddress = BoundedAddress<0x4000, 0x8000>;
|
||||
|
||||
pub(crate) type SerialAddress = BoundedAddress<0xFF01, 0xFF03>;
|
||||
pub(crate) type TimerAddress = BoundedAddress<0xFF04, 0xFF08>;
|
||||
pub(crate) type AudioAddress = BoundedAddress<0xFF10, 0xFF27>;
|
||||
pub(crate) type WaveRamAddress = BoundedAddress<0xFF30, 0xFF40>;
|
||||
pub(crate) type VideoAddress = BoundedAddress<0xFF40, 0xFF4C>;
|
||||
|
||||
pub(crate) type VramDmaAddress = BoundedAddress<0xFF51, 0xFF56>;
|
||||
pub(crate) type CgbPaletteAddress = BoundedAddress<0xFF68, 0xFF6C>;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum RomAddress {
|
||||
Bank0(Bank0Address),
|
||||
MappedBank(MappedBankAddress),
|
||||
}
|
||||
|
||||
impl UpperHex for RomAddress {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RomAddress::Bank0(a) => a.fmt(f),
|
||||
RomAddress::MappedBank(a) => a.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum IoAddress {
|
||||
Joypad,
|
||||
Serial(SerialAddress),
|
||||
Timer(TimerAddress),
|
||||
InterruptFlag,
|
||||
Audio(AudioAddress),
|
||||
WaveRam(WaveRamAddress),
|
||||
Video(VideoAddress),
|
||||
Cgb(CgbIoAddress),
|
||||
Unused(u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum CgbIoAddress {
|
||||
CompatMode,
|
||||
PrepareSpeed,
|
||||
VramBank,
|
||||
VramDma(VramDmaAddress),
|
||||
Infrared,
|
||||
Palette(CgbPaletteAddress),
|
||||
ObjPriority,
|
||||
WramBank,
|
||||
Pcm12,
|
||||
Pcm34,
|
||||
Unused(u16),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Address {
|
||||
Rom(RomAddress),
|
||||
Vram(VramAddress),
|
||||
CartRam(CartRamAddress),
|
||||
WorkRam(WorkRamAddress),
|
||||
BankedWorkRam(BankedWorkRamAddress),
|
||||
MirroredWorkRam(MirroredWorkRamAddress),
|
||||
MirroredBankedWorkRam(MirroredBankedWorkRamAddress),
|
||||
Oam(OamAddress),
|
||||
Prohibited(ProhibitedAddress),
|
||||
Io(IoAddress),
|
||||
Hram(HramAddress),
|
||||
InterruptEnable(InterruptEnable),
|
||||
}
|
||||
|
||||
impl From<u16> for Address {
|
||||
fn from(value: u16) -> Self {
|
||||
match value {
|
||||
0x0..0x8000 => Address::Rom(value.try_into().unwrap()),
|
||||
0x8000..0xA000 => Address::Vram(value.try_into().unwrap()),
|
||||
0xA000..0xC000 => Address::CartRam(value.try_into().unwrap()),
|
||||
0xC000..0xD000 => Address::WorkRam(value.try_into().unwrap()),
|
||||
0xD000..0xE000 => Address::BankedWorkRam(value.try_into().unwrap()),
|
||||
0xE000..0xF000 => Address::MirroredWorkRam(value.try_into().unwrap()),
|
||||
0xF000..0xFE00 => Address::MirroredBankedWorkRam(value.try_into().unwrap()),
|
||||
0xFE00..0xFEA0 => Address::Oam(value.try_into().unwrap()),
|
||||
0xFEA0..0xFF00 => Address::Prohibited(value.try_into().unwrap()),
|
||||
0xFF00..0xFF80 => Address::Io(value.try_into().unwrap()),
|
||||
0xFF80..0xFFFF => Address::Hram(value.try_into().unwrap()),
|
||||
0xFFFF => Address::InterruptEnable(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Address> for u16 {
|
||||
fn from(value: Address) -> Self {
|
||||
value.inner()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<RomAddress> for u16 {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_into(self) -> Result<RomAddress, Self::Error> {
|
||||
match self {
|
||||
0x0..0x4000 => Ok(RomAddress::Bank0(self.try_into().unwrap())),
|
||||
0x4000..0x8000 => Ok(RomAddress::MappedBank(self.try_into().unwrap())),
|
||||
_ => Err(AddressError::OutOfBounds),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<IoAddress> for u16 {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_into(self) -> Result<IoAddress, Self::Error> {
|
||||
match self {
|
||||
0xFF00 => Ok(IoAddress::Joypad),
|
||||
0xFF01..=0xFF02 => Ok(IoAddress::Serial(self.try_into().unwrap())),
|
||||
0xFF04..0xFF08 => Ok(IoAddress::Timer(self.try_into().unwrap())),
|
||||
0xFF0F => Ok(IoAddress::InterruptFlag),
|
||||
0xFF10..0xFF27 => Ok(IoAddress::Audio(self.try_into().unwrap())),
|
||||
0xFF30..0xFF40 => Ok(IoAddress::WaveRam(self.try_into().unwrap())),
|
||||
0xFF40..0xFF4C => Ok(IoAddress::Video(self.try_into().unwrap())),
|
||||
0xFF4C..0xFF78 => Ok(IoAddress::Cgb(self.try_into().unwrap())),
|
||||
0x0..0xFF00 | 0xFFFF => Err(AddressError::OutOfBounds),
|
||||
_ => Ok(IoAddress::Unused(self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<CgbIoAddress> for u16 {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_into(self) -> Result<CgbIoAddress, Self::Error> {
|
||||
match self {
|
||||
0xFF4C => Ok(CgbIoAddress::CompatMode),
|
||||
0xFF4D => Ok(CgbIoAddress::PrepareSpeed),
|
||||
0xFF4F => Ok(CgbIoAddress::VramBank),
|
||||
0xFF51..0xFF56 => Ok(CgbIoAddress::VramDma(self.try_into().unwrap())),
|
||||
0xFF56 => Ok(CgbIoAddress::Infrared),
|
||||
0xFF68..0xFF6C => Ok(CgbIoAddress::Palette(self.try_into().unwrap())),
|
||||
0xFF6C => Ok(CgbIoAddress::ObjPriority),
|
||||
0xFF70 => Ok(CgbIoAddress::WramBank),
|
||||
0xFF76 => Ok(CgbIoAddress::Pcm12),
|
||||
0xFF77 => Ok(CgbIoAddress::Pcm34),
|
||||
0x0..0xFF4C | 0xFF78..=0xFFFF => Err(AddressError::OutOfBounds),
|
||||
_ => Ok(CgbIoAddress::Unused(self)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressMarker for RomAddress {
|
||||
fn inner(&self) -> u16 {
|
||||
match self {
|
||||
RomAddress::Bank0(v) => v.inner(),
|
||||
RomAddress::MappedBank(v) => v.inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressMarker for IoAddress {
|
||||
fn inner(&self) -> u16 {
|
||||
match self {
|
||||
IoAddress::Joypad => 0xFF00,
|
||||
IoAddress::Serial(v) => v.inner(),
|
||||
IoAddress::Timer(v) => v.inner(),
|
||||
IoAddress::InterruptFlag => 0xFF0F,
|
||||
IoAddress::Audio(v) => v.inner(),
|
||||
IoAddress::WaveRam(v) => v.inner(),
|
||||
IoAddress::Video(v) => v.inner(),
|
||||
IoAddress::Unused(v) => *v,
|
||||
IoAddress::Cgb(v) => v.inner(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressMarker for CgbIoAddress {
|
||||
fn inner(&self) -> u16 {
|
||||
match self {
|
||||
CgbIoAddress::CompatMode => 0xFF4C,
|
||||
CgbIoAddress::PrepareSpeed => 0xFF4D,
|
||||
CgbIoAddress::VramBank => 0xFF4F,
|
||||
CgbIoAddress::VramDma(v) => v.inner(),
|
||||
CgbIoAddress::Infrared => 0xFF56,
|
||||
CgbIoAddress::Palette(v) => v.inner(),
|
||||
CgbIoAddress::ObjPriority => 0xFF6C,
|
||||
CgbIoAddress::WramBank => 0xFF70,
|
||||
CgbIoAddress::Pcm12 => 0xFF76,
|
||||
CgbIoAddress::Pcm34 => 0xFF77,
|
||||
CgbIoAddress::Unused(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AddressMarker for Address {
|
||||
fn inner(&self) -> u16 {
|
||||
match self {
|
||||
Address::Rom(v) => v.inner(),
|
||||
Address::Vram(v) => v.inner(),
|
||||
Address::CartRam(v) => v.inner(),
|
||||
Address::WorkRam(v) => v.inner(),
|
||||
Address::BankedWorkRam(v) => v.inner(),
|
||||
Address::MirroredWorkRam(v) => v.inner(),
|
||||
Address::MirroredBankedWorkRam(v) => v.inner(),
|
||||
Address::Oam(v) => v.inner(),
|
||||
Address::Prohibited(v) => v.inner(),
|
||||
Address::Io(v) => v.inner(),
|
||||
Address::Hram(v) => v.inner(),
|
||||
Address::InterruptEnable(_) => 0xFFFF,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Address {
|
||||
type Output = Address;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
self.inner().wrapping_add(rhs.inner()).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Address {
|
||||
type Output = Address;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
self.inner().wrapping_sub(rhs.inner()).into()
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
use std::{
|
||||
fmt::{Binary, Display, LowerHex, UpperHex},
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
use crate::error::AddressError;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct BoundedAddress<const MIN: u16, const MAX: u16>(u16);
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> std::fmt::Debug for BoundedAddress<MIN, MAX> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:#04X}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> Display for BoundedAddress<MIN, MAX> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> LowerHex for BoundedAddress<MIN, MAX> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
LowerHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> UpperHex for BoundedAddress<MIN, MAX> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
UpperHex::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> Binary for BoundedAddress<MIN, MAX> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Binary::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> Add<u16> for BoundedAddress<MIN, MAX> {
|
||||
type Output = Option<BoundedAddress<MIN, MAX>>;
|
||||
|
||||
fn add(self, rhs: u16) -> Self::Output {
|
||||
(self.0 + rhs).try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> Sub<u16> for BoundedAddress<MIN, MAX> {
|
||||
type Output = Option<BoundedAddress<MIN, MAX>>;
|
||||
|
||||
fn sub(self, rhs: u16) -> Self::Output {
|
||||
(self.0 - rhs).try_into().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> TryInto<BoundedAddress<MIN, MAX>> for u16 {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_into(self) -> Result<BoundedAddress<MIN, MAX>, Self::Error> {
|
||||
if self >= MIN && self < MAX {
|
||||
Ok(BoundedAddress(self))
|
||||
} else {
|
||||
Err(AddressError::OutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> BoundedAddress<MIN, MAX> {
|
||||
pub(crate) fn get_local(&self) -> u16 {
|
||||
self.0 - MIN
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> PartialEq for BoundedAddress<MIN, MAX> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0 == other.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> PartialOrd for BoundedAddress<MIN, MAX> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.0.partial_cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait AddressMarker {
|
||||
fn inner(&self) -> u16;
|
||||
}
|
||||
|
||||
impl<const MIN: u16, const MAX: u16> AddressMarker for BoundedAddress<MIN, MAX> {
|
||||
fn inner(&self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
}
|
|
@ -33,7 +33,7 @@ fn bool_to_shifted(input: bool, shift: u8) -> u8 {
|
|||
(if input { 1 } else { 0 }) << shift
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub enum Interrupt {
|
||||
Vblank,
|
||||
LcdStat,
|
||||
|
|
|
@ -4,12 +4,12 @@ use self::{
|
|||
};
|
||||
use crate::{
|
||||
connect::AudioOutput,
|
||||
processor::memory::addresses::{AddressMarker, AudioAddress, WaveRamAddress},
|
||||
processor::memory::Address,
|
||||
util::{get_bit, set_or_clear_bit},
|
||||
};
|
||||
use async_ringbuf::traits::{AsyncProducer, Observer};
|
||||
use futures::executor;
|
||||
use itertools::izip;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod channels;
|
||||
mod downsampler;
|
||||
|
@ -17,14 +17,14 @@ mod types;
|
|||
|
||||
impl DacSample {
|
||||
fn mixed(&self, mixer: &Mixer) -> [f32; 2] {
|
||||
let left = mixer.ch1.left.gate(self.one)
|
||||
+ mixer.ch2.left.gate(self.two)
|
||||
+ mixer.ch3.left.gate(self.three)
|
||||
+ mixer.ch4.left.gate(self.four);
|
||||
let right = mixer.ch1.right.gate(self.one)
|
||||
+ mixer.ch2.right.gate(self.two)
|
||||
+ mixer.ch3.right.gate(self.three)
|
||||
+ mixer.ch4.right.gate(self.four);
|
||||
let left = (self.one * mixer.ch1.left.scale())
|
||||
+ (self.two * mixer.ch2.left.scale())
|
||||
+ (self.three * mixer.ch3.left.scale())
|
||||
+ (self.four * mixer.ch4.left.scale());
|
||||
let right = (self.one * mixer.ch1.right.scale())
|
||||
+ (self.two * mixer.ch2.right.scale())
|
||||
+ (self.three * mixer.ch3.right.scale())
|
||||
+ (self.four * mixer.ch4.right.scale());
|
||||
[
|
||||
self.mix_channel(left, mixer.vol_left),
|
||||
self.mix_channel(right, mixer.vol_right),
|
||||
|
@ -42,10 +42,39 @@ pub struct Apu {
|
|||
vin: VinEnable,
|
||||
mixer: Mixer,
|
||||
div_apu: u8,
|
||||
buffer: Vec<DacSample>,
|
||||
out_buffer: Vec<[f32; 2]>,
|
||||
converter: Downsampler,
|
||||
output: AudioOutput,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ApuSaveState {
|
||||
apu_enable: bool,
|
||||
channels: Channels,
|
||||
vin: VinEnable,
|
||||
mixer: Mixer,
|
||||
div_apu: u8,
|
||||
buffer: Vec<DacSample>,
|
||||
out_buffer: Vec<[f32; 2]>,
|
||||
}
|
||||
|
||||
impl ApuSaveState {
|
||||
pub fn create(apu: &Apu) -> Self {
|
||||
Self {
|
||||
apu_enable: apu.apu_enable,
|
||||
channels: apu.channels,
|
||||
vin: apu.vin,
|
||||
mixer: apu.mixer,
|
||||
div_apu: apu.div_apu,
|
||||
buffer: apu.buffer.clone(),
|
||||
out_buffer: apu.out_buffer.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CYCLES_PER_FRAME: usize = 70224;
|
||||
|
||||
impl Apu {
|
||||
pub fn new(output: AudioOutput) -> Self {
|
||||
Self {
|
||||
|
@ -54,6 +83,22 @@ impl Apu {
|
|||
vin: VinEnable::default(),
|
||||
mixer: Mixer::default(),
|
||||
div_apu: 0,
|
||||
buffer: vec![],
|
||||
out_buffer: vec![],
|
||||
converter: Downsampler::new(output.sample_rate, output.downsample_type),
|
||||
output,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: ApuSaveState, output: AudioOutput) -> Self {
|
||||
Self {
|
||||
apu_enable: state.apu_enable,
|
||||
channels: state.channels,
|
||||
vin: state.vin,
|
||||
mixer: state.mixer,
|
||||
div_apu: state.div_apu,
|
||||
buffer: state.buffer,
|
||||
out_buffer: state.out_buffer,
|
||||
converter: Downsampler::new(output.sample_rate, output.downsample_type),
|
||||
output,
|
||||
}
|
||||
|
@ -85,49 +130,73 @@ impl Apu {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, steps: usize, output: bool) {
|
||||
if output {
|
||||
for s in izip!(
|
||||
self.channels.one.tick(steps),
|
||||
self.channels.two.tick(steps),
|
||||
self.channels.three.tick(steps),
|
||||
self.channels.four.tick(steps)
|
||||
pub fn tick(&mut self, steps: usize) {
|
||||
self.buffer.append(
|
||||
&mut izip!(
|
||||
self.channels.one.tick(steps).into_iter(),
|
||||
self.channels.two.tick(steps).into_iter(),
|
||||
self.channels.three.tick(steps).into_iter(),
|
||||
self.channels.four.tick(steps).into_iter()
|
||||
)
|
||||
.map(|(one, two, three, four)| DacSample {
|
||||
one,
|
||||
two,
|
||||
three,
|
||||
four,
|
||||
}) {
|
||||
if let Some(next) = self.converter.push(s.mixed(&self.mixer)) {
|
||||
executor::block_on(self.output.send_rb.push(next)).unwrap();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.channels.one.tick(steps);
|
||||
self.channels.two.tick(steps);
|
||||
self.channels.three.tick(steps);
|
||||
self.channels.four.tick(steps);
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
if self.buffer.len() >= CYCLES_PER_FRAME {
|
||||
println!("finished 1 frame of audio... pushing...");
|
||||
self.next_audio();
|
||||
} else if !self.out_buffer.is_empty() {
|
||||
println!("pushing remainder...");
|
||||
self.push_audio();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_pcm_1_2(&self) -> u8 {
|
||||
(self.channels.one.last & 0xF) | ((self.channels.two.last & 0xF0) << 4)
|
||||
fn next_audio(&mut self) {
|
||||
self.out_buffer.append(
|
||||
&mut self.converter.process(
|
||||
self.buffer
|
||||
.drain(..)
|
||||
.map(|v| v.mixed(&self.mixer))
|
||||
.collect::<Vec<[f32; 2]>>(),
|
||||
),
|
||||
);
|
||||
|
||||
self.push_audio();
|
||||
}
|
||||
|
||||
pub(crate) fn get_pcm_3_4(&self) -> u8 {
|
||||
(self.channels.one.last & 0xF) | ((self.channels.two.last & 0xF0) << 4)
|
||||
fn push_audio(&mut self) {
|
||||
let length = if self.output.wait_for_output {
|
||||
self.out_buffer.len()
|
||||
} else {
|
||||
self.out_buffer.len().min(self.output.send_rb.free_len())
|
||||
};
|
||||
|
||||
if length > 0 {
|
||||
executor::block_on(
|
||||
self.output
|
||||
.send_rb
|
||||
.push_slice(&self.out_buffer.drain(..length).collect::<Vec<[f32; 2]>>()),
|
||||
)
|
||||
.expect("APU: error sending audio to output ringbuffer");
|
||||
} else {
|
||||
println!("buffer already full - skipped filling");
|
||||
}
|
||||
println!("finished pushing audio");
|
||||
}
|
||||
|
||||
pub fn is_buffer_full(&self) -> bool {
|
||||
self.output.send_rb.is_full()
|
||||
}
|
||||
|
||||
pub(crate) fn get_register(&self, addr: AudioAddress) -> u8 {
|
||||
pub fn get_register(&self, addr: Address) -> u8 {
|
||||
if self.apu_enable {
|
||||
self.make_register(addr)
|
||||
} else {
|
||||
match addr.inner() {
|
||||
match addr {
|
||||
0xFF26 | 0xFF11 | 0xFF16 | 0xFF1B | 0xFF20 | 0xFF30..0xFF40 => {
|
||||
self.make_register(addr)
|
||||
}
|
||||
|
@ -136,12 +205,8 @@ impl Apu {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_wave_ram_register(&self, addr: WaveRamAddress) -> u8 {
|
||||
self.channels.three.wave_ram.data[addr.get_local() as usize]
|
||||
}
|
||||
|
||||
fn make_register(&self, addr: AudioAddress) -> u8 {
|
||||
match addr.inner() {
|
||||
fn make_register(&self, addr: Address) -> u8 {
|
||||
match addr {
|
||||
0xFF10 => self.channels.one.get_sweep_register(),
|
||||
0xFF11 => self.channels.one.get_length_timer_and_duty_cycle(),
|
||||
0xFF12 => self.channels.one.get_volume_and_envelope(),
|
||||
|
@ -192,19 +257,20 @@ impl Apu {
|
|||
// write-only registers
|
||||
0xFF13 | 0xFF18 | 0xFF1B | 0xFF1D | 0xFF20 => 0xFF,
|
||||
// not registers
|
||||
0xFF15 | 0xFF1F => 0xFF,
|
||||
0x0..0xFF10 | 0xFF27..=0xFFFF => unreachable!(),
|
||||
0xFF15 | 0xFF1F | 0xFF27..0xFF30 => 0xFF,
|
||||
// wave ram
|
||||
0xFF30..0xFF40 => self.channels.three.wave_ram.data[(addr - 0xFF30) as usize],
|
||||
0x0..0xFF10 | 0xFF40..=0xFFFF => panic!("non-apu addr in apu"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mmio_write(&mut self, addr: AudioAddress, data: u8) {
|
||||
match addr.inner() {
|
||||
pub fn mmio_write(&mut self, addr: Address, data: u8) {
|
||||
match addr {
|
||||
0xFF10 => self.channels.one.update_sweep(data),
|
||||
0xFF11 => self.channels.one.update_length_timer_and_duty_cycle(data),
|
||||
0xFF12 => self.channels.one.update_volume_and_envelope(data),
|
||||
0xFF13 => self.channels.one.update_wavelength_low(data),
|
||||
0xFF14 => self.channels.one.update_wavelength_high_and_control(data),
|
||||
0xFF15 => {}
|
||||
0xFF16 => self.channels.two.update_length_timer_and_duty_cycle(data),
|
||||
0xFF17 => self.channels.two.update_volume_and_envelope(data),
|
||||
0xFF18 => self.channels.two.update_wavelength_low(data),
|
||||
|
@ -214,7 +280,6 @@ impl Apu {
|
|||
0xFF1C => self.channels.three.update_volume(data),
|
||||
0xFF1D => self.channels.three.update_wavelength_low(data),
|
||||
0xFF1E => self.channels.three.update_wavelength_high_and_control(data),
|
||||
0xFF1F => {}
|
||||
0xFF20 => self.channels.four.update_length_timer(data),
|
||||
0xFF21 => self.channels.four.update_volume_and_envelope(data),
|
||||
0xFF22 => self.channels.four.update_frequency_and_randomness(data),
|
||||
|
@ -238,19 +303,17 @@ impl Apu {
|
|||
0xFF26 => {
|
||||
if !self.apu_enable {
|
||||
for i in 0xFF10..0xFF20 {
|
||||
self.mmio_write(i.try_into().unwrap(), 0x0);
|
||||
self.mmio_write(i, 0x0);
|
||||
}
|
||||
for i in 0xFF21..0xFF25 {
|
||||
self.mmio_write(i.try_into().unwrap(), 0x0);
|
||||
self.mmio_write(i, 0x0);
|
||||
}
|
||||
}
|
||||
self.apu_enable = (1 << 7) == (data & 0b10000000);
|
||||
}
|
||||
0x0..0xFF10 | 0xFF27..=0xFFFF => unreachable!(),
|
||||
0xFF30..0xFF40 => self.channels.three.update_wave_ram(addr, data),
|
||||
0xFF15 | 0xFF1F | 0xFF27..0xFF30 => {}
|
||||
0x0..0xFF10 | 0xFF40..=0xFFFF => panic!("non-apu addr in apu"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn mmio_write_wave_ram(&mut self, addr: WaveRamAddress, data: u8) {
|
||||
self.channels.three.update_wave_ram(addr, data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
processor::memory::addresses::WaveRamAddress,
|
||||
processor::memory::Address,
|
||||
util::{get_bit, set_or_clear_bit, Nibbles},
|
||||
};
|
||||
|
||||
|
@ -127,7 +127,6 @@ impl DutyCycle {
|
|||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct PwmChannel {
|
||||
pub(super) enabled: bool,
|
||||
pub(super) last: u8,
|
||||
sweep: Sweep,
|
||||
duty_cycle: DutyCycle,
|
||||
length_enable: bool,
|
||||
|
@ -144,7 +143,6 @@ impl PwmChannel {
|
|||
let wavelength = 0x7FF;
|
||||
Self {
|
||||
enabled,
|
||||
last: 0,
|
||||
sweep: Sweep::default(),
|
||||
duty_cycle: DutyCycle::Fifty,
|
||||
length_enable: false,
|
||||
|
@ -183,13 +181,11 @@ impl PwmChannel {
|
|||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.last = 0;
|
||||
vec![0.; steps]
|
||||
}
|
||||
}
|
||||
|
||||
fn dac(&mut self, digital: u8) -> f32 {
|
||||
self.last = digital;
|
||||
fn dac(&self, digital: u8) -> f32 {
|
||||
(((digital as f32) * (-2.)) + 1.) * ((self.envelope.current_volume as f32) / 15.)
|
||||
}
|
||||
|
||||
|
@ -343,7 +339,6 @@ impl WaveRam {
|
|||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct WaveChannel {
|
||||
pub(super) enabled: bool,
|
||||
pub(super) last: u8,
|
||||
dac_enabled: bool,
|
||||
length_enable: bool,
|
||||
length_timer: u8,
|
||||
|
@ -359,7 +354,6 @@ impl WaveChannel {
|
|||
let wavelength = 0x7FF;
|
||||
Self {
|
||||
enabled,
|
||||
last: 0,
|
||||
dac_enabled: false,
|
||||
length_enable: false,
|
||||
length_timer: 0,
|
||||
|
@ -396,17 +390,14 @@ impl WaveChannel {
|
|||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.last = 0;
|
||||
vec![0.; steps]
|
||||
}
|
||||
}
|
||||
|
||||
fn dac(&mut self, digital: u8) -> f32 {
|
||||
fn dac(&self, digital: u8) -> f32 {
|
||||
if self.dac_enabled && self.volume != ShiftVolumePercent::Zero {
|
||||
self.last = digital;
|
||||
((((digital >> self.volume.as_shift_amount()) as f32) * (-2.)) + 1.) / 15.
|
||||
} else {
|
||||
self.last = 0;
|
||||
0.
|
||||
}
|
||||
}
|
||||
|
@ -470,8 +461,8 @@ impl WaveChannel {
|
|||
set_or_clear_bit(0xFF, 6, self.length_enable)
|
||||
}
|
||||
|
||||
pub(super) fn update_wave_ram(&mut self, addr: WaveRamAddress, data: u8) {
|
||||
let real_addr = addr.get_local() as usize;
|
||||
pub(super) fn update_wave_ram(&mut self, addr: Address, data: u8) {
|
||||
let real_addr = (addr - 0xFF30) as usize;
|
||||
if real_addr >= self.wave_ram.data.len() {
|
||||
panic!("sent the wrong address to update_wave_ram");
|
||||
}
|
||||
|
@ -506,9 +497,8 @@ struct Lfsr {
|
|||
|
||||
impl Lfsr {
|
||||
fn update(&mut self) {
|
||||
self.interval = (1_u16 << (self.clock_shift as u16))
|
||||
.wrapping_mul(1 + (2 * self.clock_divider as u16))
|
||||
.wrapping_mul(8);
|
||||
self.interval =
|
||||
(1 << (self.clock_shift as u16)) * (1 + (2 * self.clock_divider as u16)) * 8;
|
||||
}
|
||||
|
||||
fn tick(&mut self) {
|
||||
|
@ -555,7 +545,6 @@ impl Default for Lfsr {
|
|||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct NoiseChannel {
|
||||
pub(super) enabled: bool,
|
||||
pub(super) last: u8,
|
||||
length_enable: bool,
|
||||
length_timer: u8,
|
||||
envelope: Envelope,
|
||||
|
@ -567,7 +556,6 @@ impl NoiseChannel {
|
|||
pub(super) fn new(enabled: bool) -> Self {
|
||||
Self {
|
||||
enabled,
|
||||
last: 0,
|
||||
length_enable: false,
|
||||
length_timer: 0,
|
||||
envelope: Envelope::default(),
|
||||
|
@ -594,13 +582,11 @@ impl NoiseChannel {
|
|||
})
|
||||
.collect()
|
||||
} else {
|
||||
self.last = 0;
|
||||
vec![0.; steps]
|
||||
}
|
||||
}
|
||||
|
||||
fn dac(&mut self, digital: u8) -> f32 {
|
||||
self.last = digital;
|
||||
fn dac(&self, digital: u8) -> f32 {
|
||||
(((digital as f32) * (-2.)) + 1.) * ((self.envelope.current_volume as f32) / 15.)
|
||||
}
|
||||
|
||||
|
|
|
@ -55,20 +55,22 @@ impl Downsampler {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn push(&mut self, signal: [f32; 2]) -> Option<[f32; 2]> {
|
||||
self.time_accum += 1.;
|
||||
if let Some(ref mut averager) = self.average {
|
||||
averager.push(&signal);
|
||||
}
|
||||
if self.time_accum >= self.ratio {
|
||||
self.time_accum -= self.ratio;
|
||||
Some(if let Some(ref mut averager) = self.average {
|
||||
averager.finish()
|
||||
} else {
|
||||
signal
|
||||
})
|
||||
} else {
|
||||
None
|
||||
pub fn process(&mut self, signal: Vec<[f32; 2]>) -> Vec<[f32; 2]> {
|
||||
let mut output = vec![];
|
||||
for ref val in signal {
|
||||
self.time_accum += 1.;
|
||||
if let Some(ref mut averager) = self.average {
|
||||
averager.push(val);
|
||||
}
|
||||
if self.time_accum >= self.ratio {
|
||||
self.time_accum -= self.ratio;
|
||||
output.push(if let Some(ref mut averager) = self.average {
|
||||
averager.finish()
|
||||
} else {
|
||||
*val
|
||||
});
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,10 +34,10 @@ pub(super) enum Volume {
|
|||
}
|
||||
|
||||
impl Volume {
|
||||
pub(super) fn gate(&self, val: f32) -> f32 {
|
||||
pub(super) fn scale(&self) -> f32 {
|
||||
match self {
|
||||
Volume::Muted => 0.,
|
||||
Volume::Enabled => val,
|
||||
Volume::Enabled => 1.,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
use crate::processor::memory::{mmio::gpu::Colour, Memory};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Copy)]
|
||||
pub(crate) struct DoubleSpeed {
|
||||
prepared: bool,
|
||||
current: bool,
|
||||
}
|
||||
|
||||
impl DoubleSpeed {
|
||||
pub(crate) fn get(&self) -> u8 {
|
||||
0b01111110 | if self.current { 0b1 << 7 } else { 0 } | if self.prepared { 0b1 } else { 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, data: u8) {
|
||||
self.prepared = data & 0b1 == 0b1;
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Memory<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(crate) fn is_double_speed(&self) -> bool {
|
||||
self.cgb_peripherals
|
||||
.as_ref()
|
||||
.map_or(false, |v| v.double_speed.current)
|
||||
}
|
||||
|
||||
pub(crate) fn try_switch_speed(&mut self) -> bool {
|
||||
if let Some(cgb_peripherals) = &mut self.cgb_peripherals {
|
||||
if cgb_peripherals.double_speed.prepared && !cgb_peripherals.double_speed.current {
|
||||
cgb_peripherals.double_speed.current = true;
|
||||
cgb_peripherals.double_speed.prepared = false;
|
||||
return true;
|
||||
} else if cgb_peripherals.double_speed.prepared && cgb_peripherals.double_speed.current
|
||||
{
|
||||
cgb_peripherals.double_speed.current = false;
|
||||
cgb_peripherals.double_speed.prepared = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::util::get_bit;
|
||||
|
||||
#[derive(Default, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Infrared {
|
||||
led: bool,
|
||||
read_enable: bool,
|
||||
}
|
||||
|
||||
impl Infrared {
|
||||
pub(crate) fn get(&self) -> u8 {
|
||||
0b111110 | if self.led { 1 } else { 0 } | if self.read_enable { 0b11 << 6 } else { 0 }
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, data: u8) {
|
||||
self.led = get_bit(data, 0);
|
||||
self.read_enable = get_bit(data, 6) && get_bit(data, 7);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
mod double_speed;
|
||||
mod ir;
|
||||
mod vram_dma;
|
||||
|
||||
pub(crate) use double_speed::DoubleSpeed;
|
||||
pub(crate) use ir::Infrared;
|
||||
pub(crate) use vram_dma::VramDma;
|
|
@ -1,139 +0,0 @@
|
|||
use crate::{
|
||||
processor::{
|
||||
memory::{
|
||||
addresses::{AddressMarker, VramDmaAddress},
|
||||
mmio::gpu::{Colour, DrawMode},
|
||||
Memory,
|
||||
},
|
||||
SplitRegister,
|
||||
},
|
||||
util::get_bit,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct VramDma {
|
||||
source: u16,
|
||||
destination: u16,
|
||||
mode: DmaMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
|
||||
enum DmaMode {
|
||||
Halt(u8),
|
||||
Hblank(u8, u16),
|
||||
Waiting,
|
||||
}
|
||||
|
||||
impl DmaMode {
|
||||
fn get_byte(&self) -> u8 {
|
||||
match self {
|
||||
DmaMode::Halt(_) => unreachable!(),
|
||||
DmaMode::Hblank(v, _) => *v,
|
||||
DmaMode::Waiting => 0xFF,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DmaMode {
|
||||
fn default() -> Self {
|
||||
Self::Waiting
|
||||
}
|
||||
}
|
||||
|
||||
impl VramDma {
|
||||
pub(crate) fn get_register(&self, address: VramDmaAddress) -> u8 {
|
||||
match address.inner() {
|
||||
0xFF51 => self.source.get_high(),
|
||||
0xFF52 => self.source.get_low(),
|
||||
0xFF53 => self.destination.get_high(),
|
||||
0xFF54 => self.destination.get_low(),
|
||||
0xFF55 => self.mode.get_byte(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_registers(&mut self) {
|
||||
self.source = 0xFFF0;
|
||||
self.destination = 0x9FF0;
|
||||
}
|
||||
|
||||
pub(crate) fn set_register(&mut self, address: VramDmaAddress, data: u8) {
|
||||
match address.inner() {
|
||||
0xFF51 => self.source.set_high(data),
|
||||
0xFF52 => self.source.set_low(data & 0xF0),
|
||||
0xFF53 => self.destination.set_high((data & 0x1F) | 0x80),
|
||||
0xFF54 => self.destination.set_low(data & 0xF0),
|
||||
0xFF55 => {
|
||||
let num = data & !(0b1 << 7);
|
||||
self.mode = if get_bit(data, 7) {
|
||||
DmaMode::Hblank(num, 0)
|
||||
} else if self.mode == DmaMode::Waiting {
|
||||
DmaMode::Halt(num)
|
||||
} else {
|
||||
DmaMode::Waiting
|
||||
};
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Memory<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(crate) fn vram_dma_tick(&mut self) -> usize {
|
||||
let mut copy = None;
|
||||
let returning = if let Some(cgb_peripherals) = &mut self.cgb_peripherals {
|
||||
match cgb_peripherals.vram_dma.mode {
|
||||
DmaMode::Halt(l) => {
|
||||
let length = 16 * ((l as u16) + 1);
|
||||
copy = Some((
|
||||
cgb_peripherals.vram_dma.source,
|
||||
cgb_peripherals.vram_dma.destination,
|
||||
length,
|
||||
));
|
||||
cgb_peripherals.vram_dma.mode = DmaMode::Waiting;
|
||||
cgb_peripherals.vram_dma.reset_registers();
|
||||
((l as usize) + 1) * 8
|
||||
}
|
||||
DmaMode::Hblank(l, ref mut progress) => {
|
||||
if self.gpu.get_mode() == DrawMode::HBlank {
|
||||
let length = 16;
|
||||
copy = Some((
|
||||
cgb_peripherals.vram_dma.source,
|
||||
cgb_peripherals.vram_dma.destination,
|
||||
length,
|
||||
));
|
||||
cgb_peripherals.vram_dma.source += length;
|
||||
cgb_peripherals.vram_dma.destination += length;
|
||||
*progress += 1;
|
||||
if *progress > (l as u16) {
|
||||
cgb_peripherals.vram_dma.mode = DmaMode::Waiting;
|
||||
cgb_peripherals.vram_dma.reset_registers();
|
||||
}
|
||||
8
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
DmaMode::Waiting => 0,
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if let Some((source, dest, length)) = copy {
|
||||
for i in 0..length {
|
||||
if let Some(s) = source.checked_add(i) {
|
||||
self.set(dest + i, self.get(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.is_double_speed() {
|
||||
returning * 2
|
||||
} else {
|
||||
returning
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +1,44 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub(crate) use self::types::DrawMode;
|
||||
use self::{
|
||||
cgb::CgbData,
|
||||
layer_window::LayerWindow,
|
||||
tile_window::TileWindow,
|
||||
types::{
|
||||
rgb_from_bytes, BgAttributes, ColourInner, GpuInterrupts, Lcdc, Oam, ObjPalette, ObjSize,
|
||||
Object, ObjectFlags, Palette, Stat, TiledataArea, TilemapArea, Vram, VramBank,
|
||||
DrawMode, GpuInterrupts, Lcdc, Oam, ObjPalette, ObjSize, Object, ObjectFlags, Palette,
|
||||
Stat, TiledataArea, TilemapArea, Vram,
|
||||
},
|
||||
};
|
||||
use crate::{
|
||||
connect::RendererMessage,
|
||||
processor::memory::addresses::{OamAddress, VramAddress},
|
||||
connect::Renderer,
|
||||
processor::SplitRegister,
|
||||
util::{clear_bit, get_bit},
|
||||
HEIGHT, WIDTH,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use types::Colour;
|
||||
|
||||
mod addresses;
|
||||
mod cgb;
|
||||
mod layer_window;
|
||||
mod tile_window;
|
||||
mod types;
|
||||
|
||||
const TILE_WINDOW_WIDTH: usize = 16 * 8;
|
||||
const TILE_WINDOW_HEIGHT: usize = 24 * 8;
|
||||
|
||||
type Buffer<T, const SIZE: usize> = Box<[T; SIZE]>;
|
||||
|
||||
trait NewBuffer {
|
||||
type BufferType;
|
||||
fn new_buffer(default: Self::BufferType) -> Self;
|
||||
}
|
||||
|
||||
impl<T, const SIZE: usize> NewBuffer for Buffer<T, SIZE>
|
||||
pub struct Gpu<ColourFormat, R>
|
||||
where
|
||||
T: Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
type BufferType = T;
|
||||
|
||||
fn new_buffer(default: Self::BufferType) -> Self {
|
||||
let mut v: Vec<T> = Vec::new();
|
||||
v.resize(SIZE, default);
|
||||
let temp = v.into_boxed_slice();
|
||||
unsafe { Box::from_raw(Box::into_raw(temp) as *mut [T; SIZE]) }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Gpu<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub buffer: Buffer<ColourFormat, { WIDTH * HEIGHT }>,
|
||||
pub buffer: Vec<ColourFormat>,
|
||||
pub vram: Vram,
|
||||
pub oam: Oam,
|
||||
pub window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
is_bg_zero: [bool; WIDTH],
|
||||
is_bg_priority: [bool; WIDTH],
|
||||
pub window: R,
|
||||
is_bg_zero: Vec<bool>,
|
||||
lcdc: Lcdc,
|
||||
stat: Stat,
|
||||
mode_clock: usize,
|
||||
scanline: u8,
|
||||
lyc: u8,
|
||||
tile_window: Option<TileWindow<ColourFormat>>,
|
||||
layer_window: Option<LayerWindow<ColourFormat>>,
|
||||
tile_window: Option<TileWindow<ColourFormat, R>>,
|
||||
window_lc: u8,
|
||||
has_window_been_enabled: bool,
|
||||
bg_palette: Palette,
|
||||
|
@ -75,40 +49,94 @@ where
|
|||
wx: u8,
|
||||
wy: u8,
|
||||
prev_stat: bool,
|
||||
cgb_data: Option<CgbData>,
|
||||
}
|
||||
|
||||
impl<ColourFormat> Gpu<ColourFormat>
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GpuSaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn new(
|
||||
cgb: bool,
|
||||
window: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
tile_window_renderer: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
layer_window_renderer: Option<Sender<RendererMessage<ColourFormat>>>,
|
||||
) -> Self {
|
||||
let tile_window = tile_window_renderer
|
||||
.map(|tile_window_renderer| TileWindow::new(tile_window_renderer, cgb));
|
||||
buffer: Vec<ColourFormat>,
|
||||
vram: Vram,
|
||||
oam: Oam,
|
||||
is_bg_zero: Vec<bool>,
|
||||
lcdc: Lcdc,
|
||||
stat: Stat,
|
||||
mode_clock: usize,
|
||||
scanline: u8,
|
||||
lyc: u8,
|
||||
window_lc: u8,
|
||||
has_window_been_enabled: bool,
|
||||
bg_palette: Palette,
|
||||
obj_palette_0: Palette,
|
||||
obj_palette_1: Palette,
|
||||
scx: u8,
|
||||
scy: u8,
|
||||
wx: u8,
|
||||
wy: u8,
|
||||
prev_stat: bool,
|
||||
#[serde(skip)]
|
||||
spooky: PhantomData<R>,
|
||||
}
|
||||
|
||||
let buffer = Buffer::new_buffer(get_blank_colour(cgb));
|
||||
impl<ColourFormat, R> GpuSaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn create(gpu: &Gpu<ColourFormat, R>) -> Self {
|
||||
Self {
|
||||
buffer: gpu.buffer.clone(),
|
||||
vram: gpu.vram,
|
||||
oam: gpu.oam,
|
||||
is_bg_zero: gpu.is_bg_zero.clone(),
|
||||
lcdc: gpu.lcdc,
|
||||
stat: gpu.stat,
|
||||
mode_clock: gpu.mode_clock,
|
||||
scanline: gpu.scanline,
|
||||
lyc: gpu.lyc,
|
||||
window_lc: gpu.window_lc,
|
||||
has_window_been_enabled: gpu.has_window_been_enabled,
|
||||
bg_palette: gpu.bg_palette,
|
||||
obj_palette_0: gpu.obj_palette_0,
|
||||
obj_palette_1: gpu.obj_palette_1,
|
||||
scx: gpu.scx,
|
||||
scy: gpu.scy,
|
||||
wx: gpu.wx,
|
||||
wy: gpu.wy,
|
||||
prev_stat: gpu.prev_stat,
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let layer_window = layer_window_renderer.map(|v| LayerWindow::new(v, cgb));
|
||||
impl<ColourFormat, R> Gpu<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn new(window: R, tile_window_renderer: Option<R>) -> Self {
|
||||
let tile_window = if let Some(mut tile_window_renderer) = tile_window_renderer {
|
||||
tile_window_renderer.prepare(TILE_WINDOW_WIDTH, TILE_WINDOW_HEIGHT);
|
||||
Some(TileWindow::new(tile_window_renderer))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let buffer = vec![Colour::Error.into(); WIDTH * HEIGHT];
|
||||
|
||||
Self {
|
||||
buffer,
|
||||
vram: Vram::new(cgb),
|
||||
vram: Vram::default(),
|
||||
oam: Oam::default(),
|
||||
window,
|
||||
is_bg_zero: [true; WIDTH],
|
||||
is_bg_priority: [false; WIDTH],
|
||||
is_bg_zero: vec![true; WIDTH],
|
||||
lcdc: Lcdc::default(),
|
||||
stat: Stat::default(),
|
||||
mode_clock: 0,
|
||||
scanline: 0,
|
||||
lyc: 0xFF,
|
||||
tile_window,
|
||||
layer_window,
|
||||
window_lc: 0,
|
||||
has_window_been_enabled: false,
|
||||
bg_palette: Palette::from_byte(0xFC),
|
||||
|
@ -119,35 +147,58 @@ where
|
|||
wx: 0,
|
||||
wy: 0,
|
||||
prev_stat: false,
|
||||
cgb_data: if cgb { Some(CgbData::default()) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tile_window(&mut self, window: Sender<RendererMessage<ColourFormat>>) {
|
||||
self.tile_window = Some(TileWindow::new(window, self.cgb_data.is_some()));
|
||||
pub fn from_save_state(
|
||||
state: GpuSaveState<ColourFormat, R>,
|
||||
window: R,
|
||||
tile_window_renderer: Option<R>,
|
||||
) -> Self {
|
||||
let tile_window = if let Some(mut tile_window_renderer) = tile_window_renderer {
|
||||
tile_window_renderer.prepare(TILE_WINDOW_WIDTH, TILE_WINDOW_HEIGHT);
|
||||
Some(TileWindow::new(tile_window_renderer))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
buffer: state.buffer,
|
||||
vram: state.vram,
|
||||
oam: state.oam,
|
||||
window,
|
||||
is_bg_zero: state.is_bg_zero,
|
||||
lcdc: state.lcdc,
|
||||
stat: state.stat,
|
||||
mode_clock: state.mode_clock,
|
||||
scanline: state.scanline,
|
||||
lyc: state.lyc,
|
||||
tile_window,
|
||||
window_lc: state.window_lc,
|
||||
has_window_been_enabled: state.has_window_been_enabled,
|
||||
bg_palette: state.bg_palette,
|
||||
obj_palette_0: state.obj_palette_0,
|
||||
obj_palette_1: state.obj_palette_1,
|
||||
scx: state.scx,
|
||||
scy: state.scy,
|
||||
wx: state.wx,
|
||||
wy: state.wy,
|
||||
prev_stat: state.prev_stat,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_layer_window(&mut self, window: Sender<RendererMessage<ColourFormat>>) {
|
||||
self.layer_window = Some(LayerWindow::new(window, self.cgb_data.is_some()));
|
||||
}
|
||||
|
||||
pub(crate) fn get_mode(&self) -> DrawMode {
|
||||
self.stat.mode
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, steps: usize, output: bool) -> GpuInterrupts {
|
||||
pub fn tick(&mut self, steps: usize) -> GpuInterrupts {
|
||||
let mut interrupts = GpuInterrupts::default();
|
||||
if self.lcdc.enable {
|
||||
self.mode_clock += steps;
|
||||
match self.stat.mode {
|
||||
DrawMode::HBlank => {
|
||||
// mode 0: hblank
|
||||
const HBLANK_MODE_CLOCK: usize = 204;
|
||||
if self.mode_clock >= HBLANK_MODE_CLOCK {
|
||||
self.mode_clock -= HBLANK_MODE_CLOCK;
|
||||
if self.mode_clock >= 204 {
|
||||
self.mode_clock = 0;
|
||||
self.scanline += 1;
|
||||
if self.scanline == 144 {
|
||||
self.enter_vblank(output);
|
||||
if self.scanline == 143 {
|
||||
self.enter_vblank();
|
||||
interrupts.vblank = true;
|
||||
} else {
|
||||
self.stat.mode = DrawMode::Mode2;
|
||||
|
@ -156,9 +207,8 @@ where
|
|||
}
|
||||
DrawMode::VBlank => {
|
||||
// mode 1: vblank
|
||||
const VBLANK_MODE_CLOCK: usize = 456;
|
||||
if self.mode_clock >= VBLANK_MODE_CLOCK {
|
||||
self.mode_clock -= VBLANK_MODE_CLOCK;
|
||||
if self.mode_clock >= 456 {
|
||||
self.mode_clock = 0;
|
||||
self.scanline += 1;
|
||||
if self.scanline == 153 {
|
||||
self.exit_vblank();
|
||||
|
@ -166,20 +216,18 @@ where
|
|||
}
|
||||
}
|
||||
DrawMode::Mode2 => {
|
||||
const MODE2_MODE_CLOCK: usize = 80;
|
||||
// search oam for sprites on this line
|
||||
// we dont really have to emulate this
|
||||
if self.mode_clock >= MODE2_MODE_CLOCK {
|
||||
self.mode_clock -= MODE2_MODE_CLOCK;
|
||||
if self.mode_clock >= 80 {
|
||||
self.mode_clock = 0;
|
||||
self.stat.mode = DrawMode::Mode3;
|
||||
}
|
||||
}
|
||||
DrawMode::Mode3 => {
|
||||
const MODE3_MODE_CLOCK: usize = 172;
|
||||
// generate scanline
|
||||
if self.mode_clock >= MODE3_MODE_CLOCK {
|
||||
self.mode_clock -= MODE3_MODE_CLOCK;
|
||||
self.enter_hblank(output);
|
||||
if self.mode_clock >= 172 {
|
||||
self.mode_clock = 0;
|
||||
self.enter_hblank();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,40 +247,14 @@ where
|
|||
interrupts
|
||||
}
|
||||
|
||||
pub(crate) fn get_vram(&self, address: VramAddress) -> u8 {
|
||||
self.vram.get(address)
|
||||
}
|
||||
|
||||
pub(crate) fn set_vram(&mut self, address: VramAddress, data: u8) {
|
||||
self.vram.set(address, data)
|
||||
}
|
||||
|
||||
pub(crate) fn get_oam(&self, address: OamAddress) -> u8 {
|
||||
if self.stat.mode == DrawMode::VBlank || self.stat.mode == DrawMode::HBlank {
|
||||
self.oam.get(address)
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_oam(&mut self, address: OamAddress, data: u8) {
|
||||
if self.stat.mode == DrawMode::VBlank || self.stat.mode == DrawMode::HBlank {
|
||||
self.oam.set(address, data)
|
||||
}
|
||||
}
|
||||
|
||||
fn enter_hblank(&mut self, output: bool) {
|
||||
fn enter_hblank(&mut self) {
|
||||
self.stat.mode = DrawMode::HBlank;
|
||||
if output {
|
||||
self.render_scanline(self.scanline);
|
||||
}
|
||||
self.render_scanline(self.scanline);
|
||||
}
|
||||
|
||||
fn enter_vblank(&mut self, output: bool) {
|
||||
fn enter_vblank(&mut self) {
|
||||
self.stat.mode = DrawMode::VBlank;
|
||||
if output {
|
||||
self.render_window();
|
||||
}
|
||||
self.render_window();
|
||||
}
|
||||
|
||||
fn exit_vblank(&mut self) {
|
||||
|
@ -240,13 +262,19 @@ where
|
|||
self.scanline = 0;
|
||||
self.window_lc = 0;
|
||||
self.has_window_been_enabled = false;
|
||||
if let Some(tile_window) = &mut self.tile_window {
|
||||
tile_window.draw_sprite_window(self.bg_palette, &self.vram);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_scanline(&mut self, scanline: u8) {
|
||||
for e in &mut self.is_bg_zero {
|
||||
*e = true;
|
||||
}
|
||||
if self.lcdc.bg_window_enable || self.is_cgb_mode() {
|
||||
for x in 0..WIDTH {
|
||||
self.buffer[(scanline as usize * WIDTH) + x] = Colour::Error.into();
|
||||
}
|
||||
if self.lcdc.bg_window_enable {
|
||||
self.render_scanline_bg(scanline);
|
||||
if self.lcdc.window_enable {
|
||||
if !self.has_window_been_enabled {
|
||||
|
@ -257,13 +285,11 @@ where
|
|||
}
|
||||
} else {
|
||||
for x in 0..WIDTH {
|
||||
self.buffer[(scanline as usize * WIDTH) + x] = ColourInner::Error
|
||||
.rgb_bytes(self.cgb_data.as_ref().map(|d| (&d.palettes.bg, 0_u8)))
|
||||
.into();
|
||||
self.buffer[(scanline as usize * WIDTH) + x] = Colour::Error.into();
|
||||
}
|
||||
}
|
||||
if self.lcdc.obj_enable {
|
||||
self.render_scanline_obj(scanline, !self.lcdc.bg_window_enable && self.is_cgb_mode());
|
||||
self.render_scanline_obj(scanline);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -295,28 +321,28 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn render_scanline_obj(&mut self, scanline: u8, obj_priority: bool) {
|
||||
fn render_scanline_obj(&mut self, scanline: u8) {
|
||||
let objs = self.parse_oam(scanline);
|
||||
for object in objs {
|
||||
self.render_object(scanline, object, obj_priority);
|
||||
self.render_object(scanline, object);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_oam(&mut self, scanline: u8) -> Vec<Object> {
|
||||
let mut objs = Vec::new();
|
||||
let mut objs = vec![];
|
||||
let effective_scanline = scanline + 16;
|
||||
for i in (0xFE00..0xFE9F).step_by(4) {
|
||||
let y_pos = self.oam.get(i.try_into().unwrap());
|
||||
let y_pos = self.oam.get(i);
|
||||
if y_pos <= effective_scanline
|
||||
&& (y_pos + self.lcdc.obj_size.get_height()) > effective_scanline
|
||||
{
|
||||
// sprite is on line
|
||||
let x_pos = self.oam.get((i + 1).try_into().unwrap());
|
||||
let mut tile_index = self.oam.get((i + 2).try_into().unwrap());
|
||||
let x_pos = self.oam.get(i + 1);
|
||||
let mut tile_index = self.oam.get(i + 2);
|
||||
if self.lcdc.obj_size == ObjSize::S8x16 {
|
||||
tile_index = clear_bit(tile_index, 0);
|
||||
}
|
||||
let flags = self.oam.get((i + 3).try_into().unwrap());
|
||||
let flags = self.oam.get(i + 3);
|
||||
objs.push(Object {
|
||||
x: x_pos,
|
||||
y: y_pos,
|
||||
|
@ -325,67 +351,49 @@ where
|
|||
behind_bg_and_window: get_bit(flags, 7),
|
||||
y_flip: get_bit(flags, 6),
|
||||
x_flip: get_bit(flags, 5),
|
||||
dmg_palette: if get_bit(flags, 4) {
|
||||
palette: if get_bit(flags, 4) {
|
||||
ObjPalette::One
|
||||
} else {
|
||||
ObjPalette::Zero
|
||||
},
|
||||
cgb_vram_bank: if get_bit(flags, 3) {
|
||||
VramBank::Bank1
|
||||
} else {
|
||||
VramBank::Bank0
|
||||
},
|
||||
cgb_palette: flags & 0b111,
|
||||
},
|
||||
oam_location: (i - 0xFE00) as u8,
|
||||
});
|
||||
if objs.len() >= 10 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
objs.sort_by_key(|o| {
|
||||
if self.is_cgb_mode() {
|
||||
o.oam_location
|
||||
} else {
|
||||
o.x
|
||||
}
|
||||
let mut v: u16 = 0x0;
|
||||
v.set_high(o.x);
|
||||
v.set_low(o.oam_location);
|
||||
v
|
||||
});
|
||||
objs.truncate(10);
|
||||
objs.reverse();
|
||||
objs
|
||||
}
|
||||
|
||||
fn render_object(&mut self, scanline: u8, object: Object, obj_priority: bool) {
|
||||
let mut object_row = scanline.wrapping_sub(object.y.wrapping_sub(16));
|
||||
fn render_object(&mut self, scanline: u8, object: Object) {
|
||||
let mut object_row = scanline - (object.y.wrapping_sub(16));
|
||||
if object.flags.y_flip {
|
||||
object_row = self.lcdc.obj_size.get_height() - (object_row + 1);
|
||||
}
|
||||
let tile_row = object_row % 8;
|
||||
let tile_addr = (TiledataArea::D8000
|
||||
let tile_addr = TiledataArea::D8000
|
||||
.get_addr(object.tile_index + if object_row >= 8 { 1 } else { 0 })
|
||||
+ (tile_row as u16 * 2))
|
||||
.unwrap();
|
||||
let bank = if self.is_cgb_mode() {
|
||||
object.flags.cgb_vram_bank
|
||||
} else {
|
||||
VramBank::Bank0
|
||||
};
|
||||
let lsbs = self.vram.get_with_bank(tile_addr, bank).unwrap();
|
||||
let msbs = self
|
||||
.vram
|
||||
.get_with_bank((tile_addr + 1).unwrap(), bank)
|
||||
.unwrap();
|
||||
+ (tile_row as u16 * 2);
|
||||
let lsbs = self.vram.get(tile_addr);
|
||||
let msbs = self.vram.get(tile_addr + 1);
|
||||
for px_x in 0..8 {
|
||||
let x_addr = if object.flags.x_flip { px_x } else { 7 - px_x };
|
||||
let lsb = get_bit(lsbs, x_addr);
|
||||
let msb = get_bit(msbs, x_addr);
|
||||
let (colour, is_zero) = if self.is_cgb_mode() {
|
||||
(ColourInner::from_bits(lsb, msb), !lsb && !msb)
|
||||
} else {
|
||||
match object.flags.dmg_palette {
|
||||
ObjPalette::Zero => self.obj_palette_0,
|
||||
ObjPalette::One => self.obj_palette_1,
|
||||
}
|
||||
.map_bits(lsb, msb)
|
||||
};
|
||||
let (colour, is_zero) = match object.flags.palette {
|
||||
ObjPalette::Zero => self.obj_palette_0,
|
||||
ObjPalette::One => self.obj_palette_1,
|
||||
}
|
||||
.map_bits(lsb, msb);
|
||||
if is_zero {
|
||||
continue;
|
||||
}
|
||||
|
@ -395,30 +403,9 @@ where
|
|||
}
|
||||
let x_coord = x_coord_uncorrected - 8;
|
||||
if x_coord < WIDTH {
|
||||
let cgb_data = self.cgb_data.as_ref().map(|v| {
|
||||
(
|
||||
&v.palettes.obj,
|
||||
if self.is_cgb_mode() {
|
||||
object.flags.cgb_palette
|
||||
} else {
|
||||
0
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
let buffer_index = (scanline as usize * WIDTH) + x_coord;
|
||||
if (!object.flags.behind_bg_and_window && !self.is_bg_priority[x_coord])
|
||||
|| self.is_bg_zero[x_coord]
|
||||
|| (self.is_cgb_mode() && !self.lcdc.bg_window_enable)
|
||||
|| obj_priority
|
||||
{
|
||||
self.buffer[buffer_index] = colour.rgb_bytes(cgb_data).into();
|
||||
}
|
||||
if let Some(ref mut layer_window) = self.layer_window {
|
||||
layer_window.set(
|
||||
buffer_index + (2 * HEIGHT * WIDTH),
|
||||
colour.rgb_bytes(cgb_data).into(),
|
||||
)
|
||||
if !object.flags.behind_bg_and_window || self.is_bg_zero[x_coord] {
|
||||
self.buffer[buffer_index] = colour.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -431,113 +418,45 @@ where
|
|||
tilemap: TilemapArea,
|
||||
offset_x: u8,
|
||||
offset_y: u8,
|
||||
is_bg: bool,
|
||||
wrap: bool,
|
||||
) {
|
||||
let (tile_line_y, did_wrap_y) = draw_from.overflowing_sub(offset_y);
|
||||
if did_wrap_y && !is_bg {
|
||||
if did_wrap_y && !wrap {
|
||||
return;
|
||||
}
|
||||
let tilemap_row = tile_line_y / 8;
|
||||
let tile_px_y = (tile_line_y) % 8;
|
||||
let tiledata_offset = tile_px_y * 2;
|
||||
let row_addr = ((tilemap_row as usize * 32) % 0x400) as u16;
|
||||
for x in 0..WIDTH {
|
||||
let (tile_line_x, did_wrap_x) = (x as u8).overflowing_sub(offset_x);
|
||||
if did_wrap_x && !is_bg {
|
||||
if did_wrap_x && !wrap {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tilemap_column = (tile_line_x / 8) as u16;
|
||||
|
||||
let tilemap_addr = tilemap.get_addr(row_addr + (tilemap_column));
|
||||
let attributes = if self.is_cgb_mode() {
|
||||
self.vram
|
||||
.get_with_bank(tilemap_addr, VramBank::Bank1)
|
||||
.map_or(BgAttributes::default(), BgAttributes::from_byte)
|
||||
} else {
|
||||
BgAttributes::default()
|
||||
};
|
||||
let tile_px_x = tile_line_x % 8;
|
||||
let tile_addr = self
|
||||
.lcdc
|
||||
.tile_area
|
||||
.get_addr(self.vram.get(tilemap.get_addr(row_addr + (tilemap_column))))
|
||||
+ tiledata_offset as u16;
|
||||
|
||||
let tile_px_y = if attributes.flip_v {
|
||||
7 - ((tile_line_y) % 8)
|
||||
} else {
|
||||
(tile_line_y) % 8
|
||||
};
|
||||
let tiledata_offset = tile_px_y * 2;
|
||||
|
||||
let tile_addr = (self.lcdc.tile_area.get_addr(
|
||||
self.vram
|
||||
.get_with_bank(tilemap_addr, VramBank::Bank0)
|
||||
.unwrap(),
|
||||
) + tiledata_offset as u16)
|
||||
.unwrap();
|
||||
|
||||
let lsbs = self
|
||||
.vram
|
||||
.get_with_bank(tile_addr, attributes.tile_bank)
|
||||
.unwrap();
|
||||
let msbs = self
|
||||
.vram
|
||||
.get_with_bank((tile_addr + 1).unwrap(), attributes.tile_bank)
|
||||
.unwrap();
|
||||
|
||||
let tile_px_x = if attributes.flip_h {
|
||||
7 - (tile_line_x % 8)
|
||||
} else {
|
||||
tile_line_x % 8
|
||||
};
|
||||
let lsbs = self.vram.get(tile_addr);
|
||||
let msbs = self.vram.get(tile_addr + 1);
|
||||
let lsb = get_bit(lsbs, 7 - tile_px_x);
|
||||
let msb = get_bit(msbs, 7 - tile_px_x);
|
||||
let (colour, is_zero) = if self.is_cgb_mode() {
|
||||
(ColourInner::from_bits(lsb, msb), !lsb && !msb)
|
||||
} else {
|
||||
self.bg_palette.map_bits(lsb, msb)
|
||||
};
|
||||
let (colour, is_zero) = self.bg_palette.map_bits(lsb, msb);
|
||||
self.is_bg_zero[x] = is_zero;
|
||||
self.is_bg_priority[x] = attributes.bg_priority;
|
||||
|
||||
let cgb_data = self
|
||||
.cgb_data
|
||||
.as_ref()
|
||||
.map(|v| (&v.palettes.bg, attributes.palette));
|
||||
|
||||
self.buffer[(scanline as usize * WIDTH) + x] = colour.rgb_bytes(cgb_data).into();
|
||||
|
||||
if let Some(ref mut layer_window) = self.layer_window {
|
||||
layer_window.set(
|
||||
(scanline as usize * WIDTH) + x + if is_bg { 0 } else { HEIGHT * WIDTH },
|
||||
colour.rgb_bytes(cgb_data).into(),
|
||||
)
|
||||
}
|
||||
self.buffer[(scanline as usize * WIDTH) + x] = colour.into();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_window(&mut self) {
|
||||
let mut buffer = Vec::new();
|
||||
buffer.extend_from_slice(self.buffer.as_ref());
|
||||
if let Some(window) = &self.window {
|
||||
window
|
||||
.send(RendererMessage::display_message(buffer))
|
||||
.expect("message error");
|
||||
}
|
||||
|
||||
self.tile_window = self
|
||||
.tile_window
|
||||
.take()
|
||||
.and_then(|v| v.draw_and_send_frame(self.bg_palette, &self.vram, self.is_cgb_mode()));
|
||||
|
||||
self.layer_window = self
|
||||
.layer_window
|
||||
.take()
|
||||
.and_then(|v| v.send_frame(self.cgb_data.is_some()));
|
||||
}
|
||||
}
|
||||
|
||||
fn get_blank_colour<ColourFormat>(cgb: bool) -> ColourFormat
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
if cgb {
|
||||
rgb_from_bytes(0xFFFF).into()
|
||||
} else {
|
||||
ColourInner::Error.rgb_bytes(None).into()
|
||||
println!("gpu sending frame");
|
||||
self.window.display(&self.buffer);
|
||||
println!("gpu finished sending frame");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
use crate::util::{get_bit, set_or_clear_bit};
|
||||
use crate::{
|
||||
connect::Renderer,
|
||||
util::{get_bit, set_or_clear_bit},
|
||||
};
|
||||
|
||||
use super::{
|
||||
types::{DrawMode, ObjSize, Palette, TiledataArea, TilemapArea},
|
||||
Colour, Gpu,
|
||||
};
|
||||
|
||||
impl<ColourFormat> Gpu<ColourFormat>
|
||||
impl<ColourFormat, R> Gpu<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn update_lcdc(&mut self, data: u8) {
|
||||
self.lcdc.enable = get_bit(data, 7);
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
processor::memory::addresses::{AddressMarker, CgbPaletteAddress},
|
||||
util::get_bit,
|
||||
};
|
||||
|
||||
use super::{Colour, Gpu};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct CgbData {
|
||||
pub(super) palettes: CgbPaletteRegisters,
|
||||
pub(super) object_priority_mode: ObjectPriorityMode,
|
||||
compat_byte: u8,
|
||||
}
|
||||
|
||||
impl Default for CgbData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
palettes: Default::default(),
|
||||
object_priority_mode: ObjectPriorityMode::Coordinate,
|
||||
compat_byte: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
|
||||
pub(super) enum ObjectPriorityMode {
|
||||
OamLocation = 0,
|
||||
Coordinate = 1,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
|
||||
pub(super) struct CgbPaletteRegisters {
|
||||
pub(super) bg: CgbPalette,
|
||||
pub(super) obj: CgbPalette,
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct CgbPalette {
|
||||
auto_increment: bool,
|
||||
index: u8,
|
||||
#[serde_as(as = "[_; 0x40]")]
|
||||
pub(super) data: [u8; 0x40],
|
||||
}
|
||||
|
||||
impl Default for CgbPalette {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
auto_increment: false,
|
||||
index: 0,
|
||||
data: [0; 0x40],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CgbPalette {
|
||||
fn set_control(&mut self, data: u8) {
|
||||
self.index = data & 0b111111;
|
||||
self.auto_increment = get_bit(data, 7);
|
||||
}
|
||||
|
||||
fn get_control(&self) -> u8 {
|
||||
(if self.auto_increment { 1 << 7 } else { 0 } | (self.index & 0b111111))
|
||||
}
|
||||
|
||||
fn set_data(&mut self, data: u8) {
|
||||
self.data[self.index as usize] = data;
|
||||
if self.auto_increment {
|
||||
self.index = (self.index + 1) & 0b111111
|
||||
}
|
||||
}
|
||||
|
||||
fn get_data(&self) -> u8 {
|
||||
self.data[self.index as usize]
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Gpu<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(crate) fn get_cgb_palette(&self, address: CgbPaletteAddress) -> u8 {
|
||||
if let Some(cgb_data) = &self.cgb_data {
|
||||
match address.inner() {
|
||||
0xFF68 => cgb_data.palettes.bg.get_control(),
|
||||
0xFF69 => cgb_data.palettes.bg.get_data(),
|
||||
0xFF6A => cgb_data.palettes.obj.get_control(),
|
||||
0xFF6B => cgb_data.palettes.obj.get_data(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_cgb_palette(&mut self, address: CgbPaletteAddress, data: u8) {
|
||||
if let Some(cgb_data) = &mut self.cgb_data {
|
||||
match address.inner() {
|
||||
0xFF68 => cgb_data.palettes.bg.set_control(data),
|
||||
0xFF69 => cgb_data.palettes.bg.set_data(data),
|
||||
0xFF6A => cgb_data.palettes.obj.set_control(data),
|
||||
0xFF6B => cgb_data.palettes.obj.set_data(data),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_obj_priority(&self) -> u8 {
|
||||
if let Some(cgb_data) = &self.cgb_data {
|
||||
cgb_data.object_priority_mode as u8
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_obj_priority(&mut self, data: u8) {
|
||||
if let Some(cgb_data) = &mut self.cgb_data {
|
||||
cgb_data.object_priority_mode = if data & 0b1 == 0 {
|
||||
ObjectPriorityMode::OamLocation
|
||||
} else {
|
||||
ObjectPriorityMode::Coordinate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_compat_byte(&self) -> u8 {
|
||||
if let Some(cgb_data) = &self.cgb_data {
|
||||
cgb_data.compat_byte
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_compat_byte(&mut self, data: u8) {
|
||||
if let Some(cgb_data) = &mut self.cgb_data {
|
||||
cgb_data.compat_byte = data;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_cgb_mode(&self) -> bool {
|
||||
if let Some(cgb_data) = &self.cgb_data {
|
||||
cgb_data.compat_byte != 0x04
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
|
||||
use super::{get_blank_colour, Buffer, Colour, NewBuffer};
|
||||
use crate::{connect::RendererMessage, HEIGHT, WIDTH};
|
||||
|
||||
pub(super) struct LayerWindow<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
sender: Sender<RendererMessage<ColourFormat>>,
|
||||
buffer: Buffer<ColourFormat, { WIDTH * HEIGHT * 3 }>,
|
||||
}
|
||||
|
||||
impl<ColourFormat> LayerWindow<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(super) fn new(sender: Sender<RendererMessage<ColourFormat>>, cgb: bool) -> Self {
|
||||
sender
|
||||
.send(RendererMessage::Prepare {
|
||||
width: WIDTH,
|
||||
height: HEIGHT * 3,
|
||||
})
|
||||
.expect("message error");
|
||||
|
||||
Self {
|
||||
sender,
|
||||
buffer: Buffer::new_buffer(get_blank_colour(cgb)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, index: usize, value: ColourFormat) {
|
||||
self.buffer[index] = value;
|
||||
}
|
||||
|
||||
pub(super) fn send_frame(mut self, cgb: bool) -> Option<Self> {
|
||||
let mut new_buffer = Vec::new();
|
||||
new_buffer.extend_from_slice(self.buffer.as_ref());
|
||||
|
||||
match self
|
||||
.sender
|
||||
.send(RendererMessage::display_message(new_buffer))
|
||||
{
|
||||
Ok(_) => {
|
||||
for val in self.buffer.iter_mut() {
|
||||
*val = get_blank_colour(cgb);
|
||||
}
|
||||
Some(self)
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,173 +1,78 @@
|
|||
use std::sync::mpsc::Sender;
|
||||
|
||||
use crate::{
|
||||
connect::RendererMessage,
|
||||
processor::memory::mmio::gpu::{Palette, TILE_WINDOW_HEIGHT, TILE_WINDOW_WIDTH},
|
||||
connect::Renderer,
|
||||
processor::memory::mmio::gpu::{Palette, TiledataArea, TILE_WINDOW_HEIGHT, TILE_WINDOW_WIDTH},
|
||||
util::get_bit,
|
||||
};
|
||||
|
||||
use super::{
|
||||
types::{BgAttributes, ColourInner, Vram, VramBank},
|
||||
Colour,
|
||||
};
|
||||
use super::{types::Vram, Colour};
|
||||
|
||||
pub(super) struct TileWindow<ColourFormat>
|
||||
pub(super) struct TileWindow<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
sender: Sender<RendererMessage<ColourFormat>>,
|
||||
buffer: Vec<ColourFormat>,
|
||||
currently_cgb: bool,
|
||||
sprite_buffer: Vec<ColourFormat>,
|
||||
sprite_renderer: R,
|
||||
}
|
||||
|
||||
impl<ColourFormat> TileWindow<ColourFormat>
|
||||
impl<ColourFormat, R> TileWindow<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub(super) fn new(window: Sender<RendererMessage<ColourFormat>>, cgb: bool) -> Self {
|
||||
let current_width = if cgb {
|
||||
TILE_WINDOW_WIDTH * 2
|
||||
} else {
|
||||
TILE_WINDOW_WIDTH
|
||||
};
|
||||
|
||||
window
|
||||
.send(RendererMessage::Prepare {
|
||||
width: current_width,
|
||||
height: TILE_WINDOW_HEIGHT,
|
||||
})
|
||||
.expect("message error");
|
||||
|
||||
pub(super) fn new(window: R) -> Self {
|
||||
Self {
|
||||
sender: window,
|
||||
buffer: vec![
|
||||
ColourInner::Error.rgb_bytes(None).into();
|
||||
current_width * TILE_WINDOW_HEIGHT
|
||||
],
|
||||
currently_cgb: cgb,
|
||||
sprite_buffer: vec![Colour::Error.into(); TILE_WINDOW_WIDTH * TILE_WINDOW_HEIGHT],
|
||||
sprite_renderer: window,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn draw_and_send_frame(
|
||||
mut self,
|
||||
palette: Palette,
|
||||
memory: &Vram,
|
||||
is_cgb_mode: bool,
|
||||
) -> Option<Self> {
|
||||
if self.currently_cgb != is_cgb_mode {
|
||||
self.currently_cgb = is_cgb_mode;
|
||||
let current_width = if is_cgb_mode {
|
||||
TILE_WINDOW_WIDTH * 2
|
||||
} else {
|
||||
TILE_WINDOW_WIDTH
|
||||
};
|
||||
self.sender
|
||||
.send(RendererMessage::Resize {
|
||||
width: current_width,
|
||||
height: TILE_WINDOW_HEIGHT,
|
||||
})
|
||||
.expect("message error");
|
||||
self.buffer =
|
||||
vec![ColourInner::Error.rgb_bytes(None).into(); current_width * TILE_WINDOW_HEIGHT];
|
||||
pub(super) fn draw_sprite_window(&mut self, palette: Palette, memory: &Vram) {
|
||||
for tile_y in 0..16 {
|
||||
self.draw_row(
|
||||
tile_y,
|
||||
tile_y as usize,
|
||||
TiledataArea::D8000,
|
||||
palette,
|
||||
memory,
|
||||
);
|
||||
}
|
||||
for tile_y in 0..8 {
|
||||
self.draw_row(
|
||||
tile_y,
|
||||
(tile_y as usize) + 16,
|
||||
TiledataArea::D9000,
|
||||
palette,
|
||||
memory,
|
||||
);
|
||||
}
|
||||
|
||||
for (tile_y, data_begin) in (0x8000..0xA000).step_by(0x10 * 16).enumerate() {
|
||||
self.draw_row(tile_y, data_begin, palette, memory, is_cgb_mode);
|
||||
}
|
||||
|
||||
match self
|
||||
.sender
|
||||
.send(RendererMessage::display_message(self.buffer.clone()))
|
||||
{
|
||||
Ok(_) => Some(self),
|
||||
Err(_) => None,
|
||||
}
|
||||
self.sprite_renderer.display(&self.sprite_buffer);
|
||||
}
|
||||
|
||||
fn draw_row(
|
||||
&mut self,
|
||||
tile_y: u8,
|
||||
display_y: usize,
|
||||
data_begin: u16,
|
||||
area: TiledataArea,
|
||||
palette: Palette,
|
||||
memory: &Vram,
|
||||
is_cgb_mode: bool,
|
||||
) {
|
||||
let line_width = if is_cgb_mode {
|
||||
TILE_WINDOW_WIDTH * 2
|
||||
} else {
|
||||
TILE_WINDOW_WIDTH
|
||||
};
|
||||
self.draw_row_from_bank(
|
||||
display_y,
|
||||
data_begin,
|
||||
palette,
|
||||
memory,
|
||||
is_cgb_mode,
|
||||
VramBank::Bank0,
|
||||
0,
|
||||
line_width,
|
||||
);
|
||||
if is_cgb_mode {
|
||||
self.draw_row_from_bank(
|
||||
display_y,
|
||||
data_begin,
|
||||
palette,
|
||||
memory,
|
||||
is_cgb_mode,
|
||||
VramBank::Bank1,
|
||||
TILE_WINDOW_WIDTH,
|
||||
line_width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_row_from_bank(
|
||||
&mut self,
|
||||
display_y: usize,
|
||||
data_begin: u16,
|
||||
palette: Palette,
|
||||
memory: &Vram,
|
||||
is_cgb_mode: bool,
|
||||
bank: VramBank,
|
||||
offset: usize,
|
||||
line_width: usize,
|
||||
) {
|
||||
for tile_x in 0..16 {
|
||||
let attributes = BgAttributes {
|
||||
tile_bank: bank,
|
||||
..Default::default()
|
||||
};
|
||||
let data_begin = data_begin + ((tile_x * 16) as u16);
|
||||
|
||||
for px_y in 0..8_u16 {
|
||||
let lsbs = memory
|
||||
.get_with_bank(
|
||||
(data_begin + (px_y * 2)).try_into().unwrap(),
|
||||
attributes.tile_bank,
|
||||
)
|
||||
.unwrap();
|
||||
let msbs = memory
|
||||
.get_with_bank(
|
||||
(data_begin + (1 + (px_y * 2))).try_into().unwrap(),
|
||||
attributes.tile_bank,
|
||||
)
|
||||
.unwrap();
|
||||
let tile_num = (tile_y * 16) + tile_x;
|
||||
let data_begin = area.get_addr(tile_num);
|
||||
for px_y in 0..8 {
|
||||
let lsbs = memory.get((px_y * 2) + data_begin);
|
||||
let msbs = memory.get((px_y * 2) + data_begin + 1);
|
||||
for px_x in 0..8 {
|
||||
let real_px_y = (display_y * 8) + px_y as usize;
|
||||
let real_px_x = (tile_x as usize * 8) + px_x as usize;
|
||||
let lsb = get_bit(lsbs, 7 - px_x);
|
||||
let msb = get_bit(msbs, 7 - px_x);
|
||||
let colour = if is_cgb_mode {
|
||||
ColourInner::from_bits(lsb, msb)
|
||||
} else {
|
||||
palette.map_bits(lsb, msb).0
|
||||
};
|
||||
let colour = palette.map_bits(lsb, msb);
|
||||
|
||||
let addr = offset + real_px_x + (real_px_y * line_width);
|
||||
if addr < self.buffer.len() {
|
||||
self.buffer[addr] = colour.rgb_bytes(None).into();
|
||||
}
|
||||
self.sprite_buffer[real_px_x + (real_px_y * TILE_WINDOW_WIDTH)] =
|
||||
colour.0.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
use bytemuck::from_bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
constants::{dmg_colours, ERROR_COLOUR},
|
||||
processor::memory::addresses::{OamAddress, VramAddress},
|
||||
util::{as_signed, get_bit, SaturatingCast},
|
||||
processor::memory::Address,
|
||||
util::{as_signed, get_bit},
|
||||
};
|
||||
|
||||
use super::cgb::CgbPalette;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub(crate) enum DrawMode {
|
||||
pub(super) enum DrawMode {
|
||||
HBlank,
|
||||
VBlank,
|
||||
Mode2,
|
||||
|
@ -24,10 +20,10 @@ pub(super) enum TilemapArea {
|
|||
}
|
||||
|
||||
impl TilemapArea {
|
||||
pub(super) fn get_addr(&self, addr: u16) -> VramAddress {
|
||||
pub(super) fn get_addr(&self, addr: u16) -> u16 {
|
||||
match self {
|
||||
TilemapArea::T9800 => (0x9800 + addr).try_into().unwrap(),
|
||||
TilemapArea::T9C00 => (0x9C00 + addr).try_into().unwrap(),
|
||||
TilemapArea::T9800 => 0x9800 + addr,
|
||||
TilemapArea::T9C00 => 0x9C00 + addr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,13 +35,10 @@ pub(super) enum TiledataArea {
|
|||
}
|
||||
|
||||
impl TiledataArea {
|
||||
pub(super) fn get_addr(&self, addr: u8) -> VramAddress {
|
||||
pub(super) fn get_addr(&self, addr: u8) -> u16 {
|
||||
match self {
|
||||
TiledataArea::D8000 => (0x8000 + ((addr as u16) * 16)).try_into().unwrap(),
|
||||
TiledataArea::D9000 => 0x9000_u16
|
||||
.wrapping_add_signed((as_signed(addr) as i16) * 16)
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
TiledataArea::D8000 => 0x8000 + ((addr as u16) * 16),
|
||||
TiledataArea::D9000 => 0x9000_u16.wrapping_add_signed((as_signed(addr) as i16) * 16),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -93,119 +86,75 @@ impl Default for Lcdc {
|
|||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ColourInner {
|
||||
Zero = 0,
|
||||
One = 1,
|
||||
Two = 2,
|
||||
Three = 3,
|
||||
Error = 255,
|
||||
pub enum Colour {
|
||||
White,
|
||||
LightGray,
|
||||
DarkGray,
|
||||
Black,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct Colour(pub u8, pub u8, pub u8);
|
||||
|
||||
impl From<Colour> for u32 {
|
||||
fn from(value: Colour) -> Self {
|
||||
let (r, g, b) = (value.0 as u32, value.1 as u32, value.2 as u32);
|
||||
let rgb = value.rgb_bytes();
|
||||
let (r, g, b) = (rgb.0 as u32, rgb.1 as u32, rgb.2 as u32);
|
||||
(r << 16) | (g << 8) | b
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Colour> for [u8; 4] {
|
||||
fn from(value: Colour) -> Self {
|
||||
let Colour(r, g, b) = value;
|
||||
let (r, g, b) = value.rgb_bytes();
|
||||
[r, g, b, 0xFF]
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn rgb_from_bytes(bytes: u16) -> Colour {
|
||||
let b = (bytes & (0b11111 << 10)) >> 10;
|
||||
let g = (bytes & (0b11111 << 5)) >> 5;
|
||||
let r = bytes & 0b11111;
|
||||
// direct colour emulation:
|
||||
// let blue = (b << 3) | (b >> 2);
|
||||
// let green = (g << 3) | (g >> 2);
|
||||
// let red = (r << 3) | (r >> 2);
|
||||
|
||||
// colour emulation from
|
||||
// https://web.archive.org/web/20200322151952/https://byuu.net/video/color-emulation
|
||||
let blue = (r * 6 + g * 4 + b * 22).min(960) >> 2;
|
||||
let green = (g * 24 + b * 8).min(960) >> 2;
|
||||
let red = (r * 26 + g * 4 + b * 2).min(960) >> 2;
|
||||
Colour(
|
||||
red.saturating_cast(),
|
||||
green.saturating_cast(),
|
||||
blue.saturating_cast(),
|
||||
)
|
||||
}
|
||||
|
||||
impl ColourInner {
|
||||
pub(super) fn rgb_bytes(&self, cgb_data: Option<(&CgbPalette, u8)>) -> Colour {
|
||||
if let Some((cgb_palette, pallete_num)) = cgb_data {
|
||||
let offset: usize = (pallete_num as usize * 2 * 4)
|
||||
+ (if *self == ColourInner::Error {
|
||||
if cfg!(feature = "error-colour") {
|
||||
return ERROR_COLOUR;
|
||||
} else {
|
||||
ColourInner::Zero
|
||||
}
|
||||
} else {
|
||||
*self
|
||||
} as usize
|
||||
* 2);
|
||||
|
||||
rgb_from_bytes(*from_bytes(&cgb_palette.data[offset..=offset + 1]))
|
||||
} else {
|
||||
match self {
|
||||
ColourInner::Zero => dmg_colours::ZERO,
|
||||
ColourInner::One => dmg_colours::ONE,
|
||||
ColourInner::Two => dmg_colours::TWO,
|
||||
ColourInner::Three => dmg_colours::THREE,
|
||||
ColourInner::Error => {
|
||||
if cfg!(feature = "error-colour") {
|
||||
ERROR_COLOUR
|
||||
} else {
|
||||
dmg_colours::ZERO
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Colour {
|
||||
fn rgb_bytes(&self) -> (u8, u8, u8) {
|
||||
match self {
|
||||
Colour::White => (0xFF, 0xFF, 0xFF),
|
||||
Colour::LightGray => (0xAA, 0xAA, 0xAA),
|
||||
Colour::DarkGray => (0x55, 0x55, 0x55),
|
||||
Colour::Black => (0x00, 0x00, 0x00),
|
||||
Colour::Error => (0xFF, 0x00, 0x00),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn from_bits(first: bool, second: bool) -> ColourInner {
|
||||
pub(super) fn from_bits(first: bool, second: bool) -> Colour {
|
||||
match (first, second) {
|
||||
(true, true) => ColourInner::Three,
|
||||
(true, false) => ColourInner::One,
|
||||
(false, true) => ColourInner::Two,
|
||||
(false, false) => ColourInner::Zero,
|
||||
(true, true) => Colour::Black,
|
||||
(true, false) => Colour::LightGray,
|
||||
(false, true) => Colour::DarkGray,
|
||||
(false, false) => Colour::White,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_bits(&self) -> u8 {
|
||||
match self {
|
||||
ColourInner::Zero => 0b00,
|
||||
ColourInner::One => 0b01,
|
||||
ColourInner::Two => 0b10,
|
||||
ColourInner::Three => 0b11,
|
||||
ColourInner::Error => 0b00,
|
||||
Colour::White => 0b00,
|
||||
Colour::LightGray => 0b10,
|
||||
Colour::DarkGray => 0b01,
|
||||
Colour::Black => 0b11,
|
||||
Colour::Error => 0b00,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub(super) struct Palette {
|
||||
pub(super) zero: ColourInner,
|
||||
pub(super) one: ColourInner,
|
||||
pub(super) two: ColourInner,
|
||||
pub(super) three: ColourInner,
|
||||
pub(super) zero: Colour,
|
||||
pub(super) one: Colour,
|
||||
pub(super) two: Colour,
|
||||
pub(super) three: Colour,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub(super) fn from_byte(byte: u8) -> Palette {
|
||||
Palette {
|
||||
zero: ColourInner::from_bits(get_bit(byte, 0), get_bit(byte, 1)),
|
||||
one: ColourInner::from_bits(get_bit(byte, 2), get_bit(byte, 3)),
|
||||
two: ColourInner::from_bits(get_bit(byte, 4), get_bit(byte, 5)),
|
||||
three: ColourInner::from_bits(get_bit(byte, 6), get_bit(byte, 7)),
|
||||
zero: Colour::from_bits(get_bit(byte, 0), get_bit(byte, 1)),
|
||||
one: Colour::from_bits(get_bit(byte, 2), get_bit(byte, 3)),
|
||||
two: Colour::from_bits(get_bit(byte, 4), get_bit(byte, 5)),
|
||||
three: Colour::from_bits(get_bit(byte, 6), get_bit(byte, 7)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,7 +165,7 @@ impl Palette {
|
|||
| (self.three.as_bits() << 6)
|
||||
}
|
||||
|
||||
pub(super) fn map_bits(&self, lsb: bool, msb: bool) -> (ColourInner, bool) {
|
||||
pub(super) fn map_bits(&self, lsb: bool, msb: bool) -> (Colour, bool) {
|
||||
match (lsb, msb) {
|
||||
(true, true) => (self.three, false),
|
||||
(true, false) => (self.one, false),
|
||||
|
@ -230,9 +179,7 @@ pub(super) struct ObjectFlags {
|
|||
pub(super) behind_bg_and_window: bool,
|
||||
pub(super) y_flip: bool,
|
||||
pub(super) x_flip: bool,
|
||||
pub(super) dmg_palette: ObjPalette,
|
||||
pub(super) cgb_vram_bank: VramBank,
|
||||
pub(super) cgb_palette: u8,
|
||||
pub(super) palette: ObjPalette,
|
||||
}
|
||||
|
||||
pub(super) enum ObjPalette {
|
||||
|
@ -248,7 +195,7 @@ pub(super) struct Object {
|
|||
pub(super) oam_location: u8,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub(super) struct Stat {
|
||||
pub(super) lyc_eq_ly_interrupt_enabled: bool,
|
||||
pub(super) mode_2_interrupt_enabled: bool,
|
||||
|
@ -269,93 +216,26 @@ impl Default for Stat {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||
pub enum VramBank {
|
||||
Bank0 = 0,
|
||||
Bank1 = 1,
|
||||
}
|
||||
|
||||
#[serde_with::serde_as]
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub enum Vram {
|
||||
Dmg {
|
||||
#[serde_as(as = "Box<[_; 8192]>")]
|
||||
inner: Box<[u8; 8192]>,
|
||||
},
|
||||
Cgb {
|
||||
#[serde_as(as = "Box<[[_; 8192];2]>")]
|
||||
inner: Box<[[u8; 8192]; 2]>,
|
||||
index: VramBank,
|
||||
},
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct Vram {
|
||||
#[serde_as(as = "[_; 8192]")]
|
||||
data: [u8; 8192],
|
||||
}
|
||||
|
||||
impl Vram {
|
||||
pub(crate) fn new(cgb: bool) -> Self {
|
||||
if cgb {
|
||||
Self::Cgb {
|
||||
inner: Box::new([[0; 8192]; 2]),
|
||||
index: VramBank::Bank0,
|
||||
}
|
||||
} else {
|
||||
Self::Dmg {
|
||||
inner: Box::new([0; 8192]),
|
||||
}
|
||||
}
|
||||
pub fn get(&self, address: Address) -> u8 {
|
||||
self.data[(address - 0x8000) as usize]
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, address: VramAddress) -> u8 {
|
||||
match self {
|
||||
Vram::Dmg { inner } => inner[address.get_local() as usize],
|
||||
Vram::Cgb { inner, index } => inner[*index as usize][address.get_local() as usize],
|
||||
}
|
||||
pub fn set(&mut self, address: Address, data: u8) {
|
||||
self.data[(address - 0x8000) as usize] = data;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_with_bank(&self, address: VramAddress, bank: VramBank) -> Option<u8> {
|
||||
match bank {
|
||||
VramBank::Bank0 => Some(self.bank0_get(address)),
|
||||
VramBank::Bank1 => self.bank1_get(address),
|
||||
}
|
||||
}
|
||||
|
||||
fn bank0_get(&self, address: VramAddress) -> u8 {
|
||||
match self {
|
||||
Vram::Dmg { inner } => inner[address.get_local() as usize],
|
||||
Vram::Cgb { inner, index: _ } => inner[0][address.get_local() as usize],
|
||||
}
|
||||
}
|
||||
|
||||
fn bank1_get(&self, address: VramAddress) -> Option<u8> {
|
||||
match self {
|
||||
Vram::Dmg { inner: _ } => None,
|
||||
Vram::Cgb { inner, index: _ } => Some(inner[1][address.get_local() as usize]),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, address: VramAddress, data: u8) {
|
||||
match self {
|
||||
Vram::Dmg { inner } => inner[address.get_local() as usize] = data,
|
||||
Vram::Cgb { inner, index } => {
|
||||
inner[*index as usize][address.get_local() as usize] = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_vram_bank(&self) -> u8 {
|
||||
if let Vram::Cgb { inner: _, index } = self {
|
||||
(*index as u8) | (!1)
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_vram_bank(&mut self, data: u8) {
|
||||
if let Vram::Cgb { inner: _, index } = self {
|
||||
*index = if data & 0b1 == 0 {
|
||||
VramBank::Bank0
|
||||
} else {
|
||||
VramBank::Bank1
|
||||
}
|
||||
}
|
||||
impl Default for Vram {
|
||||
fn default() -> Self {
|
||||
Self { data: [0x0; 8192] }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -367,12 +247,12 @@ pub struct Oam {
|
|||
}
|
||||
|
||||
impl Oam {
|
||||
pub(crate) fn get(&self, address: OamAddress) -> u8 {
|
||||
self.data[address.get_local() as usize]
|
||||
pub fn get(&self, address: Address) -> u8 {
|
||||
self.data[(address - 0xFE00) as usize]
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, address: OamAddress, data: u8) {
|
||||
self.data[address.get_local() as usize] = data;
|
||||
pub fn set(&mut self, address: Address, data: u8) {
|
||||
self.data[(address - 0xFE00) as usize] = data;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -387,40 +267,3 @@ pub struct GpuInterrupts {
|
|||
pub lcd_stat: bool,
|
||||
pub vblank: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BgAttributes {
|
||||
pub(super) bg_priority: bool,
|
||||
pub(super) flip_v: bool,
|
||||
pub(super) flip_h: bool,
|
||||
pub(super) tile_bank: VramBank,
|
||||
pub(super) palette: u8,
|
||||
}
|
||||
|
||||
impl Default for BgAttributes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bg_priority: false,
|
||||
flip_v: false,
|
||||
flip_h: false,
|
||||
tile_bank: VramBank::Bank0,
|
||||
palette: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BgAttributes {
|
||||
pub(super) fn from_byte(byte: u8) -> Self {
|
||||
Self {
|
||||
bg_priority: get_bit(byte, 7),
|
||||
flip_v: get_bit(byte, 6),
|
||||
flip_h: get_bit(byte, 5),
|
||||
tile_bank: if get_bit(byte, 3) {
|
||||
VramBank::Bank1
|
||||
} else {
|
||||
VramBank::Bank0
|
||||
},
|
||||
palette: byte & 0b111,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,6 @@ pub struct JoypadState {
|
|||
pub a: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum JoypadButtons {
|
||||
Down,
|
||||
Up,
|
||||
|
@ -45,19 +44,6 @@ impl JoypadState {
|
|||
self.a = false;
|
||||
self.b = false;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, button: JoypadButtons, state: bool) {
|
||||
*match button {
|
||||
JoypadButtons::Down => &mut self.down,
|
||||
JoypadButtons::Up => &mut self.up,
|
||||
JoypadButtons::Left => &mut self.left,
|
||||
JoypadButtons::Right => &mut self.right,
|
||||
JoypadButtons::Start => &mut self.start,
|
||||
JoypadButtons::Select => &mut self.select,
|
||||
JoypadButtons::B => &mut self.b,
|
||||
JoypadButtons::A => &mut self.a,
|
||||
} = state;
|
||||
}
|
||||
}
|
||||
|
||||
impl Joypad {
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
pub(crate) mod apu;
|
||||
pub(crate) mod cgb;
|
||||
pub(crate) mod gpu;
|
||||
pub(crate) mod joypad;
|
||||
mod oam_dma;
|
||||
pub(crate) mod serial;
|
||||
mod timer;
|
||||
pub use apu::Apu;
|
||||
pub use gpu::Gpu;
|
||||
pub use joypad::Joypad;
|
||||
pub use oam_dma::OamDma;
|
||||
pub use serial::Serial;
|
||||
pub use timer::Timer;
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
use super::gpu::Colour;
|
||||
use crate::processor::{memory::Memory, SplitRegister};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy)]
|
||||
pub struct OamDma {
|
||||
addr: u8,
|
||||
progress: Option<u8>,
|
||||
}
|
||||
|
||||
impl Default for OamDma {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
addr: 0xFF,
|
||||
progress: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OamDma {
|
||||
pub(crate) fn get_register(&self) -> u8 {
|
||||
self.addr
|
||||
}
|
||||
|
||||
pub(crate) fn set_register(&mut self, data: u8) {
|
||||
self.progress = Some(0);
|
||||
self.addr = data;
|
||||
}
|
||||
|
||||
pub(crate) fn is_active(&self) -> bool {
|
||||
self.progress.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat> Memory<ColourFormat>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
{
|
||||
pub(crate) fn oam_dma_tick(&mut self, steps: usize) {
|
||||
for _ in 0..steps {
|
||||
self.oam_dma.progress = if let Some(mut progress) = self.oam_dma.progress {
|
||||
let mut addr: u16 = 0x0;
|
||||
addr.set_high(self.oam_dma.addr);
|
||||
addr.set_low(progress);
|
||||
let val = if self.oam_dma.addr > 0xDF {
|
||||
0xFF
|
||||
} else {
|
||||
self.get(addr)
|
||||
};
|
||||
self.gpu.oam.data[progress as usize] = val;
|
||||
progress += 1;
|
||||
if progress == 0xA0 {
|
||||
None
|
||||
} else {
|
||||
Some(progress)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,12 +14,14 @@ enum ClockSource {
|
|||
External,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
enum ClockSpeed {
|
||||
Normal,
|
||||
Fast,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Debug)]
|
||||
struct SerialControl {
|
||||
transfer_in_progress: bool,
|
||||
|
@ -39,7 +41,7 @@ impl Default for SerialControl {
|
|||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum SerialTarget {
|
||||
Stdout(StdoutType),
|
||||
Stdout,
|
||||
Custom {
|
||||
#[serde(skip)]
|
||||
rx: Option<Receiver<u8>>,
|
||||
|
@ -49,12 +51,6 @@ pub enum SerialTarget {
|
|||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum StdoutType {
|
||||
Ascii,
|
||||
Hex,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Default)]
|
||||
struct InputByte {
|
||||
byte: Option<u8>,
|
||||
|
@ -115,6 +111,31 @@ pub struct Serial {
|
|||
clock_inc: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SerialSaveState {
|
||||
byte: u8,
|
||||
output_byte: u8,
|
||||
input_byte: InputByte,
|
||||
bits_remaining: u8,
|
||||
control: SerialControl,
|
||||
#[cfg(feature = "clocked-serial")]
|
||||
clock_inc: usize,
|
||||
}
|
||||
|
||||
impl SerialSaveState {
|
||||
pub fn create(serial: &Serial) -> Self {
|
||||
Self {
|
||||
byte: serial.byte,
|
||||
output_byte: serial.output_byte,
|
||||
input_byte: serial.input_byte,
|
||||
bits_remaining: serial.bits_remaining,
|
||||
control: serial.control,
|
||||
#[cfg(feature = "clocked-serial")]
|
||||
clock_inc: serial.clock_inc,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serial {
|
||||
pub fn new(target: SerialTarget) -> Self {
|
||||
Self {
|
||||
|
@ -129,6 +150,19 @@ impl Serial {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: SerialSaveState, target: SerialTarget) -> Self {
|
||||
Self {
|
||||
byte: state.byte,
|
||||
output_byte: state.output_byte,
|
||||
input_byte: state.input_byte,
|
||||
bits_remaining: state.bits_remaining,
|
||||
control: state.control,
|
||||
target,
|
||||
#[cfg(feature = "clocked-serial")]
|
||||
clock_inc: state.clock_inc,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_connected(&self) -> bool {
|
||||
!matches!(&self.target, SerialTarget::None)
|
||||
}
|
||||
|
@ -168,16 +202,14 @@ impl Serial {
|
|||
self.control.transfer_in_progress = false;
|
||||
will_interrupt = true;
|
||||
match &self.target {
|
||||
SerialTarget::Stdout(stdout_type) => {
|
||||
match stdout_type {
|
||||
StdoutType::Ascii => print!("{}", self.output_byte as char),
|
||||
StdoutType::Hex => print!("{:0>2X} ", self.output_byte),
|
||||
}
|
||||
stdout().flush().unwrap();
|
||||
SerialTarget::Stdout => {
|
||||
print!("{}", self.output_byte as char);
|
||||
stdout().flush().expect("Serial: error sending to stdout");
|
||||
}
|
||||
SerialTarget::Custom { rx: _, tx } => {
|
||||
if let Some(tx) = tx {
|
||||
tx.send(self.output_byte).unwrap();
|
||||
tx.send(self.output_byte)
|
||||
.expect("Serial: error sending to custom tx");
|
||||
}
|
||||
}
|
||||
SerialTarget::None => {}
|
||||
|
|
|
@ -74,6 +74,10 @@ pub struct Timer {
|
|||
tima_counter: usize,
|
||||
}
|
||||
|
||||
// this will need to change when cgb mode is implemented
|
||||
// as it uses bit 5 in double speed mode
|
||||
const AUDIO_BIT: u8 = 4;
|
||||
|
||||
impl Timer {
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
|
@ -86,27 +90,24 @@ impl Timer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self, steps: usize, with_div: bool, double_speed: bool) -> TimerReturn {
|
||||
pub fn tick(&mut self, steps: usize) -> TimerReturn {
|
||||
self.div_counter += steps;
|
||||
let mut div_diff = (self.div_counter / 256) as u8;
|
||||
let mut last_div = self.div;
|
||||
let mut returning = TimerReturn::default();
|
||||
if with_div {
|
||||
let audio_bit = if double_speed { 5 } else { 4 };
|
||||
self.div_counter += steps;
|
||||
let mut div_diff = (self.div_counter / 256) as u8;
|
||||
let mut last_div = self.div;
|
||||
while div_diff > 0 {
|
||||
let div = last_div.wrapping_add(1);
|
||||
while div_diff > 0 {
|
||||
let div = last_div.wrapping_add(1);
|
||||
|
||||
if (div & (1 << audio_bit)) < (last_div & (1 << audio_bit)) {
|
||||
// trigger DIV-APU
|
||||
returning.num_apu_ticks += 1;
|
||||
}
|
||||
|
||||
self.div = div;
|
||||
last_div = div;
|
||||
div_diff -= 1;
|
||||
if (div & (1 << AUDIO_BIT)) < (last_div & (1 << AUDIO_BIT)) {
|
||||
// trigger DIV-APU
|
||||
returning.num_apu_ticks += 1;
|
||||
}
|
||||
self.div_counter %= 256;
|
||||
|
||||
self.div = div;
|
||||
last_div = div;
|
||||
div_diff -= 1;
|
||||
}
|
||||
self.div_counter %= 256;
|
||||
|
||||
if self.control.enable {
|
||||
self.tima_counter += steps;
|
||||
|
|
|
@ -1,199 +1,192 @@
|
|||
use crate::error::RomHeaderError;
|
||||
|
||||
use self::{
|
||||
licensee::LicenseeCode,
|
||||
mbcs::{Mbc, Mbc1, Mbc2, Mbc3, Mbc5, None, KB, ROM_BANK_SIZE},
|
||||
sram_save::SaveDataLocation,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::from_utf8;
|
||||
|
||||
use super::addresses::{CartRamAddress, RomAddress};
|
||||
use crate::{
|
||||
connect::{CameraWrapperRef, PocketCamera as PocketCameraTrait},
|
||||
processor::memory::Address,
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
str::from_utf8,
|
||||
};
|
||||
|
||||
use self::mbcs::{
|
||||
Mbc, Mbc1, Mbc1SaveState, Mbc2, Mbc2SaveState, Mbc3, Mbc3SaveState, Mbc5, Mbc5SaveState, None,
|
||||
PocketCamera, PocketCameraSaveState,
|
||||
};
|
||||
|
||||
pub(crate) mod licensee;
|
||||
mod mbcs;
|
||||
pub mod sram_save;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub enum CgbRomType {
|
||||
Dmg,
|
||||
CgbOptional,
|
||||
CgbOnly,
|
||||
struct MaybeBufferedSram {
|
||||
buf: Vec<u8>,
|
||||
length: usize,
|
||||
inner: Option<File>,
|
||||
unbuffered_writes: usize,
|
||||
}
|
||||
|
||||
pub struct Rom {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SramSaveState {
|
||||
buf: Vec<u8>,
|
||||
length: usize,
|
||||
}
|
||||
|
||||
impl SramSaveState {
|
||||
pub fn create(sram: &MaybeBufferedSram) -> Self {
|
||||
Self {
|
||||
buf: sram.buf.clone(),
|
||||
length: sram.length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NUM_WRITES_TO_FLUSH: usize = 256;
|
||||
|
||||
impl MaybeBufferedSram {
|
||||
fn new(path: Option<PathBuf>, length: usize) -> Self {
|
||||
let mut buf = vec![];
|
||||
let inner = if let Some(path) = path {
|
||||
if path.exists() {
|
||||
let mut writer = OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
writer.read_to_end(&mut buf).unwrap();
|
||||
Some(writer)
|
||||
} else {
|
||||
buf.resize(8 * mbcs::KB, 0);
|
||||
let writer = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
Some(writer)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
buf,
|
||||
length,
|
||||
inner,
|
||||
unbuffered_writes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_save_state(state: SramSaveState) -> Self {
|
||||
// TODO - restore file path
|
||||
Self {
|
||||
buf: state.buf,
|
||||
length: state.length,
|
||||
inner: None,
|
||||
unbuffered_writes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
fn get(&self, addr: usize) -> u8 {
|
||||
if addr >= self.buf.len() {
|
||||
0
|
||||
} else {
|
||||
self.buf[addr]
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, addr: usize, data: u8) {
|
||||
self.unbuffered_writes += 1;
|
||||
while addr >= self.buf.len() {
|
||||
self.buf.resize(self.buf.len() + (8 * mbcs::KB), 0);
|
||||
}
|
||||
self.buf[addr] = data;
|
||||
if self.unbuffered_writes >= NUM_WRITES_TO_FLUSH {
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if let Some(ref mut writer) = self.inner {
|
||||
writer.seek(SeekFrom::Start(0)).unwrap();
|
||||
writer.set_len(self.buf.len() as u64).unwrap();
|
||||
writer.write_all(&self.buf).unwrap();
|
||||
self.unbuffered_writes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MaybeBufferedSram {
|
||||
fn drop(&mut self) {
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rom<C>
|
||||
where
|
||||
C: PocketCameraTrait,
|
||||
{
|
||||
title: String,
|
||||
mbc: Box<dyn Mbc>,
|
||||
pub rom_type: CgbRomType,
|
||||
spooky: PhantomData<C>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RomHeader {
|
||||
pub title: String,
|
||||
pub console_type: CgbRomType,
|
||||
pub licensee_code: LicenseeCode,
|
||||
pub sgb_flag: bool,
|
||||
pub cartridge_type: CartridgeType,
|
||||
pub rom_size: RomSize,
|
||||
pub ram_size: Option<RamSize>,
|
||||
pub mask_rom_version: u8,
|
||||
pub header_checksum: u8,
|
||||
pub cartridge_checksum: u16,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RomSaveState {
|
||||
title: String,
|
||||
mbc: MbcSaveState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CartridgeType {
|
||||
NoMapper,
|
||||
Mbc1 { battery: bool },
|
||||
Mbc2 { battery: bool },
|
||||
Mmm01 { battery: bool },
|
||||
Mbc3 { timer: bool, battery: bool },
|
||||
Mbc5 { battery: bool, rumble: bool },
|
||||
Mbc6,
|
||||
Mbc7,
|
||||
PocketCamera,
|
||||
Tama5,
|
||||
HuC3,
|
||||
HuC1,
|
||||
impl RomSaveState {
|
||||
pub fn create<C: PocketCameraTrait>(rom: &Rom<C>) -> Self {
|
||||
Self {
|
||||
title: rom.title.clone(),
|
||||
mbc: rom.mbc.get_save_state(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CartridgeType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum MbcSaveState {
|
||||
Mbc1(Mbc1SaveState),
|
||||
Mbc2(Mbc2SaveState),
|
||||
Mbc3(Mbc3SaveState),
|
||||
Mbc5(Mbc5SaveState),
|
||||
PocketCamera(PocketCameraSaveState),
|
||||
None,
|
||||
}
|
||||
|
||||
impl MbcSaveState {
|
||||
fn get_mbc<C: PocketCameraTrait + Send + 'static>(
|
||||
self,
|
||||
data: Vec<u8>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Box<dyn Mbc> {
|
||||
match self {
|
||||
CartridgeType::NoMapper => write!(f, "No mapper"),
|
||||
CartridgeType::Mbc1 { battery } => {
|
||||
write!(f, "MBC1{}", if *battery { " (battery)" } else { "" })
|
||||
MbcSaveState::Mbc1(state) => Box::new(Mbc1::from_save_state(state, data)),
|
||||
MbcSaveState::Mbc2(state) => Box::new(Mbc2::from_save_state(state, data)),
|
||||
MbcSaveState::Mbc3(state) => Box::new(Mbc3::from_save_state(state, data)),
|
||||
MbcSaveState::Mbc5(state) => Box::new(Mbc5::from_save_state(state, data)),
|
||||
MbcSaveState::None => Box::new(None::init(data)),
|
||||
MbcSaveState::PocketCamera(state) => {
|
||||
Box::new(PocketCamera::from_save_state(state, data, camera))
|
||||
}
|
||||
CartridgeType::Mbc2 { battery } => {
|
||||
write!(f, "MBC2{}", if *battery { " (battery)" } else { "" })
|
||||
}
|
||||
CartridgeType::Mmm01 { battery } => {
|
||||
write!(f, "MMM01{}", if *battery { " (battery)" } else { "" })
|
||||
}
|
||||
CartridgeType::Mbc3 {
|
||||
timer: false,
|
||||
battery: false,
|
||||
} => write!(f, "MBC3"),
|
||||
CartridgeType::Mbc3 { timer, battery } => write!(
|
||||
f,
|
||||
"MBC3 ({}{}{})",
|
||||
if *battery { "battery" } else { "" },
|
||||
if *battery && *timer { " + " } else { "" },
|
||||
if *timer { "RTC" } else { "" }
|
||||
),
|
||||
CartridgeType::Mbc5 {
|
||||
battery: false,
|
||||
rumble: false,
|
||||
} => write!(f, "MBC5"),
|
||||
CartridgeType::Mbc5 { battery, rumble } => write!(
|
||||
f,
|
||||
"MBC5 ({}{}{})",
|
||||
if *battery { "battery" } else { "" },
|
||||
if *battery && *rumble { " + " } else { "" },
|
||||
if *rumble { "Rumble" } else { "" }
|
||||
),
|
||||
CartridgeType::Mbc6 => write!(f, "MBC6"),
|
||||
CartridgeType::Mbc7 => write!(f, "MBC7"),
|
||||
CartridgeType::PocketCamera => write!(f, "Pocket Camera"),
|
||||
CartridgeType::Tama5 => write!(f, "Tama5"),
|
||||
CartridgeType::HuC3 => write!(f, "HuC3"),
|
||||
CartridgeType::HuC1 => write!(f, "HuC1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RomSize {
|
||||
B2,
|
||||
B4,
|
||||
B8,
|
||||
B16,
|
||||
B32,
|
||||
B64,
|
||||
B128,
|
||||
B256,
|
||||
B512,
|
||||
B72,
|
||||
B80,
|
||||
B96,
|
||||
}
|
||||
|
||||
impl RomSize {
|
||||
pub fn from(val: u8) -> Option<Self> {
|
||||
match val {
|
||||
0x00 => Some(Self::B2),
|
||||
0x01 => Some(Self::B4),
|
||||
0x02 => Some(Self::B8),
|
||||
0x03 => Some(Self::B16),
|
||||
0x04 => Some(Self::B32),
|
||||
0x05 => Some(Self::B64),
|
||||
0x06 => Some(Self::B128),
|
||||
0x07 => Some(Self::B256),
|
||||
0x08 => Some(Self::B512),
|
||||
0x52 => Some(Self::B72),
|
||||
0x53 => Some(Self::B80),
|
||||
0x54 => Some(Self::B96),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size_bytes(&self) -> usize {
|
||||
(match self {
|
||||
RomSize::B2 => 2,
|
||||
RomSize::B4 => 4,
|
||||
RomSize::B8 => 8,
|
||||
RomSize::B16 => 16,
|
||||
RomSize::B32 => 32,
|
||||
RomSize::B64 => 64,
|
||||
RomSize::B128 => 128,
|
||||
RomSize::B256 => 256,
|
||||
RomSize::B512 => 512,
|
||||
RomSize::B72 => 72,
|
||||
RomSize::B80 => 80,
|
||||
RomSize::B96 => 96,
|
||||
}) * ROM_BANK_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RamSize {
|
||||
B2,
|
||||
B8,
|
||||
B32,
|
||||
B64,
|
||||
B128,
|
||||
}
|
||||
|
||||
impl RamSize {
|
||||
pub fn from(val: u8) -> Result<Option<Self>, RomHeaderError> {
|
||||
match val {
|
||||
0x00 => Ok(None),
|
||||
0x01 => Ok(Some(Self::B2)),
|
||||
0x02 => Ok(Some(Self::B8)),
|
||||
0x03 => Ok(Some(Self::B32)),
|
||||
0x04 => Ok(Some(Self::B128)),
|
||||
0x05 => Ok(Some(Self::B64)),
|
||||
_ => Err(RomHeaderError::InvalidRamSize),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size_bytes(&self) -> usize {
|
||||
match self {
|
||||
RamSize::B2 => 2 * KB,
|
||||
RamSize::B8 => 8 * KB,
|
||||
RamSize::B32 => 32 * KB,
|
||||
RamSize::B64 => 64 * KB,
|
||||
RamSize::B128 => 128 * KB,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RomHeader {
|
||||
pub fn parse(data: &[u8]) -> Result<Self, RomHeaderError> {
|
||||
if data.len() < 0x150 {
|
||||
return Err(RomHeaderError::SliceLength);
|
||||
}
|
||||
|
||||
impl<C> Rom<C>
|
||||
where
|
||||
C: PocketCameraTrait + Send + 'static,
|
||||
{
|
||||
pub(crate) fn load(
|
||||
data: Vec<u8>,
|
||||
save_path: Option<PathBuf>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
let mut title_length = 0x143;
|
||||
for (i, val) in data.iter().enumerate().take(0x143).skip(0x134) {
|
||||
title_length = i;
|
||||
|
@ -201,172 +194,54 @@ impl RomHeader {
|
|||
break;
|
||||
}
|
||||
}
|
||||
let title = from_utf8(&data[0x134..title_length])?.to_string();
|
||||
let title = from_utf8(&data[0x134..title_length])
|
||||
.expect("Error parsing title")
|
||||
.to_string();
|
||||
|
||||
let console_type = match data[0x143] {
|
||||
0x80 => CgbRomType::CgbOptional,
|
||||
0xC0 => CgbRomType::CgbOnly,
|
||||
_ => CgbRomType::Dmg,
|
||||
let _gbc_flag = data[0x143];
|
||||
|
||||
let _sgb_flag = data[0x146];
|
||||
let rom_size = data[0x148];
|
||||
let ram_size = data[0x149];
|
||||
let mbc: Box<dyn Mbc> = match data[0x147] {
|
||||
0x00 => Box::new(None::init(data)),
|
||||
0x01 => Box::new(Mbc1::init(data, rom_size, 0, None)),
|
||||
0x02 => Box::new(Mbc1::init(data, rom_size, ram_size, None)),
|
||||
0x03 => Box::new(Mbc1::init(data, rom_size, ram_size, save_path)),
|
||||
0x05 => Box::new(Mbc2::init(data, rom_size, None)),
|
||||
0x06 => Box::new(Mbc2::init(data, rom_size, save_path)),
|
||||
0x0F => Box::new(Mbc3::init(data, rom_size, 0, true, save_path)),
|
||||
0x10 => Box::new(Mbc3::init(data, rom_size, ram_size, true, save_path)),
|
||||
0x11 => Box::new(Mbc3::init(data, rom_size, 0, false, None)),
|
||||
0x12 => Box::new(Mbc3::init(data, rom_size, ram_size, false, None)),
|
||||
0x13 => Box::new(Mbc3::init(data, rom_size, ram_size, false, save_path)),
|
||||
0x19 => Box::new(Mbc5::init(data, rom_size, 0, false, None)),
|
||||
0x1A => Box::new(Mbc5::init(data, rom_size, ram_size, false, None)),
|
||||
0x1B => Box::new(Mbc5::init(data, rom_size, ram_size, false, save_path)),
|
||||
0x1C => Box::new(Mbc5::init(data, rom_size, 0, true, None)),
|
||||
0x1D => Box::new(Mbc5::init(data, rom_size, ram_size, true, None)),
|
||||
0x1E => Box::new(Mbc5::init(data, rom_size, ram_size, true, save_path)),
|
||||
0xFC => Box::new(PocketCamera::init(
|
||||
data, rom_size, ram_size, save_path, camera,
|
||||
)),
|
||||
_ => panic!("unimplemented mbc: {:#X}", data[0x147]),
|
||||
};
|
||||
|
||||
let licensee_code = LicenseeCode::from_header(data[0x14B], [data[0x144], data[0x145]]);
|
||||
|
||||
let sgb_flag = data[0x146] == 0x03;
|
||||
let rom_size = RomSize::from(data[0x148]).ok_or(RomHeaderError::InvalidRomSize)?;
|
||||
let mut ram_size = RamSize::from(data[0x149])?;
|
||||
|
||||
let cartridge_type = match data[0x147] {
|
||||
0x00 => {
|
||||
ram_size = None;
|
||||
CartridgeType::NoMapper
|
||||
}
|
||||
0x01 => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc1 { battery: false }
|
||||
}
|
||||
0x02 => CartridgeType::Mbc1 { battery: false },
|
||||
0x03 => CartridgeType::Mbc1 { battery: true },
|
||||
0x05 => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc2 { battery: false }
|
||||
}
|
||||
0x06 => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc2 { battery: true }
|
||||
}
|
||||
0x0B => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mmm01 { battery: false }
|
||||
}
|
||||
0x0C => CartridgeType::Mmm01 { battery: false },
|
||||
0x0D => CartridgeType::Mmm01 { battery: true },
|
||||
0x0F => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc3 {
|
||||
timer: true,
|
||||
battery: true,
|
||||
}
|
||||
}
|
||||
0x10 => CartridgeType::Mbc3 {
|
||||
timer: true,
|
||||
battery: true,
|
||||
},
|
||||
0x11 => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc3 {
|
||||
timer: false,
|
||||
battery: false,
|
||||
}
|
||||
}
|
||||
0x12 => CartridgeType::Mbc3 {
|
||||
timer: false,
|
||||
battery: false,
|
||||
},
|
||||
0x13 => CartridgeType::Mbc3 {
|
||||
timer: false,
|
||||
battery: true,
|
||||
},
|
||||
0x19 => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc5 {
|
||||
battery: false,
|
||||
rumble: false,
|
||||
}
|
||||
}
|
||||
0x1A => CartridgeType::Mbc5 {
|
||||
battery: false,
|
||||
rumble: false,
|
||||
},
|
||||
0x1B => CartridgeType::Mbc5 {
|
||||
battery: true,
|
||||
rumble: false,
|
||||
},
|
||||
0x1C => {
|
||||
ram_size = None;
|
||||
CartridgeType::Mbc5 {
|
||||
battery: false,
|
||||
rumble: true,
|
||||
}
|
||||
}
|
||||
0x1D => CartridgeType::Mbc5 {
|
||||
battery: false,
|
||||
rumble: true,
|
||||
},
|
||||
0x1E => CartridgeType::Mbc5 {
|
||||
battery: true,
|
||||
rumble: true,
|
||||
},
|
||||
0x20 => CartridgeType::Mbc6,
|
||||
0x22 => CartridgeType::Mbc7,
|
||||
0xFC => CartridgeType::PocketCamera,
|
||||
0xFD => CartridgeType::Tama5,
|
||||
0xFE => CartridgeType::HuC3,
|
||||
0xFF => CartridgeType::HuC1,
|
||||
_ => return Err(RomHeaderError::InvalidMBC),
|
||||
};
|
||||
|
||||
let mask_rom_version = data[0x14C];
|
||||
let header_checksum = data[0x14D];
|
||||
let cartridge_checksum = u16::from_be_bytes([data[0x14E], data[0x14F]]);
|
||||
|
||||
Ok(RomHeader {
|
||||
title,
|
||||
console_type,
|
||||
licensee_code,
|
||||
sgb_flag,
|
||||
cartridge_type,
|
||||
rom_size,
|
||||
ram_size,
|
||||
mask_rom_version,
|
||||
header_checksum,
|
||||
cartridge_checksum,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Rom {
|
||||
pub(crate) fn load(data: Vec<u8>, sram_location: Option<SaveDataLocation>) -> Self {
|
||||
let header_data = RomHeader::parse(&data).unwrap();
|
||||
|
||||
let rom_type = get_cgb_rom_type(data[0x143]);
|
||||
|
||||
let mbc: Box<dyn Mbc> = match header_data.cartridge_type {
|
||||
CartridgeType::NoMapper => Box::new(None::init(data)),
|
||||
CartridgeType::Mbc1 { battery } => Box::new(Mbc1::init(
|
||||
data,
|
||||
header_data.rom_size,
|
||||
header_data.ram_size,
|
||||
if battery { sram_location } else { None },
|
||||
)),
|
||||
CartridgeType::Mbc2 { battery } => Box::new(Mbc2::init(
|
||||
data,
|
||||
header_data.rom_size,
|
||||
if battery { sram_location } else { None },
|
||||
)),
|
||||
CartridgeType::Mbc3 { timer, battery } => Box::new(Mbc3::init(
|
||||
data,
|
||||
header_data.rom_size,
|
||||
header_data.ram_size,
|
||||
timer,
|
||||
if battery { sram_location } else { None },
|
||||
)),
|
||||
CartridgeType::Mbc5 { battery, rumble } => Box::new(Mbc5::init(
|
||||
data,
|
||||
header_data.rom_size,
|
||||
header_data.ram_size,
|
||||
rumble,
|
||||
if battery { sram_location } else { None },
|
||||
)),
|
||||
_ => todo!(
|
||||
"mapper {:?} not implemented yet!",
|
||||
header_data.cartridge_type
|
||||
),
|
||||
};
|
||||
|
||||
Self {
|
||||
title: header_data.title,
|
||||
title,
|
||||
mbc,
|
||||
rom_type,
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_save_state(
|
||||
state: RomSaveState,
|
||||
data: Vec<u8>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: state.title,
|
||||
mbc: state.mbc.get_mbc(data, camera),
|
||||
spooky: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -374,19 +249,19 @@ impl Rom {
|
|||
&self.title
|
||||
}
|
||||
|
||||
pub(super) fn get(&self, address: RomAddress) -> u8 {
|
||||
pub(super) fn get(&self, address: Address) -> u8 {
|
||||
self.mbc.get(address)
|
||||
}
|
||||
|
||||
pub(super) fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
pub(super) fn get_ram(&self, address: Address) -> u8 {
|
||||
self.mbc.get_ram(address)
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, address: RomAddress, data: u8) {
|
||||
pub(super) fn set(&mut self, address: Address, data: u8) {
|
||||
self.mbc.set(address, data);
|
||||
}
|
||||
|
||||
pub(super) fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
pub(super) fn set_ram(&mut self, address: Address, data: u8) {
|
||||
self.mbc.set_ram(address, data);
|
||||
}
|
||||
|
||||
|
@ -407,10 +282,43 @@ impl Rom {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_cgb_rom_type(data: u8) -> CgbRomType {
|
||||
match data {
|
||||
0x80 => CgbRomType::CgbOptional,
|
||||
0xC0 => CgbRomType::CgbOnly,
|
||||
_ => CgbRomType::Dmg,
|
||||
const CHECKSUM_TABLE: [u8; 79] = [
|
||||
0x00, 0x88, 0x16, 0x36, 0xD1, 0xDB, 0xF2, 0x3C, 0x8C, 0x92, 0x3D, 0x5C, 0x58, 0xC9, 0x3E, 0x70,
|
||||
0x1D, 0x59, 0x69, 0x19, 0x35, 0xA8, 0x14, 0xAA, 0x75, 0x95, 0x99, 0x34, 0x6F, 0x15, 0xFF, 0x97,
|
||||
0x4B, 0x90, 0x17, 0x10, 0x39, 0xF7, 0xF6, 0xA2, 0x49, 0x4E, 0x43, 0x68, 0xE0, 0x8B, 0xF0, 0xCE,
|
||||
0x0C, 0x29, 0xE8, 0xB7, 0x86, 0x9A, 0x52, 0x01, 0x9D, 0x71, 0x9C, 0xBD, 0x5D, 0x6D, 0x67, 0x3F,
|
||||
0x6B, 0xB3, 0x46, 0x28, 0xA5, 0xC6, 0xD3, 0x27, 0x61, 0x18, 0x66, 0x6A, 0xBF, 0x0D, 0xF4,
|
||||
];
|
||||
|
||||
const TIEBREAKER_TABLE: [u8; 29] = [
|
||||
0x42, 0x45, 0x46, 0x41, 0x41, 0x52, 0x42, 0x45, 0x4B, 0x45, 0x4B, 0x20, 0x52, 0x2D, 0x55, 0x52,
|
||||
0x41, 0x52, 0x20, 0x49, 0x4E, 0x41, 0x49, 0x4C, 0x49, 0x43, 0x45, 0x20, 0x52,
|
||||
];
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn get_cgb_compat_palette(data: &[u8]) {
|
||||
if data[0x14B] == 0x01 || (data[0x14B] == 0x33 && data[0x144] == 0x30 && data[0x145] == 0x31) {
|
||||
let checksum = data
|
||||
.iter()
|
||||
.take(0x143)
|
||||
.skip(0x134)
|
||||
.fold(0_u8, |acc, val| acc.wrapping_add(*val));
|
||||
|
||||
let index = CHECKSUM_TABLE
|
||||
.iter()
|
||||
.position(|v| *v == checksum)
|
||||
.unwrap_or(0);
|
||||
if index <= 64 {
|
||||
println!("checksum: {checksum:#X}, index: {index:#X}");
|
||||
} else {
|
||||
let fourth = data[0x137];
|
||||
let tiebreaker = TIEBREAKER_TABLE
|
||||
.iter()
|
||||
.position(|v| *v == fourth)
|
||||
.unwrap_or(0);
|
||||
println!("checksum: {checksum:#X}, index: {index:#X}, fourth: {fourth:#X}, tiebreaker: {tiebreaker:#X}");
|
||||
}
|
||||
} else {
|
||||
// zero
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,351 +0,0 @@
|
|||
#[derive(Debug, Clone)]
|
||||
pub enum LicenseeCode {
|
||||
None,
|
||||
Nintendo,
|
||||
Capcom,
|
||||
HotB,
|
||||
Jaleco,
|
||||
Coconuts,
|
||||
EliteSystems,
|
||||
ElectronicArts,
|
||||
Hudsonsoft,
|
||||
ItcEntertainment,
|
||||
Yanoman,
|
||||
Clary,
|
||||
Virgin,
|
||||
PcmComplete,
|
||||
SanX,
|
||||
KotobukiSystems,
|
||||
Seta,
|
||||
Infogrames,
|
||||
Bandai,
|
||||
Konami,
|
||||
Hector,
|
||||
Banpresto,
|
||||
EntertainmentI,
|
||||
Gremlin,
|
||||
Ubisoft,
|
||||
Atlus,
|
||||
Malibu,
|
||||
Angel,
|
||||
SpectrumHoloby,
|
||||
Irem,
|
||||
USGold,
|
||||
Absolute,
|
||||
Acclaim,
|
||||
Activision,
|
||||
AmericanSammy,
|
||||
Gametek,
|
||||
ParkPlace,
|
||||
Ljn,
|
||||
Matchbox,
|
||||
MiltonBradley,
|
||||
Mindscape,
|
||||
Romstar,
|
||||
NaxatSoft,
|
||||
Tradewest,
|
||||
Titus,
|
||||
Ocean,
|
||||
ElectroBrain,
|
||||
Interplay,
|
||||
Broderbund,
|
||||
SculpturedSoft,
|
||||
TheSalesCurve,
|
||||
Thq,
|
||||
Accolade,
|
||||
TriffixEntertainment,
|
||||
Microprose,
|
||||
Kemco,
|
||||
MisawaEntertainment,
|
||||
Lozc,
|
||||
TokumaShotenIntermedia,
|
||||
BulletProofSoftware,
|
||||
VicTokai,
|
||||
Ape,
|
||||
IMax,
|
||||
ChunSoft,
|
||||
VideoSystem,
|
||||
Tsuburava,
|
||||
Varie,
|
||||
YonezawaSpal,
|
||||
Kaneko,
|
||||
Arc,
|
||||
NihonBussan,
|
||||
Tecmo,
|
||||
Imagineer,
|
||||
Nova,
|
||||
HoriElectric,
|
||||
Kawada,
|
||||
Takara,
|
||||
TechnosJapan,
|
||||
ToeiAnimation,
|
||||
Toho,
|
||||
Namco,
|
||||
AsciiNexoft,
|
||||
Enix,
|
||||
Hal,
|
||||
Snk,
|
||||
PonyCanyon,
|
||||
CultureBrain,
|
||||
Sunsoft,
|
||||
SonyImagesoft,
|
||||
Sammy,
|
||||
Taito,
|
||||
Squaresoft,
|
||||
DataEast,
|
||||
TonkinHouse,
|
||||
Koei,
|
||||
Ufl,
|
||||
Ultra,
|
||||
Vap,
|
||||
Use,
|
||||
Meldac,
|
||||
Sofel,
|
||||
Quest,
|
||||
SigmaEnterprises,
|
||||
AskKodansha,
|
||||
CopyaSystems,
|
||||
Tomy,
|
||||
Ncs,
|
||||
Human,
|
||||
Altron,
|
||||
Towachiki,
|
||||
Uutaka,
|
||||
Epoch,
|
||||
Athena,
|
||||
Asmik,
|
||||
Natsume,
|
||||
KingRecords,
|
||||
EpicSonyRecords,
|
||||
Igs,
|
||||
AWave,
|
||||
ExtremeEntertainment,
|
||||
BAi,
|
||||
Kss,
|
||||
Pow,
|
||||
Viacom,
|
||||
OceanAcclaim,
|
||||
HiTechEntertainment,
|
||||
Mattel,
|
||||
Lucasarts,
|
||||
Sci,
|
||||
TsukudaOri,
|
||||
PackInSoft,
|
||||
}
|
||||
|
||||
impl LicenseeCode {
|
||||
pub fn from_header(old_licensee_code: u8, new_code: [u8; 2]) -> Self {
|
||||
match old_licensee_code {
|
||||
0x00 => Self::None,
|
||||
0x01 => Self::Nintendo,
|
||||
0x08 => Self::Capcom,
|
||||
0x09 => Self::HotB,
|
||||
0x0A => Self::Jaleco,
|
||||
0x0B => Self::Coconuts,
|
||||
0x0C => Self::EliteSystems,
|
||||
0x13 => Self::ElectronicArts,
|
||||
0x18 => Self::Hudsonsoft,
|
||||
0x19 => Self::ItcEntertainment,
|
||||
0x1A => Self::Yanoman,
|
||||
0x1D => Self::Clary,
|
||||
0x1F => Self::Virgin,
|
||||
0x24 => Self::PcmComplete,
|
||||
0x25 => Self::SanX,
|
||||
0x28 => Self::KotobukiSystems,
|
||||
0x29 => Self::Seta,
|
||||
0x30 => Self::Infogrames,
|
||||
0x31 => Self::Nintendo,
|
||||
0x32 => Self::Bandai,
|
||||
0x33 => match &new_code {
|
||||
b"00" => Self::None,
|
||||
b"01" => Self::Nintendo,
|
||||
b"08" => Self::Capcom,
|
||||
b"13" => Self::ElectronicArts,
|
||||
b"18" => Self::Hudsonsoft,
|
||||
b"19" => Self::BAi,
|
||||
b"20" => Self::Kss,
|
||||
b"22" => Self::Pow,
|
||||
b"24" => Self::PcmComplete,
|
||||
b"25" => Self::SanX,
|
||||
b"28" => Self::Kemco,
|
||||
b"29" => Self::Seta,
|
||||
b"30" => Self::Viacom,
|
||||
b"31" => Self::Nintendo,
|
||||
b"32" => Self::Bandai,
|
||||
b"33" => Self::OceanAcclaim,
|
||||
b"34" => Self::Konami,
|
||||
b"35" => Self::Hector,
|
||||
b"37" => Self::Taito,
|
||||
b"38" => Self::Hudsonsoft,
|
||||
b"39" => Self::Banpresto,
|
||||
b"41" => Self::Ubisoft,
|
||||
b"42" => Self::Atlus,
|
||||
b"44" => Self::Malibu,
|
||||
b"46" => Self::Angel,
|
||||
b"47" => Self::BulletProofSoftware,
|
||||
b"49" => Self::Irem,
|
||||
b"50" => Self::Absolute,
|
||||
b"51" => Self::Acclaim,
|
||||
b"52" => Self::Activision,
|
||||
b"53" => Self::AmericanSammy,
|
||||
b"54" => Self::Konami,
|
||||
b"55" => Self::HiTechEntertainment,
|
||||
b"56" => Self::Ljn,
|
||||
b"57" => Self::Matchbox,
|
||||
b"58" => Self::Mattel,
|
||||
b"59" => Self::MiltonBradley,
|
||||
b"60" => Self::Titus,
|
||||
b"61" => Self::Virgin,
|
||||
b"64" => Self::Lucasarts,
|
||||
b"67" => Self::Ocean,
|
||||
b"69" => Self::ElectronicArts,
|
||||
b"70" => Self::Infogrames,
|
||||
b"71" => Self::Interplay,
|
||||
b"72" => Self::Broderbund,
|
||||
b"73" => Self::SculpturedSoft,
|
||||
b"75" => Self::Sci,
|
||||
b"78" => Self::Thq,
|
||||
b"79" => Self::Accolade,
|
||||
b"80" => Self::MisawaEntertainment,
|
||||
b"83" => Self::Lozc,
|
||||
b"86" => Self::TokumaShotenIntermedia,
|
||||
b"87" => Self::TsukudaOri,
|
||||
b"91" => Self::ChunSoft,
|
||||
b"92" => Self::VideoSystem,
|
||||
b"93" => Self::OceanAcclaim,
|
||||
b"95" => Self::Varie,
|
||||
b"96" => Self::YonezawaSpal,
|
||||
b"97" => Self::Kaneko,
|
||||
b"99" => Self::PackInSoft,
|
||||
_ => Self::None,
|
||||
},
|
||||
0x34 => Self::Konami,
|
||||
0x35 => Self::Hector,
|
||||
0x38 => Self::Capcom,
|
||||
0x39 => Self::Banpresto,
|
||||
0x3C => Self::EntertainmentI,
|
||||
0x3E => Self::Gremlin,
|
||||
0x41 => Self::Ubisoft,
|
||||
0x42 => Self::Atlus,
|
||||
0x44 => Self::Malibu,
|
||||
0x46 => Self::Angel,
|
||||
0x47 => Self::SpectrumHoloby,
|
||||
0x49 => Self::Irem,
|
||||
0x4A => Self::Virgin,
|
||||
0x4D => Self::Malibu,
|
||||
0x4F => Self::USGold,
|
||||
0x50 => Self::Absolute,
|
||||
0x51 => Self::Acclaim,
|
||||
0x52 => Self::Activision,
|
||||
0x53 => Self::AmericanSammy,
|
||||
0x54 => Self::Gametek,
|
||||
0x55 => Self::ParkPlace,
|
||||
0x56 => Self::Ljn,
|
||||
0x57 => Self::Matchbox,
|
||||
0x59 => Self::MiltonBradley,
|
||||
0x5A => Self::Mindscape,
|
||||
0x5B => Self::Romstar,
|
||||
0x5C => Self::NaxatSoft,
|
||||
0x5D => Self::Tradewest,
|
||||
0x60 => Self::Titus,
|
||||
0x61 => Self::Virgin,
|
||||
0x67 => Self::Ocean,
|
||||
0x69 => Self::ElectronicArts,
|
||||
0x6E => Self::EliteSystems,
|
||||
0x6F => Self::ElectroBrain,
|
||||
0x70 => Self::Infogrames,
|
||||
0x71 => Self::Interplay,
|
||||
0x72 => Self::Broderbund,
|
||||
0x73 => Self::SculpturedSoft,
|
||||
0x75 => Self::TheSalesCurve,
|
||||
0x78 => Self::Thq,
|
||||
0x79 => Self::Accolade,
|
||||
0x7A => Self::TriffixEntertainment,
|
||||
0x7C => Self::Microprose,
|
||||
0x7F => Self::Kemco,
|
||||
0x80 => Self::MisawaEntertainment,
|
||||
0x83 => Self::Lozc,
|
||||
0x86 => Self::TokumaShotenIntermedia,
|
||||
0x8B => Self::BulletProofSoftware,
|
||||
0x8C => Self::VicTokai,
|
||||
0x8E => Self::Ape,
|
||||
0x8F => Self::IMax,
|
||||
0x91 => Self::ChunSoft,
|
||||
0x92 => Self::VideoSystem,
|
||||
0x93 => Self::Tsuburava,
|
||||
0x95 => Self::Varie,
|
||||
0x96 => Self::YonezawaSpal,
|
||||
0x97 => Self::Kaneko,
|
||||
0x99 => Self::Arc,
|
||||
0x9A => Self::NihonBussan,
|
||||
0x9B => Self::Tecmo,
|
||||
0x9C => Self::Imagineer,
|
||||
0x9D => Self::Banpresto,
|
||||
0x9F => Self::Nova,
|
||||
0xA1 => Self::HoriElectric,
|
||||
0xA2 => Self::Bandai,
|
||||
0xA4 => Self::Konami,
|
||||
0xA6 => Self::Kawada,
|
||||
0xA7 => Self::Takara,
|
||||
0xA9 => Self::TechnosJapan,
|
||||
0xAA => Self::Broderbund,
|
||||
0xAC => Self::ToeiAnimation,
|
||||
0xAD => Self::Toho,
|
||||
0xAF => Self::Namco,
|
||||
0xB0 => Self::Acclaim,
|
||||
0xB1 => Self::AsciiNexoft,
|
||||
0xB2 => Self::Bandai,
|
||||
0xB4 => Self::Enix,
|
||||
0xB6 => Self::Hal,
|
||||
0xB7 => Self::Snk,
|
||||
0xB9 => Self::PonyCanyon,
|
||||
0xBA => Self::CultureBrain,
|
||||
0xBB => Self::Sunsoft,
|
||||
0xBD => Self::SonyImagesoft,
|
||||
0xBF => Self::Sammy,
|
||||
0xC0 => Self::Taito,
|
||||
0xC2 => Self::Kemco,
|
||||
0xC3 => Self::Squaresoft,
|
||||
0xC4 => Self::TokumaShotenIntermedia,
|
||||
0xC5 => Self::DataEast,
|
||||
0xC6 => Self::TonkinHouse,
|
||||
0xC8 => Self::Koei,
|
||||
0xC9 => Self::Ufl,
|
||||
0xCA => Self::Ultra,
|
||||
0xCB => Self::Vap,
|
||||
0xCC => Self::Use,
|
||||
0xCD => Self::Meldac,
|
||||
0xCE => Self::PonyCanyon,
|
||||
0xCF => Self::Angel,
|
||||
0xD0 => Self::Taito,
|
||||
0xD1 => Self::Sofel,
|
||||
0xD2 => Self::Quest,
|
||||
0xD3 => Self::SigmaEnterprises,
|
||||
0xD4 => Self::AskKodansha,
|
||||
0xD6 => Self::NaxatSoft,
|
||||
0xD7 => Self::CopyaSystems,
|
||||
0xD9 => Self::Banpresto,
|
||||
0xDA => Self::Tomy,
|
||||
0xDB => Self::Ljn,
|
||||
0xDD => Self::Ncs,
|
||||
0xDE => Self::Human,
|
||||
0xDF => Self::Altron,
|
||||
0xE0 => Self::Jaleco,
|
||||
0xE1 => Self::Towachiki,
|
||||
0xE2 => Self::Uutaka,
|
||||
0xE3 => Self::Varie,
|
||||
0xE5 => Self::Epoch,
|
||||
0xE7 => Self::Athena,
|
||||
0xE8 => Self::Asmik,
|
||||
0xE9 => Self::Natsume,
|
||||
0xEA => Self::KingRecords,
|
||||
0xEB => Self::Atlus,
|
||||
0xEC => Self::EpicSonyRecords,
|
||||
0xEE => Self::Igs,
|
||||
0xF0 => Self::AWave,
|
||||
0xF3 => Self::ExtremeEntertainment,
|
||||
0xFF => Self::Ljn,
|
||||
_ => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use crate::processor::memory::addresses::{CartRamAddress, RomAddress};
|
||||
use crate::processor::memory::Address;
|
||||
|
||||
mod mbc1;
|
||||
mod mbc2;
|
||||
|
@ -6,24 +6,28 @@ mod mbc3;
|
|||
mod mbc5;
|
||||
mod none;
|
||||
mod pocketcamera;
|
||||
pub use mbc1::Mbc1;
|
||||
pub use mbc2::Mbc2;
|
||||
pub use mbc3::Mbc3;
|
||||
pub use mbc5::Mbc5;
|
||||
pub use mbc1::{Mbc1, Mbc1SaveState};
|
||||
pub use mbc2::{Mbc2, Mbc2SaveState};
|
||||
pub use mbc3::{Mbc3, Mbc3SaveState};
|
||||
pub use mbc5::{Mbc5, Mbc5SaveState};
|
||||
pub use none::None;
|
||||
pub use pocketcamera::{PocketCamera, PocketCameraSaveState};
|
||||
|
||||
use super::MbcSaveState;
|
||||
|
||||
pub(super) const KB: usize = 1024;
|
||||
pub(super) const ROM_BANK_SIZE: usize = 16 * KB;
|
||||
pub(super) const RAM_BANK_SIZE: usize = 8 * KB;
|
||||
const ROM_BANK_SIZE: usize = 16 * KB;
|
||||
const RAM_BANK_SIZE: usize = 8 * KB;
|
||||
|
||||
pub(super) trait Mbc: Send {
|
||||
// addresses 0x0000 - 0x7FFF
|
||||
fn get(&self, address: RomAddress) -> u8;
|
||||
fn get(&self, address: Address) -> u8;
|
||||
// addresses 0xA000 - 0xBFFF
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8;
|
||||
fn set(&mut self, address: RomAddress, data: u8);
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8);
|
||||
fn get_ram(&self, address: Address) -> u8;
|
||||
fn set(&mut self, address: Address, data: u8);
|
||||
fn set_ram(&mut self, address: Address, data: u8);
|
||||
fn mbc_type(&self) -> String;
|
||||
fn get_save_state(&self) -> MbcSaveState;
|
||||
fn is_rumbling(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
use super::{Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use crate::processor::memory::{
|
||||
addresses::{AddressMarker, CartRamAddress, RomAddress},
|
||||
rom::{
|
||||
sram_save::{BufferedSramTrait, MaybeBufferedSram, SaveDataLocation},
|
||||
RamSize, RomSize,
|
||||
},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{ram_size_kb, rom_banks, Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use crate::processor::memory::{
|
||||
rom::{MaybeBufferedSram, MbcSaveState, SramSaveState},
|
||||
Address,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
enum BankingMode {
|
||||
Simple,
|
||||
|
@ -25,16 +25,22 @@ pub struct Mbc1 {
|
|||
bank_mode: BankingMode,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Mbc1SaveState {
|
||||
rom_len: usize,
|
||||
rom_bank: u8,
|
||||
ram_enabled: bool,
|
||||
ram: Option<SramSaveState>,
|
||||
ram_bank: u8,
|
||||
upper_banks: u8,
|
||||
bank_mode: BankingMode,
|
||||
}
|
||||
|
||||
impl Mbc1 {
|
||||
pub fn init(
|
||||
data: Vec<u8>,
|
||||
rom_size: RomSize,
|
||||
ram_size: Option<RamSize>,
|
||||
save_file: Option<SaveDataLocation>,
|
||||
) -> Self {
|
||||
let rom_len = rom_size.size_bytes();
|
||||
pub fn init(data: Vec<u8>, rom_size: u8, ram_size: u8, save_file: Option<PathBuf>) -> Self {
|
||||
let rom_len = rom_banks(rom_size) * ROM_BANK_SIZE;
|
||||
// in kb
|
||||
let ram = ram_size.map(|s| MaybeBufferedSram::new(save_file, s.size_bytes()));
|
||||
let ram = ram_size_kb(ram_size).map(|s| MaybeBufferedSram::new(save_file, s * KB));
|
||||
Self {
|
||||
data,
|
||||
rom_len,
|
||||
|
@ -47,63 +53,79 @@ impl Mbc1 {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_rom_addr(&self, address: RomAddress) -> usize {
|
||||
fn get_rom_addr(&self, address: Address) -> usize {
|
||||
(match address {
|
||||
RomAddress::Bank0(address) => match self.bank_mode {
|
||||
BankingMode::Simple => address.inner() as usize,
|
||||
0x0..0x4000 => match self.bank_mode {
|
||||
BankingMode::Simple => address as usize,
|
||||
BankingMode::Advanced => {
|
||||
(address.inner() as usize) + (self.upper_banks as usize * 512 * KB)
|
||||
(address as usize) + (self.upper_banks as usize * 512 * KB)
|
||||
}
|
||||
},
|
||||
RomAddress::MappedBank(address) => {
|
||||
(address.get_local() as usize)
|
||||
0x4000..0x8000 => {
|
||||
(address - 0x4000) as usize
|
||||
+ (ROM_BANK_SIZE * self.rom_bank as usize)
|
||||
+ (self.upper_banks as usize * 512 * KB)
|
||||
}
|
||||
|
||||
0xA000..0xC000 => panic!("passed ram address to rom address function"),
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
} % self.rom_len)
|
||||
}
|
||||
|
||||
fn get_ram_addr(&self, address: CartRamAddress) -> usize {
|
||||
match self.bank_mode {
|
||||
BankingMode::Simple => {
|
||||
(address.get_local() as usize) + (RAM_BANK_SIZE * self.ram_bank as usize)
|
||||
}
|
||||
BankingMode::Advanced => {
|
||||
(address.get_local() as usize)
|
||||
+ (RAM_BANK_SIZE * self.ram_bank as usize)
|
||||
+ (self.upper_banks as usize * 16 * KB)
|
||||
}
|
||||
fn get_ram_addr(&self, address: Address) -> usize {
|
||||
match address {
|
||||
0x0..0x8000 => panic!("passed rom address to ram address function"),
|
||||
0xA000..0xC000 => match self.bank_mode {
|
||||
BankingMode::Simple => {
|
||||
(address - 0xA000) as usize + (RAM_BANK_SIZE * self.ram_bank as usize)
|
||||
}
|
||||
BankingMode::Advanced => {
|
||||
(address - 0xA000) as usize
|
||||
+ (RAM_BANK_SIZE * self.ram_bank as usize)
|
||||
+ (self.upper_banks as usize * 16 * KB)
|
||||
}
|
||||
},
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: Mbc1SaveState, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
rom_len: state.rom_len,
|
||||
rom_bank: state.rom_bank,
|
||||
ram_enabled: state.ram_enabled,
|
||||
ram: state.ram.map(MaybeBufferedSram::from_save_state),
|
||||
ram_bank: state.ram_bank,
|
||||
upper_banks: state.upper_banks,
|
||||
bank_mode: state.bank_mode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mbc for Mbc1 {
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
self.data[self.get_rom_addr(address)]
|
||||
}
|
||||
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
if self.ram_enabled
|
||||
&& let Some(ram) = &self.ram
|
||||
{
|
||||
fn get_ram(&self, address: Address) -> u8 {
|
||||
if self.ram_enabled && let Some(ram) = &self.ram {
|
||||
let addr = self.get_ram_addr(address) % ram.len();
|
||||
return ram.get(addr);
|
||||
}
|
||||
0xFF
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
fn set_ram(&mut self, address: Address, data: u8) {
|
||||
let mut addr = self.get_ram_addr(address);
|
||||
if self.ram_enabled
|
||||
&& let Some(ram) = &mut self.ram
|
||||
{
|
||||
if self.ram_enabled && let Some(ram) = &mut self.ram {
|
||||
addr %= ram.len();
|
||||
ram.set(addr, data);
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, address: RomAddress, data: u8) {
|
||||
match address.inner() {
|
||||
fn set(&mut self, address: Address, data: u8) {
|
||||
match address {
|
||||
0x0..0x2000 => {
|
||||
// enable/disable ram
|
||||
self.ram_enabled = (data & 0x0F) == 0xA;
|
||||
|
@ -145,4 +167,16 @@ impl Mbc for Mbc1 {
|
|||
ram.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::Mbc1(Mbc1SaveState {
|
||||
rom_len: self.rom_len,
|
||||
rom_bank: self.rom_bank,
|
||||
ram_enabled: self.ram_enabled,
|
||||
ram: self.ram.as_ref().map(SramSaveState::create),
|
||||
ram_bank: self.ram_bank,
|
||||
upper_banks: self.upper_banks,
|
||||
bank_mode: self.bank_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
use super::Mbc;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::processor::memory::{
|
||||
addresses::{AddressMarker, CartRamAddress, RomAddress},
|
||||
rom::{
|
||||
sram_save::{BufferedSramTrait, MaybeBufferedSram, SaveDataLocation},
|
||||
RomSize,
|
||||
},
|
||||
rom::{MaybeBufferedSram, MbcSaveState, SramSaveState},
|
||||
Address,
|
||||
};
|
||||
|
||||
use super::{rom_banks, Mbc, ROM_BANK_SIZE};
|
||||
|
||||
pub struct Mbc2 {
|
||||
data: Vec<u8>,
|
||||
rom_len: usize,
|
||||
|
@ -15,9 +17,17 @@ pub struct Mbc2 {
|
|||
ram_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Mbc2SaveState {
|
||||
rom_len: usize,
|
||||
rom_bank: u8,
|
||||
ram: SramSaveState,
|
||||
ram_enabled: bool,
|
||||
}
|
||||
|
||||
impl Mbc2 {
|
||||
pub fn init(data: Vec<u8>, rom_size: RomSize, save_file: Option<SaveDataLocation>) -> Self {
|
||||
let rom_len = rom_size.size_bytes();
|
||||
pub fn init(data: Vec<u8>, rom_size: u8, save_file: Option<PathBuf>) -> Self {
|
||||
let rom_len = rom_banks(rom_size) * ROM_BANK_SIZE;
|
||||
let ram = MaybeBufferedSram::new(save_file, 512);
|
||||
|
||||
Self {
|
||||
|
@ -28,30 +38,41 @@ impl Mbc2 {
|
|||
ram_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: Mbc2SaveState, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
rom_len: state.rom_len,
|
||||
rom_bank: state.rom_bank,
|
||||
ram: MaybeBufferedSram::from_save_state(state.ram),
|
||||
ram_enabled: state.ram_enabled,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mbc for Mbc2 {
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
match address {
|
||||
RomAddress::Bank0(address) => self.data[address.inner() as usize],
|
||||
RomAddress::MappedBank(address) => {
|
||||
self.data[((address.get_local() as usize) + (0x4000 * self.rom_bank as usize))
|
||||
0x0..0x4000 => self.data[address as usize],
|
||||
0x4000..0x8000 => {
|
||||
self.data[((address as usize - 0x4000) + (0x4000 * self.rom_bank as usize))
|
||||
% self.rom_len]
|
||||
}
|
||||
_ => panic!("passed wrong address to mbc"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
fn get_ram(&self, address: Address) -> u8 {
|
||||
if self.ram_enabled {
|
||||
0xF0 | (0x0F & self.ram.get((address.get_local() as usize) % 512))
|
||||
0xF0 | (0x0F & self.ram.get((address - 0xA000) as usize % 512))
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, address: RomAddress, data: u8) {
|
||||
if let RomAddress::Bank0(_) = address {
|
||||
if address.inner() & (1 << 8) == (1 << 8) {
|
||||
fn set(&mut self, address: Address, data: u8) {
|
||||
if address < 0x4000 {
|
||||
if address & (1 << 8) == (1 << 8) {
|
||||
// bit 8 is set - rom bank
|
||||
self.rom_bank = data & 0xF;
|
||||
if self.rom_bank == 0 {
|
||||
|
@ -64,9 +85,9 @@ impl Mbc for Mbc2 {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
fn set_ram(&mut self, address: Address, data: u8) {
|
||||
if self.ram_enabled {
|
||||
self.ram.set((address.get_local() as usize) % 512, data);
|
||||
self.ram.set((address - 0xA000) as usize % 512, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,4 +98,13 @@ impl Mbc for Mbc2 {
|
|||
fn flush(&mut self) {
|
||||
self.ram.flush();
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::Mbc2(Mbc2SaveState {
|
||||
rom_len: self.rom_len,
|
||||
rom_bank: self.rom_bank,
|
||||
ram: SramSaveState::create(&self.ram),
|
||||
ram_enabled: self.ram_enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
use super::{Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{ram_size_kb, rom_banks, Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use crate::{
|
||||
processor::memory::{
|
||||
addresses::{AddressMarker, CartRamAddress, RomAddress},
|
||||
rom::{
|
||||
sram_save::{BufferedSramTrait, MaybeBufferedSram, SaveDataLocation},
|
||||
RamSize, RomSize,
|
||||
},
|
||||
rom::{MaybeBufferedSram, MbcSaveState, SramSaveState},
|
||||
Address,
|
||||
},
|
||||
util::set_or_clear_bit,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
enum RtcRegister {
|
||||
|
@ -95,50 +96,73 @@ pub struct Mbc3 {
|
|||
// TODO - save/load rtc!!
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Mbc3SaveState {
|
||||
rom_bank: u8,
|
||||
rom_size: usize,
|
||||
ram: Option<SramSaveState>,
|
||||
ram_bank: RamBank,
|
||||
ram_size: usize,
|
||||
ram_enabled: bool,
|
||||
}
|
||||
|
||||
impl Mbc3 {
|
||||
pub fn init(
|
||||
data: Vec<u8>,
|
||||
rom_size: RomSize,
|
||||
ram_size: Option<RamSize>,
|
||||
rom_size: u8,
|
||||
ram_size: u8,
|
||||
rtc: bool,
|
||||
save_file: Option<SaveDataLocation>,
|
||||
save_file: Option<PathBuf>,
|
||||
) -> Self {
|
||||
let ram = ram_size
|
||||
.as_ref()
|
||||
.map(|s| MaybeBufferedSram::new(save_file, s.size_bytes()));
|
||||
let ram = ram_size_kb(ram_size).map(|s| MaybeBufferedSram::new(save_file, s * KB));
|
||||
Self {
|
||||
data,
|
||||
rom_bank: 1,
|
||||
rom_size: rom_size.size_bytes(),
|
||||
rom_size: rom_banks(rom_size) * ROM_BANK_SIZE,
|
||||
ram,
|
||||
ram_bank: RamBank::Ram(0),
|
||||
ram_size: ram_size.map(|s| s.size_bytes()).unwrap_or(0),
|
||||
ram_size: ram_size_kb(ram_size).map_or(1, |s| s * KB),
|
||||
ram_enabled: false,
|
||||
rtc: if rtc { Some(Rtc::default()) } else { None },
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rom_addr(&self, address: RomAddress) -> usize {
|
||||
fn get_rom_addr(&self, address: Address) -> usize {
|
||||
(match address {
|
||||
RomAddress::Bank0(address) => address.inner() as usize,
|
||||
RomAddress::MappedBank(address) => {
|
||||
let internal_addr = address.get_local() as usize;
|
||||
0x0..0x4000 => address as usize,
|
||||
0x4000..0x8000 => {
|
||||
let internal_addr = address as usize - 0x4000;
|
||||
internal_addr + (ROM_BANK_SIZE * self.rom_bank as usize)
|
||||
}
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
} % self.rom_size)
|
||||
}
|
||||
|
||||
fn get_ram_addr(&self, address: CartRamAddress, ram_bank: usize) -> usize {
|
||||
((address.get_local() as usize) + (RAM_BANK_SIZE * ram_bank)) % self.ram_size
|
||||
fn get_ram_addr(&self, address: Address, ram_bank: usize) -> usize {
|
||||
((address as usize - 0xA000) + (RAM_BANK_SIZE * ram_bank)) % self.ram_size
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: Mbc3SaveState, data: Vec<u8>) -> Self {
|
||||
// TODO - FIX RTC!!!
|
||||
Self {
|
||||
data,
|
||||
rom_bank: state.rom_bank,
|
||||
rom_size: state.rom_size,
|
||||
ram: state.ram.map(MaybeBufferedSram::from_save_state),
|
||||
ram_bank: state.ram_bank,
|
||||
ram_size: state.ram_size,
|
||||
ram_enabled: state.ram_enabled,
|
||||
rtc: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mbc for Mbc3 {
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
self.data[self.get_rom_addr(address)]
|
||||
}
|
||||
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
fn get_ram(&self, address: Address) -> u8 {
|
||||
if self.ram_enabled {
|
||||
match &self.ram_bank {
|
||||
RamBank::Ram(ram_bank) => {
|
||||
|
@ -156,8 +180,8 @@ impl Mbc for Mbc3 {
|
|||
0xFF
|
||||
}
|
||||
|
||||
fn set(&mut self, address: RomAddress, data: u8) {
|
||||
match address.inner() {
|
||||
fn set(&mut self, address: Address, data: u8) {
|
||||
match address {
|
||||
0x0..0x2000 => {
|
||||
if data & 0xF == 0xA {
|
||||
self.ram_enabled = true;
|
||||
|
@ -187,14 +211,19 @@ impl Mbc for Mbc3 {
|
|||
rtc.latched_time = Some(Instant::now());
|
||||
}
|
||||
|
||||
rtc.latch_prepared = data == 0x00;
|
||||
if data == 0x00 {
|
||||
rtc.latch_prepared = true;
|
||||
} else {
|
||||
rtc.latch_prepared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => panic!("unsupported addr"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
fn set_ram(&mut self, address: Address, data: u8) {
|
||||
if self.ram_enabled {
|
||||
match &self.ram_bank {
|
||||
RamBank::Ram(ram_bank) => {
|
||||
|
@ -229,4 +258,15 @@ impl Mbc for Mbc3 {
|
|||
ram.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::Mbc3(Mbc3SaveState {
|
||||
rom_bank: self.rom_bank,
|
||||
rom_size: self.rom_size,
|
||||
ram: self.ram.as_ref().map(SramSaveState::create),
|
||||
ram_bank: self.ram_bank,
|
||||
ram_size: self.ram_size,
|
||||
ram_enabled: self.ram_enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
processor::memory::{
|
||||
addresses::{AddressMarker, CartRamAddress, RomAddress},
|
||||
rom::{
|
||||
sram_save::{BufferedSramTrait, MaybeBufferedSram, SaveDataLocation},
|
||||
RamSize, RomSize,
|
||||
},
|
||||
rom::{MaybeBufferedSram, MbcSaveState, SramSaveState},
|
||||
Address,
|
||||
},
|
||||
util::get_bit,
|
||||
};
|
||||
|
||||
use super::{Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use super::{ram_size_kb, rom_banks, Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
|
||||
pub struct Mbc5 {
|
||||
data: Vec<u8>,
|
||||
|
@ -23,64 +24,91 @@ pub struct Mbc5 {
|
|||
is_rumbling: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Mbc5SaveState {
|
||||
rom_bank: u16,
|
||||
rom_size: usize,
|
||||
ram: Option<SramSaveState>,
|
||||
ram_bank: u8,
|
||||
ram_size: usize,
|
||||
ram_enabled: bool,
|
||||
rumble: bool,
|
||||
is_rumbling: bool,
|
||||
}
|
||||
|
||||
impl Mbc5 {
|
||||
pub fn init(
|
||||
data: Vec<u8>,
|
||||
rom_size: RomSize,
|
||||
ram_size: Option<RamSize>,
|
||||
rom_size: u8,
|
||||
ram_size: u8,
|
||||
rumble: bool,
|
||||
save_file: Option<SaveDataLocation>,
|
||||
save_file: Option<PathBuf>,
|
||||
) -> Self {
|
||||
let ram = ram_size
|
||||
.as_ref()
|
||||
.map(|s| MaybeBufferedSram::new(save_file, s.size_bytes()));
|
||||
let ram = ram_size_kb(ram_size).map(|s| MaybeBufferedSram::new(save_file, s * KB));
|
||||
Self {
|
||||
data,
|
||||
rom_bank: 1,
|
||||
rom_size: rom_size.size_bytes(),
|
||||
rom_size: rom_banks(rom_size) * ROM_BANK_SIZE,
|
||||
ram,
|
||||
ram_bank: 0,
|
||||
ram_size: ram_size.map(|s| s.size_bytes()).unwrap_or(0),
|
||||
ram_size: ram_size_kb(ram_size).map_or(1, |s| s * KB),
|
||||
ram_enabled: false,
|
||||
rumble,
|
||||
is_rumbling: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_rom_addr(&self, address: RomAddress) -> usize {
|
||||
fn get_rom_addr(&self, address: Address) -> usize {
|
||||
(match address {
|
||||
RomAddress::Bank0(address) => address.inner() as usize,
|
||||
RomAddress::MappedBank(address) => {
|
||||
let internal_addr = address.get_local() as usize;
|
||||
0x0..0x4000 => address as usize,
|
||||
0x4000..0x8000 => {
|
||||
let internal_addr = address as usize - 0x4000;
|
||||
internal_addr + (ROM_BANK_SIZE * self.rom_bank as usize)
|
||||
}
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
} % self.rom_size)
|
||||
}
|
||||
|
||||
fn get_ram_addr(&self, address: CartRamAddress) -> usize {
|
||||
((address.get_local() as usize) + (RAM_BANK_SIZE * self.ram_bank as usize)) % self.ram_size
|
||||
fn get_ram_addr(&self, address: Address) -> usize {
|
||||
((address as usize - 0xA000) + (RAM_BANK_SIZE * self.ram_bank as usize)) % self.ram_size
|
||||
}
|
||||
|
||||
pub fn from_save_state(state: Mbc5SaveState, data: Vec<u8>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
rom_bank: state.rom_bank,
|
||||
rom_size: state.rom_size,
|
||||
ram: state.ram.map(MaybeBufferedSram::from_save_state),
|
||||
ram_bank: state.ram_bank,
|
||||
ram_size: state.ram_size,
|
||||
ram_enabled: state.ram_enabled,
|
||||
rumble: state.rumble,
|
||||
is_rumbling: state.is_rumbling,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mbc for Mbc5 {
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
self.data[self.get_rom_addr(address)]
|
||||
}
|
||||
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
if self.ram_enabled
|
||||
&& let Some(ram) = &self.ram
|
||||
{
|
||||
fn get_ram(&self, address: Address) -> u8 {
|
||||
if self.ram_enabled && let Some(ram) = &self.ram {
|
||||
ram.get(self.get_ram_addr(address))
|
||||
} else {
|
||||
0xFF
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, address: RomAddress, data: u8) {
|
||||
match address.inner() {
|
||||
fn set(&mut self, address: Address, data: u8) {
|
||||
match address {
|
||||
0x0..0x2000 => {
|
||||
self.ram_enabled = (data & 0xF) == 0xA;
|
||||
if (data & 0xF) == 0xA {
|
||||
self.ram_enabled = true
|
||||
} else {
|
||||
self.ram_enabled = false
|
||||
}
|
||||
}
|
||||
0x2000..0x3000 => self.rom_bank = (self.rom_bank & 0x100) | (data as u16),
|
||||
0x3000..0x4000 => self.rom_bank = (self.rom_bank & 0xFF) | ((data as u16 & 0b1) << 8),
|
||||
|
@ -93,15 +121,13 @@ impl Mbc for Mbc5 {
|
|||
}
|
||||
}
|
||||
0x6000..0x8000 => {}
|
||||
_ => panic!(),
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
fn set_ram(&mut self, address: Address, data: u8) {
|
||||
let real_addr = self.get_ram_addr(address);
|
||||
if self.ram_enabled
|
||||
&& let Some(ram) = &mut self.ram
|
||||
{
|
||||
if self.ram_enabled && let Some(ram) = &mut self.ram {
|
||||
ram.set(real_addr, data);
|
||||
}
|
||||
}
|
||||
|
@ -132,4 +158,17 @@ impl Mbc for Mbc5 {
|
|||
ram.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::Mbc5(Mbc5SaveState {
|
||||
rom_bank: self.rom_bank,
|
||||
rom_size: self.rom_size,
|
||||
ram: self.ram.as_ref().map(SramSaveState::create),
|
||||
ram_bank: self.ram_bank,
|
||||
ram_size: self.ram_size,
|
||||
ram_enabled: self.ram_enabled,
|
||||
rumble: self.rumble,
|
||||
is_rumbling: self.is_rumbling,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use super::Mbc;
|
||||
use crate::processor::memory::addresses::{AddressMarker, CartRamAddress, RomAddress};
|
||||
use crate::processor::memory::{rom::MbcSaveState, Address};
|
||||
|
||||
pub struct None {
|
||||
data: Vec<u8>,
|
||||
|
@ -12,19 +12,23 @@ impl None {
|
|||
}
|
||||
|
||||
impl Mbc for None {
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
self.data[address.inner() as usize]
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
self.data[address as usize]
|
||||
}
|
||||
|
||||
fn get_ram(&self, _address: CartRamAddress) -> u8 {
|
||||
fn get_ram(&self, _address: Address) -> u8 {
|
||||
0xFF
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, _address: CartRamAddress, _data: u8) {}
|
||||
fn set_ram(&mut self, _address: Address, _data: u8) {}
|
||||
|
||||
fn set(&mut self, _address: RomAddress, _data: u8) {}
|
||||
fn set(&mut self, _address: Address, _data: u8) {}
|
||||
|
||||
fn mbc_type(&self) -> String {
|
||||
String::from("None")
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use super::{ram_size_kb, rom_banks, Mbc, KB, RAM_BANK_SIZE, ROM_BANK_SIZE};
|
||||
use crate::{
|
||||
connect::{CameraWrapper, PocketCamera as PocketCameraTrait},
|
||||
connect::{CameraWrapperRef, PocketCamera as PocketCameraTrait},
|
||||
processor::memory::{
|
||||
addresses::{AddressMarker, CartRamAddress, RomAddress},
|
||||
rom::sram_save::{BufferedSramTrait, MaybeBufferedSram, SaveDataLocation},
|
||||
rom::{MaybeBufferedSram, MbcSaveState},
|
||||
Address,
|
||||
},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
enum RamBank {
|
||||
Ram(u8),
|
||||
|
@ -23,25 +25,27 @@ where
|
|||
ram_bank: RamBank,
|
||||
ram_size: usize,
|
||||
ram_enabled: bool,
|
||||
camera: CameraWrapper<C>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
extra_bits_a000: u8,
|
||||
camera_ram: [u8; 53],
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PocketCameraSaveState;
|
||||
|
||||
impl<C> PocketCamera<C>
|
||||
where
|
||||
C: PocketCameraTrait,
|
||||
{
|
||||
#[allow(unused)]
|
||||
pub(crate) fn init(
|
||||
data: Vec<u8>,
|
||||
rom_size: u8,
|
||||
ram_size: u8,
|
||||
save_file: Option<SaveDataLocation>,
|
||||
mut camera: CameraWrapper<C>,
|
||||
save_file: Option<PathBuf>,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
let ram = ram_size_kb(ram_size).map(|s| MaybeBufferedSram::new(save_file, s * KB));
|
||||
camera.inner.init();
|
||||
camera.lock().unwrap().inner.init();
|
||||
Self {
|
||||
data,
|
||||
rom_bank: 1,
|
||||
|
@ -56,45 +60,60 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn get_rom_addr(&self, address: RomAddress) -> usize {
|
||||
fn get_rom_addr(&self, address: Address) -> usize {
|
||||
(match address {
|
||||
RomAddress::Bank0(address) => address.inner() as usize,
|
||||
RomAddress::MappedBank(address) => {
|
||||
let internal_addr = address.get_local() as usize;
|
||||
0x0..0x4000 => address as usize,
|
||||
0x4000..0x8000 => {
|
||||
let internal_addr = address as usize - 0x4000;
|
||||
internal_addr + (ROM_BANK_SIZE * self.rom_bank as usize)
|
||||
}
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
} % self.rom_size)
|
||||
}
|
||||
|
||||
fn get_ram_addr(&self, address: CartRamAddress, bank: u8) -> usize {
|
||||
((address.get_local() as usize) + (RAM_BANK_SIZE * bank as usize)) % self.ram_size
|
||||
fn get_ram_addr(&self, address: Address, bank: u8) -> usize {
|
||||
((address as usize - 0xA000) + (RAM_BANK_SIZE * bank as usize)) % self.ram_size
|
||||
}
|
||||
|
||||
fn get_cam_reg(&self, address: CartRamAddress) -> u8 {
|
||||
match address.inner() {
|
||||
0xA000 => (if self.camera.is_capturing() { 0x1 } else { 0x0 }) | self.extra_bits_a000,
|
||||
0xA001..=0xA035 => self.camera_ram[(address.inner() - 0xA001) as usize],
|
||||
pub(crate) fn from_save_state(
|
||||
_state: PocketCameraSaveState,
|
||||
_data: Vec<u8>,
|
||||
_camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
todo!();
|
||||
}
|
||||
|
||||
fn get_cam_reg(&self, address: Address) -> u8 {
|
||||
match address {
|
||||
0xA000 => {
|
||||
(if self.camera.lock().unwrap().is_capturing() {
|
||||
0x1
|
||||
} else {
|
||||
0x0
|
||||
}) | self.extra_bits_a000
|
||||
}
|
||||
0xA001..=0xA035 => self.camera_ram[(address - 0xA001) as usize],
|
||||
_ => 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_cam_reg(&mut self, address: CartRamAddress, data: u8) {
|
||||
match address.inner() {
|
||||
fn set_cam_reg(&mut self, address: Address, data: u8) {
|
||||
match address {
|
||||
0xA000 => {
|
||||
if data & 0x1 == 0x1 {
|
||||
self.camera.begin_capture();
|
||||
self.camera.lock().unwrap().begin_capture();
|
||||
}
|
||||
self.extra_bits_a000 = data & 0b110;
|
||||
}
|
||||
0xA001..=0xA035 => {
|
||||
self.camera_ram[(address.inner() - 0xA001) as usize] = data;
|
||||
self.camera_ram[(address - 0xA001) as usize] = data;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_new_image(&mut self) {
|
||||
if let Some(image) = self.camera.get_next() {
|
||||
if let Some(image) = self.camera.lock().unwrap().get_next() {
|
||||
if let Some(ram) = &mut self.ram {
|
||||
for (i, v) in image.iter().enumerate() {
|
||||
ram.set(0x100 + i, *v);
|
||||
|
@ -108,11 +127,11 @@ impl<C> Mbc for PocketCamera<C>
|
|||
where
|
||||
C: PocketCameraTrait + Send,
|
||||
{
|
||||
fn get(&self, address: RomAddress) -> u8 {
|
||||
fn get(&self, address: Address) -> u8 {
|
||||
self.data[self.get_rom_addr(address)]
|
||||
}
|
||||
|
||||
fn get_ram(&self, address: CartRamAddress) -> u8 {
|
||||
fn get_ram(&self, address: Address) -> u8 {
|
||||
match self.ram_bank {
|
||||
RamBank::Ram(bank) => {
|
||||
if let Some(ram) = &self.ram {
|
||||
|
@ -125,11 +144,15 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, address: RomAddress, data: u8) {
|
||||
fn set(&mut self, address: Address, data: u8) {
|
||||
self.check_for_new_image();
|
||||
match address.inner() {
|
||||
match address {
|
||||
0x0..0x2000 => {
|
||||
self.ram_enabled = (data & 0xF) == 0xA;
|
||||
if (data & 0xF) == 0xA {
|
||||
self.ram_enabled = true
|
||||
} else {
|
||||
self.ram_enabled = false
|
||||
}
|
||||
}
|
||||
0x2000..0x4000 => {
|
||||
if data < 0x40 {
|
||||
|
@ -144,18 +167,16 @@ where
|
|||
}
|
||||
}
|
||||
0x6000..0x8000 => {}
|
||||
_ => panic!(),
|
||||
_ => panic!("address {address} incompatible with MBC"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ram(&mut self, address: CartRamAddress, data: u8) {
|
||||
fn set_ram(&mut self, address: Address, data: u8) {
|
||||
self.check_for_new_image();
|
||||
match self.ram_bank {
|
||||
RamBank::Ram(bank) => {
|
||||
let real_addr = self.get_ram_addr(address, bank);
|
||||
if self.ram_enabled
|
||||
&& let Some(ram) = &mut self.ram
|
||||
{
|
||||
if self.ram_enabled && let Some(ram) = &mut self.ram {
|
||||
ram.set(real_addr, data);
|
||||
}
|
||||
}
|
||||
|
@ -180,4 +201,8 @@ where
|
|||
ram.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn get_save_state(&self) -> MbcSaveState {
|
||||
MbcSaveState::PocketCamera(PocketCameraSaveState)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,230 +0,0 @@
|
|||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Seek, SeekFrom, Write},
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use super::mbcs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SaveDataLocation {
|
||||
File(PathBuf),
|
||||
Raw(Arc<RwLock<Vec<u8>>>),
|
||||
}
|
||||
|
||||
pub(crate) enum MaybeBufferedSram {
|
||||
File(FileBufferedSram),
|
||||
Raw(RawBufferedSram),
|
||||
Unbuffered(UnbufferedSram),
|
||||
}
|
||||
|
||||
impl MaybeBufferedSram {
|
||||
pub(crate) fn new(save: Option<SaveDataLocation>, length: usize) -> Self {
|
||||
match save {
|
||||
Some(SaveDataLocation::File(path)) => Self::File(FileBufferedSram::new(path, length)),
|
||||
Some(SaveDataLocation::Raw(buf)) => Self::Raw(RawBufferedSram::new(buf, length)),
|
||||
None => Self::Unbuffered(UnbufferedSram::new(length)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct UnbufferedSram {
|
||||
buf: Vec<u8>,
|
||||
}
|
||||
|
||||
impl UnbufferedSram {
|
||||
fn new(length: usize) -> Self {
|
||||
Self {
|
||||
buf: vec![0; length],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferedSramTrait for UnbufferedSram {
|
||||
fn len(&self) -> usize {
|
||||
self.buf.len()
|
||||
}
|
||||
|
||||
fn get(&self, addr: usize) -> u8 {
|
||||
if addr >= self.buf.len() {
|
||||
0
|
||||
} else {
|
||||
self.buf[addr]
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, addr: usize, data: u8) {
|
||||
if addr < self.buf.len() {
|
||||
self.buf[addr] = data;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
|
||||
pub(crate) struct RawBufferedSram {
|
||||
buf: Arc<RwLock<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl RawBufferedSram {
|
||||
fn new(buf: Arc<RwLock<Vec<u8>>>, length: usize) -> Self {
|
||||
if let Ok(mut buf) = buf.write() {
|
||||
buf.resize(length, 0);
|
||||
}
|
||||
Self { buf }
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferedSramTrait for RawBufferedSram {
|
||||
fn len(&self) -> usize {
|
||||
match self.buf.read() {
|
||||
Ok(buf) => buf.len(),
|
||||
Err(e) => panic!("failed to lock sram buffer: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, addr: usize) -> u8 {
|
||||
match self.buf.read() {
|
||||
Ok(buf) => {
|
||||
if addr >= buf.len() {
|
||||
0
|
||||
} else {
|
||||
buf[addr]
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("failed to lock sram buffer: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, addr: usize, data: u8) {
|
||||
match self.buf.write() {
|
||||
Ok(mut buf) => {
|
||||
if addr < buf.len() {
|
||||
buf[addr] = data;
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("failed to lock sram buffer: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {}
|
||||
}
|
||||
|
||||
pub(crate) struct FileBufferedSram {
|
||||
buf: Vec<u8>,
|
||||
length: usize,
|
||||
inner: File,
|
||||
unbuffered_writes: usize,
|
||||
}
|
||||
|
||||
const NUM_WRITES_TO_FLUSH: usize = 256;
|
||||
|
||||
impl FileBufferedSram {
|
||||
fn new(path: PathBuf, length: usize) -> Self {
|
||||
let mut buf = Vec::new();
|
||||
let inner = {
|
||||
if path.exists() {
|
||||
let mut writer = OpenOptions::new()
|
||||
.write(true)
|
||||
.read(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
writer.read_to_end(&mut buf).unwrap();
|
||||
writer
|
||||
} else {
|
||||
buf.resize(8 * mbcs::KB, 0);
|
||||
let writer = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path)
|
||||
.unwrap();
|
||||
writer
|
||||
}
|
||||
};
|
||||
|
||||
Self {
|
||||
buf,
|
||||
length,
|
||||
inner,
|
||||
unbuffered_writes: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferedSramTrait for FileBufferedSram {
|
||||
fn len(&self) -> usize {
|
||||
self.length
|
||||
}
|
||||
|
||||
fn get(&self, addr: usize) -> u8 {
|
||||
if addr >= self.buf.len() {
|
||||
0
|
||||
} else {
|
||||
self.buf[addr]
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, addr: usize, data: u8) {
|
||||
self.unbuffered_writes += 1;
|
||||
while addr >= self.buf.len() {
|
||||
self.buf.resize(self.buf.len() + (8 * mbcs::KB), 0);
|
||||
}
|
||||
self.buf[addr] = data;
|
||||
if self.unbuffered_writes >= NUM_WRITES_TO_FLUSH {
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
self.inner.seek(SeekFrom::Start(0)).unwrap();
|
||||
self.inner.set_len(self.buf.len() as u64).unwrap();
|
||||
self.inner.write_all(&self.buf).unwrap();
|
||||
self.unbuffered_writes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait BufferedSramTrait {
|
||||
fn len(&self) -> usize;
|
||||
fn get(&self, addr: usize) -> u8;
|
||||
fn set(&mut self, addr: usize, data: u8);
|
||||
fn flush(&mut self);
|
||||
}
|
||||
|
||||
impl BufferedSramTrait for MaybeBufferedSram {
|
||||
fn len(&self) -> usize {
|
||||
match self {
|
||||
MaybeBufferedSram::File(f) => f.len(),
|
||||
MaybeBufferedSram::Raw(r) => r.len(),
|
||||
MaybeBufferedSram::Unbuffered(u) => u.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, addr: usize) -> u8 {
|
||||
match self {
|
||||
MaybeBufferedSram::File(f) => f.get(addr),
|
||||
MaybeBufferedSram::Raw(r) => r.get(addr),
|
||||
MaybeBufferedSram::Unbuffered(u) => u.get(addr),
|
||||
}
|
||||
}
|
||||
|
||||
fn set(&mut self, addr: usize, data: u8) {
|
||||
match self {
|
||||
MaybeBufferedSram::File(f) => f.set(addr, data),
|
||||
MaybeBufferedSram::Raw(r) => r.set(addr, data),
|
||||
MaybeBufferedSram::Unbuffered(u) => u.set(addr, data),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if let Self::File(ref mut f) = self {
|
||||
f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MaybeBufferedSram {
|
||||
fn drop(&mut self) {
|
||||
self.flush();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use self::memory::{mmio::gpu::Colour, Interrupt, Memory};
|
||||
use crate::connect::JoypadState;
|
||||
use self::memory::{mmio::gpu::Colour, Interrupt, Memory, MemorySaveState};
|
||||
use crate::{
|
||||
connect::{AudioOutput, CameraWrapperRef, PocketCamera, Renderer, SerialTarget},
|
||||
verbose_println,
|
||||
};
|
||||
|
||||
mod instructions;
|
||||
pub mod memory;
|
||||
|
@ -20,38 +23,68 @@ pub(crate) enum Direction {
|
|||
Right,
|
||||
}
|
||||
|
||||
pub struct Cpu<ColourFormat>
|
||||
pub struct Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub memory: Memory<ColourFormat>,
|
||||
pub memory: Memory<ColourFormat, R, C>,
|
||||
pub reg: Registers,
|
||||
pub last_instruction: u8,
|
||||
pub last_instruction_addr: u16,
|
||||
last_instruction_addr: u16,
|
||||
halted: bool,
|
||||
should_halt_bug: bool,
|
||||
pub(super) cycle_count: usize,
|
||||
pub(crate) is_skipping: bool,
|
||||
pub(crate) no_output: bool,
|
||||
pub(super) next_joypad_state: Option<JoypadState>,
|
||||
}
|
||||
|
||||
impl<ColourFormat> Cpu<ColourFormat>
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CpuSaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub(crate) fn new(memory: Memory<ColourFormat>, run_bootrom: bool, no_output: bool) -> Self {
|
||||
memory: MemorySaveState<ColourFormat, R>,
|
||||
reg: Registers,
|
||||
last_instruction: u8,
|
||||
last_instruction_addr: u16,
|
||||
halted: bool,
|
||||
should_halt_bug: bool,
|
||||
}
|
||||
|
||||
impl<ColourFormat, R> CpuSaveState<ColourFormat, R>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
{
|
||||
pub fn create<C: PocketCamera + Send + 'static>(cpu: &Cpu<ColourFormat, R, C>) -> Self {
|
||||
Self {
|
||||
memory: MemorySaveState::create(&cpu.memory),
|
||||
reg: cpu.reg,
|
||||
last_instruction: cpu.last_instruction,
|
||||
last_instruction_addr: cpu.last_instruction_addr,
|
||||
halted: cpu.halted,
|
||||
should_halt_bug: cpu.should_halt_bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ColourFormat, R, C> Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub fn new(mut memory: Memory<ColourFormat, R, C>, run_bootrom: bool) -> Self {
|
||||
if !run_bootrom {
|
||||
memory.cpu_ram_init();
|
||||
}
|
||||
Self {
|
||||
memory,
|
||||
reg: Registers::init(),
|
||||
reg: Registers::init(run_bootrom),
|
||||
last_instruction: 0x0,
|
||||
last_instruction_addr: 0x0,
|
||||
halted: false,
|
||||
should_halt_bug: false,
|
||||
cycle_count: 0,
|
||||
is_skipping: !run_bootrom,
|
||||
no_output,
|
||||
next_joypad_state: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,18 +96,6 @@ where
|
|||
return;
|
||||
}
|
||||
|
||||
if self.is_skipping && !self.memory.has_bootrom() {
|
||||
self.is_skipping = false;
|
||||
}
|
||||
|
||||
// double this if in double speed mode
|
||||
let vram_dma_cycles = self.memory.vram_dma_tick();
|
||||
if vram_dma_cycles > 0 {
|
||||
self.increment_timers(vram_dma_cycles);
|
||||
let interrupt_cycles = self.handle_interrupts();
|
||||
self.increment_timers(interrupt_cycles);
|
||||
}
|
||||
|
||||
if self.memory.ime_scheduled > 0 {
|
||||
self.memory.ime_scheduled = self.memory.ime_scheduled.saturating_sub(1);
|
||||
if self.memory.ime_scheduled == 0 {
|
||||
|
@ -83,19 +104,19 @@ where
|
|||
}
|
||||
|
||||
self.last_instruction_addr = self.reg.pc;
|
||||
|
||||
self.memory.user_mode = true;
|
||||
let opcode = self.next_opcode();
|
||||
|
||||
if self.should_halt_bug {
|
||||
self.reg.pc = self.reg.pc.wrapping_sub(0x1);
|
||||
self.should_halt_bug = false;
|
||||
}
|
||||
self.last_instruction = opcode;
|
||||
|
||||
let cycles = self.run_opcode(opcode);
|
||||
self.memory.user_mode = false;
|
||||
self.increment_timers(cycles);
|
||||
verbose_println!(
|
||||
"exec {:#4X} from pc: {:#X}",
|
||||
opcode,
|
||||
self.last_instruction_addr
|
||||
);
|
||||
self.run_and_increment_timers(opcode);
|
||||
|
||||
let interrupt_cycles = self.handle_interrupts();
|
||||
self.increment_timers(interrupt_cycles);
|
||||
|
@ -107,6 +128,11 @@ where
|
|||
opcode
|
||||
}
|
||||
|
||||
fn run_and_increment_timers(&mut self, opcode: u8) {
|
||||
let cycles = self.run_opcode(opcode);
|
||||
self.increment_timers(cycles);
|
||||
}
|
||||
|
||||
fn halt(&mut self) {
|
||||
if !self.memory.ime && self.memory.interrupts.is_interrupt_queued() {
|
||||
// halt bug
|
||||
|
@ -116,7 +142,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_interrupts(&mut self) -> usize {
|
||||
fn handle_interrupts(&mut self) -> u8 {
|
||||
if self.memory.ime {
|
||||
if let Some(interrupt) = self.memory.interrupts.get_next_interrupt() {
|
||||
let interrupt_addr = match interrupt {
|
||||
|
@ -145,6 +171,31 @@ where
|
|||
self.reg.pc = addr;
|
||||
self.memory.ime = false;
|
||||
}
|
||||
|
||||
pub(crate) fn from_save_state(
|
||||
state: CpuSaveState<ColourFormat, R>,
|
||||
data: Vec<u8>,
|
||||
window: R,
|
||||
output: AudioOutput,
|
||||
serial_target: SerialTarget,
|
||||
camera: CameraWrapperRef<C>,
|
||||
) -> Self {
|
||||
Self {
|
||||
memory: Memory::from_save_state(
|
||||
state.memory,
|
||||
data,
|
||||
window,
|
||||
output,
|
||||
serial_target,
|
||||
camera,
|
||||
),
|
||||
reg: state.reg,
|
||||
last_instruction: state.last_instruction,
|
||||
last_instruction_addr: state.last_instruction_addr,
|
||||
halted: state.halted,
|
||||
should_halt_bug: state.should_halt_bug,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
@ -169,20 +220,31 @@ pub struct Registers {
|
|||
}
|
||||
|
||||
impl Registers {
|
||||
fn init() -> Self {
|
||||
Self {
|
||||
af: 0,
|
||||
bc: 0,
|
||||
de: 0,
|
||||
hl: 0,
|
||||
sp: 0xFFFE,
|
||||
pc: 0,
|
||||
fn init(run_bootrom: bool) -> Self {
|
||||
if run_bootrom {
|
||||
Self {
|
||||
af: 0,
|
||||
bc: 0,
|
||||
de: 0,
|
||||
hl: 0,
|
||||
sp: 0xFFFE,
|
||||
pc: 0,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
af: 0x01B0,
|
||||
bc: 0x0013,
|
||||
de: 0x00D8,
|
||||
hl: 0x014D,
|
||||
sp: 0xFFFE,
|
||||
pc: 0x0100,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Registers {
|
||||
pub(crate) fn get_8(&self, register: Reg8) -> u8 {
|
||||
fn get_8(&self, register: Reg8) -> u8 {
|
||||
match register {
|
||||
Reg8::A => self.af.get_high(),
|
||||
Reg8::B => self.bc.get_high(),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{
|
||||
connect::{PocketCamera, Renderer},
|
||||
processor::{
|
||||
instructions::{res, set},
|
||||
instructions::instructions::{res, set},
|
||||
Cpu, Flags, Reg8, SplitRegister,
|
||||
},
|
||||
util::as_signed,
|
||||
|
@ -8,11 +9,13 @@ use crate::{
|
|||
|
||||
use super::memory::mmio::gpu::Colour;
|
||||
|
||||
impl<ColourFormat> Cpu<ColourFormat>
|
||||
impl<ColourFormat, R, C> Cpu<ColourFormat, R, C>
|
||||
where
|
||||
ColourFormat: From<Colour> + Copy,
|
||||
ColourFormat: From<Colour> + Clone,
|
||||
R: Renderer<ColourFormat>,
|
||||
C: PocketCamera + Send + 'static,
|
||||
{
|
||||
pub fn run_opcode(&mut self, opcode: u8) -> usize {
|
||||
pub fn run_opcode(&mut self, opcode: u8) -> u8 {
|
||||
match opcode {
|
||||
0x00 => {
|
||||
// noop
|
||||
|
@ -90,12 +93,7 @@ where
|
|||
0x10 => {
|
||||
// stop
|
||||
// 1 cycle long
|
||||
if self.memory.try_switch_speed() {
|
||||
self.increment_timers_div_optional(2050, false);
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
panic!("stop instruction");
|
||||
}
|
||||
0x11 => {
|
||||
self.reg.de = self.ld_immediate_word();
|
||||
|
@ -1233,7 +1231,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn cb_subop(&mut self, subop: u8) -> usize {
|
||||
fn cb_subop(&mut self, subop: u8) -> u8 {
|
||||
match subop {
|
||||
0x00 => {
|
||||
let val = self.rlc(self.reg.get_8(Reg8::B));
|
||||
|
@ -2455,6 +2453,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn undefined(opcode: u8) -> usize {
|
||||
fn undefined(opcode: u8) -> u8 {
|
||||
panic!("Undefined behaviour: opcode {opcode:#X}");
|
||||
}
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
|
||||
|
||||
use crate::connect::ResolutionData;
|
||||
|
||||
#[cfg(feature = "wgpu-renderer")]
|
||||
pub type ActiveBackend = wgpu::WgpuBackend;
|
||||
#[cfg(all(feature = "vulkan-renderer", not(feature = "wgpu-renderer")))]
|
||||
pub type ActiveBackend = vulkan::VulkanBackend;
|
||||
#[cfg(all(
|
||||
feature = "pixels-renderer",
|
||||
not(any(feature = "wgpu-renderer", feature = "vulkan-renderer"))
|
||||
))]
|
||||
pub type ActiveBackend = pixels::PixelsBackend;
|
||||
|
||||
#[cfg(feature = "pixels-renderer")]
|
||||
pub mod pixels;
|
||||
#[cfg(feature = "vulkan-renderer")]
|
||||
pub mod vulkan;
|
||||
#[cfg(feature = "wgpu-renderer")]
|
||||
pub mod wgpu;
|
||||
|
||||
#[cfg(feature = "librashader")]
|
||||
mod shaders;
|
||||
|
||||
pub trait RendererBackend {
|
||||
type RendererBackendManager: RendererBackendManager;
|
||||
type RendererError: std::error::Error;
|
||||
|
||||
fn new<W: HasDisplayHandle + HasWindowHandle>(
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
shader_path: Option<PathBuf>,
|
||||
manager: Arc<Self::RendererBackendManager>,
|
||||
) -> Result<Self, Self::RendererError>
|
||||
where
|
||||
Self: std::marker::Sized;
|
||||
fn resize<W: HasDisplayHandle + HasWindowHandle>(
|
||||
&mut self,
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
) -> Result<(), Self::RendererError>;
|
||||
fn new_frame(&mut self, buffer: &[[u8; 4]]) -> Result<(), Self::RendererError>;
|
||||
fn render(
|
||||
&mut self,
|
||||
resolutions: ResolutionData,
|
||||
manager: &Self::RendererBackendManager,
|
||||
) -> Result<(), Self::RendererError>;
|
||||
}
|
||||
|
||||
pub trait RendererBackendManager {
|
||||
fn new(display_handle: DisplayHandle) -> Self;
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use pixels::{Pixels, SurfaceTexture};
|
||||
use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
|
||||
|
||||
use crate::{connect::ResolutionData, error::PixelsError};
|
||||
|
||||
use super::{RendererBackend, RendererBackendManager};
|
||||
|
||||
pub struct PixelsBackendManager {}
|
||||
|
||||
impl RendererBackendManager for PixelsBackendManager {
|
||||
fn new(_: DisplayHandle) -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PixelsBackend {
|
||||
pub pixels: Pixels,
|
||||
}
|
||||
|
||||
impl RendererBackend for PixelsBackend {
|
||||
type RendererBackendManager = PixelsBackendManager;
|
||||
type RendererError = PixelsError;
|
||||
|
||||
fn new<W: HasDisplayHandle + HasWindowHandle>(
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
_: Option<PathBuf>,
|
||||
_: Arc<Self::RendererBackendManager>,
|
||||
) -> Result<Self, Self::RendererError> {
|
||||
Ok(Self {
|
||||
pixels: new_pixels(resolutions, window)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn resize<W: HasDisplayHandle + HasWindowHandle>(
|
||||
&mut self,
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
) -> Result<(), Self::RendererError> {
|
||||
self.pixels = new_pixels(resolutions, window)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_frame(&mut self, buffer: &[[u8; 4]]) -> Result<(), Self::RendererError> {
|
||||
if !buffer.is_empty() {
|
||||
self.pixels
|
||||
.frame_mut()
|
||||
.copy_from_slice(bytemuck::cast_slice(buffer));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
_: ResolutionData,
|
||||
_: &PixelsBackendManager,
|
||||
) -> Result<(), Self::RendererError> {
|
||||
self.pixels.render()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn new_pixels<W: HasDisplayHandle + HasWindowHandle>(
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
) -> Result<Pixels, pixels::Error> {
|
||||
let dummy = DummyHandle::new(window).unwrap();
|
||||
let surface_texture: SurfaceTexture<'_, DummyHandle> =
|
||||
SurfaceTexture::new(resolutions.real_width, resolutions.real_height, &dummy);
|
||||
pixels::PixelsBuilder::new(
|
||||
resolutions.scaled_width,
|
||||
resolutions.scaled_height,
|
||||
surface_texture,
|
||||
)
|
||||
.request_adapter_options(pixels::wgpu::RequestAdapterOptionsBase {
|
||||
power_preference: pixels::wgpu::PowerPreference::HighPerformance,
|
||||
..pixels::wgpu::RequestAdapterOptionsBase::default()
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
struct DummyHandle<'a> {
|
||||
window: raw_window_handle::WindowHandle<'a>,
|
||||
display: raw_window_handle::DisplayHandle<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DummyHandle<'a> {
|
||||
fn new<T>(value: &'a T) -> Option<Self>
|
||||
where
|
||||
T: HasWindowHandle + HasDisplayHandle + 'a,
|
||||
{
|
||||
Some(Self {
|
||||
window: value.window_handle().ok()?,
|
||||
display: value.display_handle().ok()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<'a> Sync for DummyHandle<'a> {}
|
||||
unsafe impl<'a> Send for DummyHandle<'a> {}
|
||||
|
||||
impl<'a> HasWindowHandle for DummyHandle<'a> {
|
||||
fn window_handle(
|
||||
&self,
|
||||
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError> {
|
||||
Ok(self.window)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> HasDisplayHandle for DummyHandle<'a> {
|
||||
fn display_handle(&self) -> Result<DisplayHandle<'_>, raw_window_handle::HandleError> {
|
||||
Ok(self.display)
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
use librashader_presets::ShaderPreset;
|
||||
|
||||
pub fn default_preset() -> ShaderPreset {
|
||||
ShaderPreset {
|
||||
shader_count: 1,
|
||||
shaders: vec![librashader_presets::ShaderPassConfig {
|
||||
id: 0,
|
||||
name: librashader_common::ShaderStorage::String(
|
||||
include_str!("./stock.slang").to_string(),
|
||||
),
|
||||
alias: None,
|
||||
filter: librashader::FilterMode::Nearest,
|
||||
wrap_mode: librashader::WrapMode::ClampToBorder,
|
||||
frame_count_mod: 0,
|
||||
srgb_framebuffer: false,
|
||||
float_framebuffer: false,
|
||||
mipmap_input: false,
|
||||
scaling: librashader_presets::Scale2D {
|
||||
valid: false,
|
||||
x: librashader_presets::Scaling {
|
||||
scale_type: librashader_presets::ScaleType::Input,
|
||||
factor: librashader_presets::ScaleFactor::Float(1.0),
|
||||
},
|
||||
y: librashader_presets::Scaling {
|
||||
scale_type: librashader_presets::ScaleType::Input,
|
||||
factor: librashader_presets::ScaleFactor::Float(1.0),
|
||||
},
|
||||
},
|
||||
}],
|
||||
textures: vec![],
|
||||
parameters: vec![],
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
#version 450
|
||||
|
||||
layout(push_constant) uniform Push
|
||||
{
|
||||
vec4 SourceSize;
|
||||
vec4 OriginalSize;
|
||||
vec4 OutputSize;
|
||||
uint FrameCount;
|
||||
} params;
|
||||
|
||||
layout(std140, set = 0, binding = 0) uniform UBO
|
||||
{
|
||||
mat4 MVP;
|
||||
} global;
|
||||
|
||||
#pragma stage vertex
|
||||
layout(location = 0) in vec4 Position;
|
||||
layout(location = 1) in vec2 TexCoord;
|
||||
layout(location = 0) out vec2 vTexCoord;
|
||||
|
||||
void main()
|
||||
{
|
||||
gl_Position = global.MVP * Position;
|
||||
vTexCoord = TexCoord;
|
||||
}
|
||||
|
||||
#pragma stage fragment
|
||||
layout(location = 0) in vec2 vTexCoord;
|
||||
layout(location = 0) out vec4 FragColor;
|
||||
layout(set = 0, binding = 2) uniform sampler2D Source;
|
||||
|
||||
void main()
|
||||
{
|
||||
FragColor = vec4(texture(Source, vTexCoord).rgb, 1.0);
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
use ash::{ext::debug_utils, vk, Entry, Instance};
|
||||
|
||||
pub(super) struct VulkanDebug {
|
||||
debug_utils_loader: debug_utils::Instance,
|
||||
debug_call_back: vk::DebugUtilsMessengerEXT,
|
||||
}
|
||||
|
||||
impl VulkanDebug {
|
||||
pub(super) fn new(entry: &Entry, instance: &Instance) -> Self {
|
||||
let debug_info = vk::DebugUtilsMessengerCreateInfoEXT::default()
|
||||
.message_severity(
|
||||
vk::DebugUtilsMessageSeverityFlagsEXT::ERROR
|
||||
| vk::DebugUtilsMessageSeverityFlagsEXT::WARNING
|
||||
| vk::DebugUtilsMessageSeverityFlagsEXT::INFO
|
||||
| vk::DebugUtilsMessageSeverityFlagsEXT::VERBOSE,
|
||||
)
|
||||
.message_type(
|
||||
vk::DebugUtilsMessageTypeFlagsEXT::GENERAL
|
||||
| vk::DebugUtilsMessageTypeFlagsEXT::VALIDATION
|
||||
| vk::DebugUtilsMessageTypeFlagsEXT::PERFORMANCE,
|
||||
)
|
||||
.pfn_user_callback(Some(vulkan_debug_callback));
|
||||
|
||||
let debug_utils_loader = debug_utils::Instance::new(entry, instance);
|
||||
let debug_call_back =
|
||||
unsafe { debug_utils_loader.create_debug_utils_messenger(&debug_info, None) }.unwrap();
|
||||
|
||||
Self {
|
||||
debug_utils_loader,
|
||||
debug_call_back,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VulkanDebug {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.debug_utils_loader
|
||||
.destroy_debug_utils_messenger(self.debug_call_back, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn vulkan_debug_callback(
|
||||
message_severity: vk::DebugUtilsMessageSeverityFlagsEXT,
|
||||
message_type: vk::DebugUtilsMessageTypeFlagsEXT,
|
||||
p_callback_data: *const vk::DebugUtilsMessengerCallbackDataEXT,
|
||||
_user_data: *mut std::os::raw::c_void,
|
||||
) -> vk::Bool32 {
|
||||
let callback_data = *p_callback_data;
|
||||
let message_id_number = callback_data.message_id_number;
|
||||
|
||||
let message_id_name = if callback_data.p_message_id_name.is_null() {
|
||||
std::borrow::Cow::from("")
|
||||
} else {
|
||||
std::ffi::CStr::from_ptr(callback_data.p_message_id_name).to_string_lossy()
|
||||
};
|
||||
|
||||
let message = if callback_data.p_message.is_null() {
|
||||
std::borrow::Cow::from("")
|
||||
} else {
|
||||
std::ffi::CStr::from_ptr(callback_data.p_message).to_string_lossy()
|
||||
};
|
||||
|
||||
log::warn!(
|
||||
"{message_severity:?}:\n{message_type:?} [{message_id_name} ({message_id_number})] : {message}\n",
|
||||
);
|
||||
|
||||
vk::FALSE
|
||||
}
|
|
@ -1,548 +0,0 @@
|
|||
use ash::{util::Align, vk, Entry, Instance};
|
||||
use ash_window::enumerate_required_extensions;
|
||||
use librashader::runtime::vk::{FilterChain, FilterChainOptions, FrameOptions, VulkanObjects};
|
||||
use raw_window_handle::{DisplayHandle, HasDisplayHandle, HasWindowHandle};
|
||||
use std::{mem::ManuallyDrop, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{connect::ResolutionData, error::VulkanError};
|
||||
|
||||
use self::{
|
||||
types::{FramebufferData, SurfaceData, SwapchainData, Vertex, VulkanData, SHADER_INPUT_FORMAT},
|
||||
utils::{
|
||||
begin_commandbuffer, find_memorytype_index, record_submit_commandbuffer,
|
||||
submit_commandbuffer,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{shaders::default_preset, RendererBackend, RendererBackendManager};
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "vulkan-debug"))]
|
||||
mod debug;
|
||||
mod types;
|
||||
mod utils;
|
||||
|
||||
// much of this is lifted from the Ash examples
|
||||
// https://github.com/ash-rs/ash/blob/master/examples/src/lib.rs
|
||||
// https://github.com/ash-rs/ash/blob/master/examples/src/bin/texture.rs
|
||||
|
||||
const VERTICES: [Vertex; 3] = [Vertex(-1.0, -1.0), Vertex(3.0, -1.0), Vertex(-1.0, 3.0)];
|
||||
|
||||
pub struct VulkanBackendManager {
|
||||
entry: Entry,
|
||||
instance: Instance,
|
||||
#[cfg(all(debug_assertions, feature = "vulkan-debug"))]
|
||||
#[allow(dead_code)]
|
||||
debug: debug::VulkanDebug,
|
||||
}
|
||||
|
||||
impl RendererBackendManager for VulkanBackendManager {
|
||||
fn new(display_handle: DisplayHandle) -> Self {
|
||||
#[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "vulkan-static"))]
|
||||
let entry = ash_molten::load();
|
||||
#[cfg(not(all(any(target_os = "macos", target_os = "ios"), feature = "vulkan-static")))]
|
||||
let entry = Entry::linked();
|
||||
|
||||
let name = std::ffi::CString::new("gameboy").unwrap();
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut extension_names = enumerate_required_extensions(display_handle.as_raw())
|
||||
.unwrap()
|
||||
.to_vec();
|
||||
|
||||
#[cfg(all(debug_assertions, feature = "vulkan-debug"))]
|
||||
extension_names.push(ash::ext::debug_utils::NAME.as_ptr());
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
{
|
||||
#[cfg(not(feature = "vulkan-static"))]
|
||||
extension_names.push(vk::KHR_PORTABILITY_ENUMERATION_NAME.as_ptr());
|
||||
|
||||
extension_names.push(vk::KHR_GET_PHYSICAL_DEVICE_PROPERTIES2_NAME.as_ptr());
|
||||
}
|
||||
|
||||
let appinfo = vk::ApplicationInfo::default()
|
||||
.application_name(&name)
|
||||
.engine_name(&name)
|
||||
.application_version(0)
|
||||
.engine_version(0)
|
||||
.api_version(vk::make_api_version(0, 1, 0, 0));
|
||||
|
||||
let create_flags = if cfg!(any(target_os = "macos", target_os = "ios")) {
|
||||
vk::InstanceCreateFlags::ENUMERATE_PORTABILITY_KHR
|
||||
} else {
|
||||
vk::InstanceCreateFlags::default()
|
||||
};
|
||||
|
||||
let create_info = vk::InstanceCreateInfo::default()
|
||||
.application_info(&appinfo)
|
||||
.enabled_extension_names(&extension_names)
|
||||
.flags(create_flags);
|
||||
|
||||
let instance = unsafe { entry.create_instance(&create_info, None) }.unwrap();
|
||||
|
||||
Self {
|
||||
#[cfg(all(debug_assertions, feature = "vulkan-debug"))]
|
||||
debug: debug::VulkanDebug::new(&entry, &instance),
|
||||
entry,
|
||||
instance,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VulkanBackendManager {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.instance.destroy_instance(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VulkanBackend {
|
||||
inner: ManuallyDrop<VulkanWindowInner>,
|
||||
filter_chain: ManuallyDrop<FilterChain>,
|
||||
manager: Arc<VulkanBackendManager>,
|
||||
}
|
||||
|
||||
impl Drop for VulkanBackend {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ManuallyDrop::drop(&mut self.filter_chain);
|
||||
ManuallyDrop::drop(&mut self.inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VulkanWindowOptions {
|
||||
pub shader_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl RendererBackend for VulkanBackend {
|
||||
type RendererBackendManager = VulkanBackendManager;
|
||||
type RendererError = VulkanError;
|
||||
|
||||
fn new<W: HasDisplayHandle + HasWindowHandle>(
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
shader_path: Option<PathBuf>,
|
||||
manager: Arc<VulkanBackendManager>,
|
||||
) -> Result<Self, Self::RendererError> {
|
||||
let inner = unsafe { VulkanWindowInner::new(resolutions, &window, manager.as_ref()) };
|
||||
|
||||
let filter_chain_options = FilterChainOptions {
|
||||
frames_in_flight: 0,
|
||||
force_no_mipmaps: false,
|
||||
disable_cache: false,
|
||||
use_dynamic_rendering: false,
|
||||
};
|
||||
|
||||
let vulkan = VulkanObjects::try_from((
|
||||
inner.vulkan_data.pdevice,
|
||||
manager.instance.clone(),
|
||||
inner.vulkan_data.device.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let filter_chain = match shader_path {
|
||||
Some(path) => {
|
||||
unsafe { FilterChain::load_from_path(path, vulkan, Some(&filter_chain_options)) }
|
||||
.unwrap()
|
||||
}
|
||||
None => unsafe {
|
||||
FilterChain::load_from_preset(default_preset(), vulkan, Some(&filter_chain_options))
|
||||
}
|
||||
.unwrap(),
|
||||
};
|
||||
|
||||
// TODO - don't unwrap
|
||||
Ok(Self {
|
||||
inner: ManuallyDrop::new(inner),
|
||||
filter_chain: ManuallyDrop::new(filter_chain),
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
fn resize<W: HasDisplayHandle + HasWindowHandle>(
|
||||
&mut self,
|
||||
resolutions: ResolutionData,
|
||||
_window: &W,
|
||||
) -> Result<(), Self::RendererError> {
|
||||
unsafe { self.inner.resize(resolutions, self.manager.as_ref()) };
|
||||
// TODO - make inner return a result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_frame(&mut self, buffer: &[[u8; 4]]) -> Result<(), Self::RendererError> {
|
||||
unsafe { self.inner.new_frame(buffer) }; // TODO - make inner return a result
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
&mut self,
|
||||
resolutions: ResolutionData,
|
||||
manager: &VulkanBackendManager,
|
||||
) -> Result<(), Self::RendererError> {
|
||||
unsafe {
|
||||
self.inner
|
||||
.render(&mut self.filter_chain, resolutions, manager)
|
||||
}; // TODO - make inner return a result
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct VulkanWindowInner {
|
||||
vulkan_data: VulkanData,
|
||||
renderpass: vk::RenderPass,
|
||||
swapchain: SwapchainData,
|
||||
surface: SurfaceData,
|
||||
framebuffers: FramebufferData,
|
||||
image_slice: Align<u8>,
|
||||
frame_counter: usize,
|
||||
}
|
||||
|
||||
impl VulkanWindowInner {
|
||||
unsafe fn new<W: HasDisplayHandle + HasWindowHandle>(
|
||||
resolutions: ResolutionData,
|
||||
window: &W,
|
||||
manager: &VulkanBackendManager,
|
||||
) -> Self {
|
||||
let surface = SurfaceData::new(window, manager);
|
||||
|
||||
let vulkan_data = VulkanData::new(manager, &surface);
|
||||
|
||||
let swapchain = SwapchainData::new(resolutions, manager, &surface, &vulkan_data);
|
||||
|
||||
let renderpass_attachments = [vk::AttachmentDescription {
|
||||
format: swapchain.format.format,
|
||||
samples: vk::SampleCountFlags::TYPE_1,
|
||||
load_op: vk::AttachmentLoadOp::CLEAR,
|
||||
store_op: vk::AttachmentStoreOp::STORE,
|
||||
final_layout: vk::ImageLayout::PRESENT_SRC_KHR,
|
||||
..Default::default()
|
||||
}];
|
||||
let color_attachment_refs = [vk::AttachmentReference {
|
||||
attachment: 0,
|
||||
layout: vk::ImageLayout::COLOR_ATTACHMENT_OPTIMAL,
|
||||
}];
|
||||
let dependencies = [vk::SubpassDependency {
|
||||
src_subpass: vk::SUBPASS_EXTERNAL,
|
||||
src_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
|
||||
dst_access_mask: vk::AccessFlags::COLOR_ATTACHMENT_READ
|
||||
| vk::AccessFlags::COLOR_ATTACHMENT_WRITE,
|
||||
dst_stage_mask: vk::PipelineStageFlags::COLOR_ATTACHMENT_OUTPUT,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let subpass = vk::SubpassDescription::default()
|
||||
.color_attachments(&color_attachment_refs)
|
||||
.pipeline_bind_point(vk::PipelineBindPoint::GRAPHICS);
|
||||
|
||||
let renderpass_create_info = vk::RenderPassCreateInfo::default()
|
||||
.attachments(&renderpass_attachments)
|
||||
.subpasses(std::slice::from_ref(&subpass))
|
||||
.dependencies(&dependencies);
|
||||
|
||||
let renderpass = vulkan_data
|
||||
.device
|
||||
.create_render_pass(&renderpass_create_info, None)
|
||||
.unwrap();
|
||||
|
||||
let framebuffers = FramebufferData::new(&swapchain, &vulkan_data, renderpass);
|
||||
|
||||
let vertex_input_buffer_info = vk::BufferCreateInfo {
|
||||
size: std::mem::size_of_val(&VERTICES) as u64,
|
||||
usage: vk::BufferUsageFlags::VERTEX_BUFFER,
|
||||
sharing_mode: vk::SharingMode::EXCLUSIVE,
|
||||
..Default::default()
|
||||
};
|
||||
let vertex_input_buffer = vulkan_data
|
||||
.device
|
||||
.create_buffer(&vertex_input_buffer_info, None)
|
||||
.unwrap();
|
||||
let vertex_input_buffer_memory_req = vulkan_data
|
||||
.device
|
||||
.get_buffer_memory_requirements(vertex_input_buffer);
|
||||
let vertex_input_buffer_memory_index = find_memorytype_index(
|
||||
&vertex_input_buffer_memory_req,
|
||||
&vulkan_data.device_memory_properties,
|
||||
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT,
|
||||
)
|
||||
.expect("Unable to find suitable memorytype for the vertex buffer.");
|
||||
|
||||
let vertex_buffer_allocate_info = vk::MemoryAllocateInfo {
|
||||
allocation_size: vertex_input_buffer_memory_req.size,
|
||||
memory_type_index: vertex_input_buffer_memory_index,
|
||||
..Default::default()
|
||||
};
|
||||
let vertex_input_buffer_memory = vulkan_data
|
||||
.device
|
||||
.allocate_memory(&vertex_buffer_allocate_info, None)
|
||||
.unwrap();
|
||||
|
||||
let vert_ptr = vulkan_data
|
||||
.device
|
||||
.map_memory(
|
||||
vertex_input_buffer_memory,
|
||||
0,
|
||||
vertex_input_buffer_memory_req.size,
|
||||
vk::MemoryMapFlags::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut slice = Align::new(
|
||||
vert_ptr,
|
||||
std::mem::align_of::<Vertex>() as u64,
|
||||
vertex_input_buffer_memory_req.size,
|
||||
);
|
||||
slice.copy_from_slice(&VERTICES);
|
||||
vulkan_data.device.unmap_memory(vertex_input_buffer_memory);
|
||||
vulkan_data
|
||||
.device
|
||||
.bind_buffer_memory(vertex_input_buffer, vertex_input_buffer_memory, 0)
|
||||
.unwrap();
|
||||
|
||||
let image_ptr = vulkan_data
|
||||
.device
|
||||
.map_memory(
|
||||
swapchain.shader_input_image_buffer_memory,
|
||||
0,
|
||||
swapchain.shader_input_image_buffer_memory_req.size,
|
||||
vk::MemoryMapFlags::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
let image_slice: Align<u8> = Align::new(
|
||||
image_ptr,
|
||||
std::mem::align_of::<u8>() as u64,
|
||||
swapchain.shader_input_image_buffer_memory_req.size,
|
||||
);
|
||||
|
||||
Self {
|
||||
renderpass,
|
||||
swapchain,
|
||||
surface,
|
||||
framebuffers,
|
||||
vulkan_data,
|
||||
image_slice,
|
||||
frame_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn resize(&mut self, resolutions: ResolutionData, manager: &VulkanBackendManager) {
|
||||
self.swapchain.manual_drop(&self.vulkan_data);
|
||||
for framebuffer in &self.framebuffers.framebuffers {
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_framebuffer(*framebuffer, None);
|
||||
}
|
||||
self.swapchain = SwapchainData::new(resolutions, manager, &self.surface, &self.vulkan_data);
|
||||
self.framebuffers =
|
||||
FramebufferData::new(&self.swapchain, &self.vulkan_data, self.renderpass);
|
||||
|
||||
let image_ptr = self
|
||||
.vulkan_data
|
||||
.device
|
||||
.map_memory(
|
||||
self.swapchain.shader_input_image_buffer_memory,
|
||||
0,
|
||||
self.swapchain.shader_input_image_buffer_memory_req.size,
|
||||
vk::MemoryMapFlags::empty(),
|
||||
)
|
||||
.unwrap();
|
||||
self.image_slice = Align::new(
|
||||
image_ptr,
|
||||
std::mem::align_of::<u8>() as u64,
|
||||
self.swapchain.shader_input_image_buffer_memory_req.size,
|
||||
);
|
||||
}
|
||||
|
||||
unsafe fn new_frame(&mut self, buffer: &[[u8; 4]]) {
|
||||
self.image_slice
|
||||
.copy_from_slice(bytemuck::cast_slice(buffer));
|
||||
|
||||
record_submit_commandbuffer(
|
||||
&self.vulkan_data.device,
|
||||
self.vulkan_data.texture_copy_command_buffer,
|
||||
self.vulkan_data.texture_copy_commands_reuse_fence,
|
||||
self.vulkan_data.present_queue,
|
||||
&[],
|
||||
&[],
|
||||
&[],
|
||||
|device, texture_command_buffer| {
|
||||
let texture_barrier = vk::ImageMemoryBarrier {
|
||||
dst_access_mask: vk::AccessFlags::TRANSFER_WRITE,
|
||||
new_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
image: self.swapchain.shader_input_texture,
|
||||
subresource_range: vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
level_count: 1,
|
||||
layer_count: 1,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
device.cmd_pipeline_barrier(
|
||||
texture_command_buffer,
|
||||
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[texture_barrier],
|
||||
);
|
||||
let buffer_copy_regions = vk::BufferImageCopy::default()
|
||||
.image_subresource(
|
||||
vk::ImageSubresourceLayers::default()
|
||||
.aspect_mask(vk::ImageAspectFlags::COLOR)
|
||||
.layer_count(1),
|
||||
)
|
||||
.image_extent(self.swapchain.shader_input_image_extent.into());
|
||||
|
||||
device.cmd_copy_buffer_to_image(
|
||||
texture_command_buffer,
|
||||
self.swapchain.shader_input_image_buffer,
|
||||
self.swapchain.shader_input_texture,
|
||||
vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
&[buffer_copy_regions],
|
||||
);
|
||||
let texture_barrier_end = vk::ImageMemoryBarrier {
|
||||
src_access_mask: vk::AccessFlags::TRANSFER_WRITE,
|
||||
dst_access_mask: vk::AccessFlags::SHADER_READ,
|
||||
old_layout: vk::ImageLayout::TRANSFER_DST_OPTIMAL,
|
||||
new_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
|
||||
image: self.swapchain.shader_input_texture,
|
||||
subresource_range: vk::ImageSubresourceRange {
|
||||
aspect_mask: vk::ImageAspectFlags::COLOR,
|
||||
level_count: 1,
|
||||
layer_count: 1,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
device.cmd_pipeline_barrier(
|
||||
texture_command_buffer,
|
||||
vk::PipelineStageFlags::TRANSFER,
|
||||
vk::PipelineStageFlags::FRAGMENT_SHADER,
|
||||
vk::DependencyFlags::empty(),
|
||||
&[],
|
||||
&[],
|
||||
&[texture_barrier_end],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
unsafe fn render(
|
||||
&mut self,
|
||||
filter_chain: &mut FilterChain,
|
||||
resolutions: ResolutionData,
|
||||
manager: &VulkanBackendManager,
|
||||
) {
|
||||
let (present_index, is_suboptimal) = self
|
||||
.swapchain
|
||||
.swapchain_loader
|
||||
.acquire_next_image(
|
||||
self.swapchain.swapchain,
|
||||
u64::MAX,
|
||||
self.vulkan_data.present_complete_semaphore,
|
||||
vk::Fence::null(),
|
||||
)
|
||||
.unwrap();
|
||||
if is_suboptimal {
|
||||
self.resize(resolutions, manager);
|
||||
return;
|
||||
}
|
||||
|
||||
begin_commandbuffer(
|
||||
&self.vulkan_data.device,
|
||||
self.vulkan_data.draw_command_buffer,
|
||||
self.vulkan_data.draw_commands_reuse_fence,
|
||||
);
|
||||
|
||||
filter_chain
|
||||
.frame(
|
||||
&librashader::runtime::vk::VulkanImage {
|
||||
image: self.swapchain.shader_input_texture,
|
||||
size: self.swapchain.shader_input_image_extent.into(),
|
||||
format: SHADER_INPUT_FORMAT,
|
||||
},
|
||||
&librashader::runtime::Viewport {
|
||||
x: 0.,
|
||||
y: 0.,
|
||||
mvp: None,
|
||||
output: librashader::runtime::vk::VulkanImage {
|
||||
image: self.swapchain.present_images[present_index as usize],
|
||||
size: self.swapchain.surface_resolution.into(),
|
||||
format: self.swapchain.format.format,
|
||||
},
|
||||
},
|
||||
self.vulkan_data.draw_command_buffer,
|
||||
self.frame_counter,
|
||||
Some(&FrameOptions {
|
||||
clear_history: true,
|
||||
frame_direction: 0,
|
||||
rotation: 0,
|
||||
total_subframes: 0,
|
||||
current_subframe: 0,
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
self.frame_counter += 1;
|
||||
|
||||
submit_commandbuffer(
|
||||
&self.vulkan_data.device,
|
||||
self.vulkan_data.draw_command_buffer,
|
||||
self.vulkan_data.draw_commands_reuse_fence,
|
||||
self.vulkan_data.present_queue,
|
||||
&[vk::PipelineStageFlags::BOTTOM_OF_PIPE],
|
||||
&[self.vulkan_data.present_complete_semaphore],
|
||||
&[self.vulkan_data.rendering_complete_semaphore],
|
||||
);
|
||||
|
||||
let present_info = vk::PresentInfoKHR {
|
||||
wait_semaphore_count: 1,
|
||||
p_wait_semaphores: &self.vulkan_data.rendering_complete_semaphore,
|
||||
swapchain_count: 1,
|
||||
p_swapchains: &self.swapchain.swapchain,
|
||||
p_image_indices: &present_index,
|
||||
..Default::default()
|
||||
};
|
||||
self.swapchain
|
||||
.swapchain_loader
|
||||
.queue_present(self.vulkan_data.present_queue, &present_info)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VulkanWindowInner {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
self.vulkan_data.device.device_wait_idle().unwrap();
|
||||
|
||||
for framebuffer in &self.framebuffers.framebuffers {
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_framebuffer(*framebuffer, None);
|
||||
}
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_render_pass(self.renderpass, None);
|
||||
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_semaphore(self.vulkan_data.present_complete_semaphore, None);
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_semaphore(self.vulkan_data.rendering_complete_semaphore, None);
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_fence(self.vulkan_data.draw_commands_reuse_fence, None);
|
||||
self.vulkan_data
|
||||
.device
|
||||
.destroy_fence(self.vulkan_data.setup_commands_reuse_fence, None);
|
||||
|
||||
self.swapchain.manual_drop(&self.vulkan_data);
|
||||
self.vulkan_data.device.destroy_device(None);
|
||||
self.surface
|
||||
.surface_loader
|
||||
.destroy_surface(self.surface.surface, None);
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue