Compare commits

...
14 Commits
Author SHA1 Message Date
opencode.energia.arti.ee 5dd01629b9 fix(termtris): do not select ExposureMask
A map-time Expose arrives before the game has a current piece, and the
game's full-redraw path then draws a bogus piece over the top row: visible
as a blanked well border with stray X tiles (the previous commit's
screenshots showed it). The window is never obscured in this session, so
drop the mask, drop the Expose branch, and note the reason in HANDOFF.md.

Refreshed with verify.sh: move 354/18, panels 346/12, quit 300/10, all
with focus=00400001; evidence re-captured and clean.
2026-11-12 05:19:52 +00:00
opencode.energia.arti.ee a12fb47d5f chore(termtris): handoff script and notes for the next machine
apps/termtris/verify.sh runs the whole acceptance: ROM md5 guard, 2ine
check, MAME build (--skip-mame to reuse one), the six tests, the
acceptance summary and the evidence list, all logged under build/.

HANDOFF.md states what is done, what is still open (the docs' figures and
screenshots predate the VR319 default because the installed MAME binary is
the 30 Oct build), the expected outputs, and the failure modes already
diagnosed. Refresh the acceptance numbers from the verify run:
348/19, 338/13, 292/11.
2026-11-12 05:14:33 +00:00
opencode.energia.arti.ee be5b323f5d docs(termtris): build the harness binary before reproducing evidence 2026-11-11 17:59:30 +00:00
opencode.energia.arti.ee 7a2cef56ad fix(termtris): review pass -- seed, focus/pause, expose, test assertions
- seed the game from the raw 0x1E93C0 counter (now_ms() returns 0 on its
  first call, so the old seed was always zero)
- track pause ourselves: FocusOut pauses once, FocusIn resumes only what we
  paused (game_input('p') is a toggle)
- select ExposureMask so the Expose redraw path actually fires
- play.sh/play_keys.sh rebuild the COFF (OUTNAME=termtris) and assert the
  run: status=0, tick>0, loops/events/quit per mode; render.sh asserts the
  renderer finished; map symbol lookup is column-order independent
- run.sh passes -lightgun -lightgun_device mouse so the pointer drives the
  tablet
- docs: link map addresses, monochrome/reverse wording, honest caption and
  limits text

Verified here: play.sh and input.sh pass. Still open: the MAME driver in the
tree is newer than the installed binary (Oct 30 build, VR150-era geometry),
so the acceptance suite must be re-run after a rebuild.
2026-11-11 17:59:16 +00:00
opencode.energia.arti.ee eb896f7b8c test(termtris): assert the play acceptance results; drop the dead drain hook 2026-11-03 20:11:57 +00:00
opencode.energia.arti.ee 64bd781aa6 chore(termtris): stop tracking per-test build outputs 2026-11-03 20:07:41 +00:00
opencode.energia.arti.ee 3e2dddeb6a docs(termtris): port report, run wrapper and harness write-up 2026-11-03 20:07:35 +00:00
opencode.energia.arti.ee 41d3cecd49 feat(termtris): run the game on the VT1200 X server
Board, score panel, preview and help/scores toggles draw through the
ROM Xlib cell grid; keys arrive as real X KeyPress events. The backend
is now monochrome (the VT1200 raster is one bit deep) and flushes whole
rows so the in-machine server can drain the request stream.
2026-11-03 20:03:47 +00:00
opencode.energia.arti.ee c1945cfc73 feat(termtris): X11 key map, millisecond clock and main loop 2026-11-03 19:03:14 +00:00
opencode.energia.arti.ee 264b53a6a0 feat(termtris): 80x24 cell-grid X11 renderer (6x13 cells) 2026-11-03 19:00:42 +00:00
opencode.energia.arti.ee a8041818bb feat(termtris): compat sprintf/stdio for the TI r5.01 runtime 2026-11-03 18:51:22 +00:00
opencode.energia.arti.ee a8612681ba feat(termtris): vendor source and VT1200 Xlib XG smoke test 2026-11-03 18:49:44 +00:00
opencode.energia.arti.ee 2c80547cc3 docs(termtris): implementation plan for the VT1200 X11 port 2026-11-03 18:42:47 +00:00
opencode.energia.arti.ee 989b8a11e1 docs(termtris): VT1200 X11 port design; local Xlib XG fix 2026-11-03 17:51:23 +00:00
53 changed files with 6701 additions and 0 deletions

No files matched your search

+9
View File
@@ -69,3 +69,12 @@ ghidra/vt1200.lock
tools/tms34010-sdk/Docs/ tools/tms34010-sdk/Docs/
tools/tms34010-sdk/Patents/ tools/tms34010-sdk/Patents/
tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/ tools/tms34010-sdk/1991-07 TIGA Promo Kit and Art Software/
# termtris build scratch (TI tools + 2ine libs)
apps/termtris/work/
apps/termtris/build/*.log
apps/termtris/build/smoke.*
apps/termtris/build/fmt.*
apps/termtris/build/render.*
apps/termtris/build/input.*
apps/termtris/evidence/loader_frame*.png
+674
View File
@@ -0,0 +1,674 @@
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>.
+134
View File
@@ -0,0 +1,134 @@
# Handoff — termtris running inside the VT1200
Everything here is on branch **`termtris`** of
`https://git.wut.ee/opencode.energia.arti.ee/vt1200.git` (remote `origin`).
The branch was developed in a worktree at
`/home/arti/.local/share/opencode/worktree/26e92e/termtris`; any checkout of the
branch is equivalent.
```sh
git fetch origin termtris
git checkout -b termtris origin/termtris
apps/termtris/verify.sh
```
`verify.sh` is the whole acceptance run: ROM checksum, toolchain check, MAME
build (skip with `--skip-mame`), the six tests, the acceptance summary and the
evidence list. It exits non-zero on the first failure and keeps every test log
under `apps/termtris/build/verify_<name>.log`. `RUNTIME` is roughly 12 minutes
once MAME is built; the MAME build itself is the long part.
## What is done
The upstream game (John Tsiombikas' `termtris`, commit `7c5435d`) builds as a
native TMS34010 program with the 1990 TI r5.01 C toolchain under 2ine, loads
into VT1200 RAM through `dwt/loader`, opens a 480×312 window on the terminal's
own X server through the ROM Xlib, and reads real X11 key events. It is the
actual game: board, score/level/lines panels, piece preview, help and
high-score panels, moving/rotating/dropping pieces, and `q` to quit.
| task | state | commit |
|---|---|---|
| vendor + toolchain + XG smoke test | done, `smoke.sh` PASS | `a861268` |
| `sprintf`/stdio compat | done, `fmt.sh` PASS | `a804181` |
| cell-grid X11 backend | done, `render.sh` PASS | `264b53a` |
| key map, clock, main loop | done, `input.sh` PASS | `c1945cf` |
| game integration, scores, COFF | done, `play.sh`/`play_keys.sh` PASS | `41d3cec` |
| docs + harness report | done | `3e2ddde` |
| review pass (seed, focus, expose, assertions) | done | `7a2cef5` |
| handoff script + this note | done | (this commit) |
`VT1200.md` is the port documentation; `harness/mame/TERMTRIS_REPORT.md` is the
harness-side report; the design and plan are in
`docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md` and
`docs/superpowers/plans/2026-11-03-termtris-vt1200.md`.
## What is *not* done yet — read this first
All screenshots and the acceptance numbers in the docs were produced with an
**older MAME binary** (`/home/arti/code/k-space/mame0289/vt1200d`, 30 Oct) that
predates the driver's VR319 default. That binary has no `VT1200_MONITOR`, and
its frames are 1024×864, i.e. the old VR150 geometry. The driver in this tree
defaults to **VR319** (1280×1024 active, 1696×1063 raster), which changes the
frame cadence and therefore the frame at which the loader and the tests act.
So on the new box, after `harness/mame/build.sh`:
1. Re-run `apps/termtris/verify.sh --skip-mame` and note the new
`DIAG: loops=... events=... quit=...` values.
2. Re-capture the screenshots (the test scripts write them into
`apps/termtris/evidence/`) and look at them: the board must still be a
bordered well with `[`/`]` tetrominoes, the score/level/lines boxes on the
right, and the preview box. The window is positioned at (80,80) inside a
larger raster, so the PNGs get bigger but the window content is unchanged.
3. Update the numbers in `VT1200.md` ("Acceptance values") and in
`harness/mame/TERMTRIS_REPORT.md`, and say plainly that the earlier figures
were VR150.
If the behaviour itself changes at VR319 (pieces not visible, no events), that
is a real finding, not a formatting detail — the loader acts at frame 2500 and
the keys at 3100–3290, which may need re-timing if the new raster shifts the
frame budget.
## Prerequisites on the new box
* a MAME 0.289 checkout (`MAME_SRC`, default `/home/arti/code/k-space/mame0289`);
* the 2ine build with `lx_loader` (`NINE=`, default
`/home/arti/os2-work/2ine/build`);
* the r5.01 SDK zips, already in the repo under
`tools/tms34010-sdk/1990-11 TMS340 Family SDK/`;
* `vt1200_v22.bin` in the repo root (the harness ROM dir symlinks to it).
`verify.sh` checks the ROM and the 2ine loader before doing anything, and
`harness/mame/build.sh` re-registers the driver in `mame.lst` and builds the
single-driver `vt1200` subtarget.
## Expected results
Last run here (older MAME, VR150 geometry):
```
smoke PASS type=2 keycode=194 lookup_n=1 ch=61 ks=0061
fmt PASS six formats exact
render PASS flus=464C5553 progress=00FFFFFF
input PASS bytes 61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70
(a/up/down/left/right/0/Return/Delete/q/p)
play PASS DIAG: tick=887 quit=0
play_keys PASS move DIAG: loops=354 events=18 focus=00400001 quit=0
panels DIAG: loops=346 events=12 focus=00400001 quit=0
quit DIAG: loops=300 events=10 focus=00400001 quit=1
```
`focus=00400001` is the window the process created; `quit=1` is the game's own
global after `q` — that is the proof the key reached the game and not just the
window.
## Known failure modes (all seen, all diagnosed)
| symptom | cause | handling |
|---|---|---|
| build stage "fails" with exit 139 but produced its file | 2ine-hosted TI tools crash during teardown | `build.sh` retries only when the output is missing; `gspac` needs `-i.` |
| linker points `cinit` into `.text` | empty `.cinit` section | `build.sh` relinks with the one-word `CINITZ.obj` terminator |
| `XNextEvent` never returns an event, keycode zero | child's per-process Xlib context is NULL | `src/vt1200/xgfix.c` writes a zeroed 1 KiB block before `XOpenDisplay` (RAM only) |
| the game exits immediately at startup, sometimes | `XOpenDisplay`/window creation while the display server settles | `main()` retries 40 × 50 ms, then returns 1 |
| window paints once, then hangs; a debug marker stops mid-flush | per-cell flushing overflowed the X socket / `XSync` per row deadlocked the client | flush repaints whole rows (fill + one 80-char string + inverse spans + one `XFlush`) |
| playfield is a white block with invisible pieces | the 1-bit raster cannot show colour | backend drops colour and uses bold (`0x80`) as reverse; pieces are `[`/`]` glyphs |
| blanked well border with stray `X` tiles at the top | `ExposureMask` + map-time Expose runs the game's full redraw before it has a piece | do not select `ExposureMask` (current code); if repainting on expose is ever needed, guard `full_redraw` against `cur_piece < 0` |
## Layout
```
apps/termtris/
├── build.sh TI r5.01 -> COFF (OUTNAME=termtris for the game)
├── run.sh interactive MAME session (lightgun/tablet input)
├── verify.sh one-command acceptance (see above)
├── VT1200.md port documentation
├── src/game.c upstream game; only srand(vt_rand_seed()) differs
├── src/vt1200/ main.c x11.c xgfix.c compat.c xlib.h scoredb.c *.asm
├── tests/ smoke fmt render input play play_keys (+ .lua drivers)
├── evidence/ screenshots
└── build/termtris.coff the built image (tracked for convenience)
```
The tests build from source every run (`play.sh`/`play_keys.sh` rebuild
`termtris.coff`), so an edit cannot silently pass by using a stale binary.
+156
View File
@@ -0,0 +1,156 @@
Termtris - tetris game for terminals
====================================
> **VT1200 port:** this tree also builds a native TMS34010 program that runs
> inside the VT1200 and draws through the terminal's own ROM Xlib. Build it with
> `apps/termtris/build.sh` (see `apps/termtris/VT1200.md`) and run it with
> `apps/termtris/run.sh`. The upstream backends (unix/ANSI, DOS, ADM3) are not
> part of that build.
![shots](http://nuclear.mutantstargoat.com/sw/termtris/img/termtris-banner.png)
About
-----
Termtris is a text-based tetris game, initially written for UNIX systems and
ANSI/VT100-compatible terminals, but later extended to support more terminal
types, operating systems, and text-mode video hardware.
I always liked the GameBoy tetris best. So I made termtris as close as possible
to that version gameplay-wise.
Termtris can run in color or monochrome mode, and can load custom character sets
for graphical blocks, if there is support for that.
There are two main flavors of termtris which can be compiled from the same
source tree:
1. The original **UNIX version** which runs on terminals, and can also be
compiled for windows using a POSIX emulation layer such as *msys2* or
*cygwin*. It has been tested on multiple UNIX systems (GNU/Linux, FreeBSD, IRIX,
Solaris, MacOSX), and multiple terminals (VT52, VT100, VT220, VT420,
MS 6102), and terminal emulators (xterm, linux console, kermit, iTerm2).
2. The **DOS version** which uses the PC BIOS to access display and keyboard,
but can also optionally work with the same terminals supported by the UNIX
version.
### Links
- website: http://nuclear.mutantstargoat.com/sw/termtris
- source repository: https://github.com/jtsiomb/termtris
Controls
--------
- `A` or *left arrow* moves the block left.
- `D` or *right arrow* moves the block right.
- `S` or *down arrow* drops the block faster.
- `W`, *up arrow*, or *space* rotates the block.
- *enter*, *tab*, or `0` drops the block immediately.
- `P` pauses and unpauses the game.
- *backspace*, or *delete* starts a new game.
- `Q` or hitting escape twice, quits immediately.
- `H` shows or hides the help panel which lists these keybindings.
- `R` shows or hides the high score table.
Additionally on GNU/Linux systems any joystick can be used to control the game.
The first available joystick is used automatically, or you can specify a
joystick device with the `-j` commandline option. If you'd rather disable
joystick control, use the option `-j /dev/null`.
When using a joystick, even axes are mapped to left/right, Odd axes are mapped
to up/down, the first four buttons rotate, and the rest of the buttons pause
and unpause.
There is no way to remap the controls without changing the source code at this
time.
Custom character set
--------------------
For terminals with support for custom loadable character sets, you can instruct
termtris to use graphical blocks instead of the bracket characters with the `-g`
command-line argument. If `TERM` is set to a known supported terminal, graphical
blocks are enabled automatically, use the `-T` option to disable and inhibit
auto-detection.
Different terminals have different capabilities when it comes to soft character
sets. Termtris will try to detect the terminal type and choose the correct soft
font to load, but if it fails or you want to force a different terminal type,
make sure to define the `TERM` environent variable. If `TERM` is undefined, or
an unknown or unsupported terminal type, termtris will try to interrogate the
terminal itself about its capabilities.
The DOS version of termtris will automatically enable graphical blocks when it
detects an EGA or VGA/SVGA graphics card. Use `-g` or `-T` to enable or disable
it explicitly.
License
-------
Copyright (C) 2019-2023 John Tsiombikas <nuclear@mutantstargoat.com>
This program is free software. Feel free to use, modify, and/or redistribute it
under the terms of the GNU General Public License version 3, or at your option,
any later version published by the Free Software Foundation. See COPYING for
details.
Download
--------
### Source code
Current release (1.9): http://nuclear.mutantstargoat.com/sw/termtris/termtris-1.9.tar.gz
Releases are also mirrored on github: https://github.com/jtsiomb/termtris/releases
Alternatively you may clone the latest version of the source code from the git
repository:
git clone https://github.com/jtsiomb/termtris
### Pre-compiled
Building from source is highly recommended. If however you want to try a
pre-compiled binary, you might be able to find something for your system in:
http://nuclear.mutantstargoat.com/sw/termtris/bin
Not all releases are compiled for all platforms. Any binaries available here are
provided purely for convenience. Do not open bug reports about a pre-compiled
binary not running, or misbehaving on your system; compile it yourself first.
Another source of binaries, is the github automated builds. Head over to the
"actions" page (https://github.com/jtsiomb/termtris/actions), choose the latest
successful build for your platform, and download the binary from the "artifacts"
section at the bottom.
Pre-compiled binaries might also be available in your package management system of
choice, but please do not submit any bug reports without first trying to build
termtris yourself from the latest git source code.
- Debian/Ubuntu/Mint: `apt-get install termtris`
The latest pre-compiled DOS binary (currently 1.9) is always available at:
http://nuclear.mutantstargoat.com/sw/termtris/termtris.com
Build (UNIX)
------------
There are no external dependencies. Simply type `make` to build, and `make
install` if you wish to install termtris.
Default installation prefix is `/usr/local`. Change the first line of the
`Makefile` if you'd rather install somewhere else, or simply invoke the install
rule like so:
make PREFIX=/some/other/prefix install
If you don't have GNU make and GCC installed, or you'd rather use the default
make utility and C compiler on your UNIX system, you can use `Makefile.sgi`:
make -f Makefile.sgi
(originally included for building on SGI IRIX with MIPSPro and the native make).
Build (DOS)
-----------
To build the DOS version of termtris you'll need some version of the Watcom or
OpenWatcom C compiler, and the NASM assembler:
make -f Makefile.wat
+1
View File
@@ -0,0 +1 @@
7c5435dd4e93ffc10f3b5c68fe1ae9718a365258
+165
View File
@@ -0,0 +1,165 @@
# termtris on the VT1200
`apps/termtris/` is a port of John Tsiombikas' `termtris`
(https://github.com/jtsiomb/termtris, commit `7c5435dd4e93ffc10f3b5c68fe1ae9718a365258`)
that runs **inside the VT1200 as a native TMS34010 program**, drawing through
the terminal's own ROM Xlib and reading real X11 key events from the local X
server. There is no host process and no terminal escape output: the COFF is
loaded into VT1200 RAM and executed by the TMS34010.
![termtris board](evidence/termtris_frame3300.png)
## What runs where
| piece | file | role |
|---|---|---|
| game rules | `src/game.c` (upstream) | unchanged logic; one portability edit (`srand((now_ms()))` instead of `time(0)`) |
| term callbacks | `src/vt1200/x11.c` | 80×24 character-cell grid drawn with `XFillRectangle`/`XDrawString` |
| event loop | `src/vt1200/main.c` | `XNextEvent` + `XLookupString`, key map, millisecond clock, `sleep()` |
| scores | `src/vt1200/scoredb.c` | RAM-only high-score table (no filesystem) |
| libc shims | `src/vt1200/compat.c`, `stdio.h`, `inttypes.h` | `sprintf`/`fflush`/`putchar`, fixed-width types |
| Xlib | `src/vt1200/xlib.h` | ROM entry points by bit address, called with the TI C ABI |
| startup | `src/vt1200/entry.asm`, `cinit.asm`, `cinitz.asm` | set `A14 = _cstack`, run the `.cinit` table, `calla _main` |
| toolchain | `build.sh` | TI TMS340 Family Code Generation Tools r5.01 (1990-11) under 2ine |
## Building
```sh
apps/termtris/build.sh src/vt1200/main.c src/game.c src/vt1200/scoredb.c \
src/vt1200/x11.c src/vt1200/compat.c src/vt1200/xgfix.c
```
`build.sh` unpacks the r5.01 SDK zips, runs `gspac`/`gspopt`/`gspcg`/`gspa` and
`gsplnk` through `lx_loader`, and writes `build/<main>.coff` + `.map`. Set
`NINE=` to the 2ine build directory (default `/home/arti/os2-work/2ine/build`).
The 2ine-hosted tools routinely segfault *after* producing their output, so the
script treats an output file as success and retries a missing one; `gspac` needs
`-i.` for the angle-bracket shim headers.
Link map (relevant addresses are **bit** addresses, like everything the
TMS34010 sees):
```
.text 018A0000 .data 018A0000 .cinit 018B4B10
.bss 01A00000
ENTRY POINT SYMBOL: "_start" address: 018a0000
```
An empty `.cinit` makes the linker point `cinit` at `.text`, and the entry
walker would then read an instruction as a record count; when the map says the
section is empty `build.sh` relinks with the one-word `CINITZ.obj` terminator.
## The Xlib context (the one thing that had to be discovered)
A freshly `create_process`ed child has `*(*(proc + 0x100)) == NULL`. The ROM
Xlib's `_XEnq` does `XG = *(*(proc+0x100)); free_list = *(XG + 0x198)`, so
`XNextEvent` reads low-RAM garbage as queue nodes and delivers nothing. The
port installs a zeroed 1 KiB block as the per-process Xlib context before
`XOpenDisplay` (`src/vt1200/xgfix.c`), which is a RAM write only — the ROM is
untouched. With that in place the smoke test receives a real
`KeyPress (type=2, keycode=194)` and `XLookupString` returns `a`.
## Rendering
The frame is an 80×24 grid of 6×13 cells (the default GC font, baseline +10),
so the window is 480×312. The VT1200 raster is **one bit deep**, so the backend
renders like the game's monochrome mode (colour is dropped) and uses the bold
bit (`0x80`) as reverse video for emphasis — a choice of this backend, not the
upstream ANSI convention (which emits SGR bold and leaves the colours alone). The playfield is therefore black
spaces and the tetrominoes are the `[`/`]` tiles, exactly what a 1-bit terminal
can show.
`vt_x11_flush()` repaints whole rows rather than cells: one `XFillRectangle`
for the row background, one `XDrawString` for its 80 characters, plus
fill/string pairs for reversed spans, then a single `XFlush`. This matters. An
earlier per-cell version (four requests per cell) filled the X socket faster
than the in-machine server could drain it, and an `XSync` per row deadlocked
the client: the debug progress marker stopped at row 20 (`0x00010014`) and the
game never reached its main loop. Batching the row makes a full 80×24 redraw a
few hundred bytes of requests and the loop runs (measured: 300+ iterations
between frames 2500 and 3200).
## Timing and input
`now_ms()` accumulates the 16-bit counter at bit `0x1E93C0` into a 32-bit
millisecond count; `sleep_ms()` calls the ROM `sleep` at `0x3FB33A20` with a
1–10 ms clamp. The game's own `update()` decides the tick interval (887 ms at
level 0).
Keys arrive as X11 `KeyPress` events and go through `XLookupString`; letters are
lower-cased and handed to `game_input()`. Arrows are sent as the game's own CSI
codes (`0x9b` + `A`/`B`/`C`/`D`), which `game_input` translates back to
`w`/`s`/`d`/`a`. `0x7F` is Backspace/Delete. `FocusOut` pauses and `FocusIn`
resumes (only if we paused it).
`ExposureMask` is deliberately **not** selected: the map-time `Expose` arrives
before the game has a current piece, and the game's full-redraw path then draws
a non-existent piece over the top row — visible as a blanked border with stray
`X` tiles. The window is never obscured in this session (the Terminal Manager
sits beside it), so the mask buys nothing. See `HANDOFF.md`.
## Running it in MAME
The emulator must be built from the driver in this tree; an older
`/home/arti/code/k-space/mame0289/vt1200` may still carry the pre-VR319
geometry:
```sh
harness/mame/build.sh # rebuild the vt1200 subtarget (JOBS=4 on small hosts)
```
Then:
```sh
apps/termtris/verify.sh # build MAME + run every test (see HANDOFF.md)
apps/termtris/tests/play.sh # load, click, snapshot
apps/termtris/tests/play_keys.sh # move / panels / quit acceptance runs
apps/termtris/run.sh # same run, interactive session
```
Under the hood the loader payload is dropped at bit `0x01880000` and the COFF at
`0x01900000`; at frame 2500 the script enters `dwt/loader/build/loader.bin`,
which copies the sections to their linked addresses and calls
`create_process`. The X server is already up because the run uses
`VT1200_SKIP_LEVEL1=1` to reach the OS.
## Evidence
| file | what it shows |
|---|---|
| `evidence/termtris_frame3300.png` | the board, score/level/lines panels, preview and `Toggle (H)elp` |
| `evidence/termtris_move_3200.png` | after `a d s w 0`: a piece has been moved/rotated/dropped to the bottom-left and a new one spawned |
| `evidence/termtris_panels_3200.png` | after `h` and `r`: the help text and the high-score list (all `VT1200`) |
| `evidence/termtris_quit_3200.png` | after `q`: same screen, and the run reports `quit=1` |
| `evidence/render_frame3400.png` | the cell-grid renderer test pattern |
Acceptance values from the last full run (`play_keys.sh`):
```
move DIAG: loops=354 events=18 focus=00400001 quit=0
panels DIAG: loops=346 events=12 focus=00400001 quit=0
quit DIAG: loops=300 events=10 focus=00400001 quit=1
```
`focus=00400001` is the window this process created (it is the only one in the
run), so the key events were delivered to it, and `quit=1` is the game's own
global after `q`.
## Limits, stated plainly
* **No sound.** The game's bell is a no-op.
* **Scores are RAM-only.** `read_scores`/`save_score` keep ten entries in
`.bss`; they vanish when the process exits. The VT1200 has no writable
filesystem for a child.
* **Startup is retried.** If `XOpenDisplay`/window creation fails while the
display server is still settling, `main()` retries up to 40 times with a
50 ms sleep before giving up. Observed as an intermittent early exit.
* **`q` ends the game loop but the window stays on screen.** `quit=1` is
observable in RAM and `main()` returns; the frame captured about a second
later still shows the window, so the X connection is not torn down by process
exit in this OS. Treat that as an observation, not a designed behaviour.
* **The board is text.** Tetrominoes are `[`/`]` tiles, not filled blocks — the
only thing a 1-bit cell can show without a custom font.
The ROM (`vt1200_v22.bin`, md5 `b46a3e442ccb86134d2356bd70d22fd2`) is never
modified; the port lives entirely in RAM.
+170
View File
@@ -0,0 +1,170 @@
#!/bin/sh
# build.sh -- build a VT1200 COFF with TI TMS34010 r5.01 under 2ine.
#
# apps/termtris/build.sh SOURCE.c [SOURCE.c ...] [EXTRA.obj ...]
#
# The first .c is the main program; the output is build/<main>.coff. entry.asm
# and cinit.asm are always assembled and linked. The 2ine loader is outside the
# repo: point NINE at its build directory (default /home/arti/os2-work/2ine/build).
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$DIR/../.." && pwd)
WORK="$DIR/work"
OUT="$DIR/build"
NINE="${NINE:-/home/arti/os2-work/2ine/build}"
LX="$NINE/lx_loader"
SDK="$ROOT/tools/tms34010-sdk/1990-11 TMS340 Family SDK"
Z1="$SDK/1990-11-19 TMS340 Family Code Generation Tools (1 of 2) r5.01 rev2564062-1641.zip"
Z2="$SDK/1990-11-19 TMS340 Family Code Generation Tools (2 of 2) r5.01 rev2564062-1641.zip"
if [ ! -x "$LX" ]; then
echo "build.sh: no 2ine lx_loader at $LX (set NINE=...)" >&2
exit 1
fi
if [ ! -f "$Z1" ] || [ ! -f "$Z2" ]; then
echo "build.sh: r5.01 SDK zips not found under $SDK" >&2
exit 1
fi
mkdir -p "$WORK" "$OUT"
if [ ! -x "$WORK/gspac.exe" ]; then
unzip -oq "$Z1" -d "$WORK"
unzip -oq "$Z2" -d "$WORK"
fi
for so in "$NINE"/lib*.so; do
[ -e "$so" ] && ln -sf "$so" "$WORK/"
done
cp "$DIR/src/vt1200/"*.h "$WORK/" 2>/dev/null || true
cp "$DIR/src/"*.h "$WORK/" 2>/dev/null || true
cp "$DIR/src/vt1200/entry.asm" "$DIR/src/vt1200/cinit.asm" "$DIR/src/vt1200/cinitz.asm" "$WORK/"
# Collect objects. C sources are copied under their uppercase basename because
# the OS/2 tools are case-sensitive on this host.
OBJS=""
MAIN=""
for arg in "$@"; do
case "$arg" in
*.c)
base=$(basename "$arg" .c | tr 'a-z' 'A-Z')
[ -n "$MAIN" ] || MAIN="$base"
cp "$arg" "$WORK/$base.C"
OBJS="$OBJS $base"
;;
*.obj)
cp "$arg" "$WORK/"
OBJS="$OBJS $(basename "$arg")"
;;
*.asm)
base=$(basename "$arg" .asm | tr 'a-z' 'A-Z')
cp "$arg" "$WORK/$base.ASM"
OBJS="$OBJS $base"
;;
*)
echo "build.sh: unsupported input: $arg" >&2
exit 1
;;
esac
done
if [ -z "$MAIN" ]; then
echo "build.sh: give at least one .c source" >&2
exit 1
fi
# The 2ine-hosted tools frequently crash during teardown after doing their work
# (exit 139 / 134). Treat an output file as success, and retry a missing one.
run() {
_out=$1
shift
i=0
while [ "$i" -lt 5 ]; do
( cd "$WORK" && "$LX" "$@" ) >/dev/null 2>&1 || true
if [ -f "$WORK/$_out" ]; then
return 0
fi
i=$((i + 1))
done
echo "build.sh: stage failed to produce $WORK/$_out: $*" >&2
return 1
}
compile_c() {
base=$1
rm -f "$WORK/$base.IF" "$WORK/$base.OPT" "$WORK/$base.ASM" "$WORK/$base.obj"
run "$base.IF" ./gspac.exe -k -i. "$base.C" "$base.IF"
run "$base.OPT" ./gspopt.exe -k -O2 "$base.IF" "$base.OPT"
run "$base.ASM" ./gspcg.exe -m "$base.OPT" "$base.ASM" "$base.TMP"
run "$base.obj" ./gspa.exe "$base.ASM"
}
compile_asm() {
base=$1
rm -f "$WORK/$base.obj"
run "$base.obj" ./gspa.exe "$base.ASM"
}
# Compile every collected object exactly once, in the order given.
for arg in "$@"; do
case "$arg" in
*.c) compile_c "$(basename "$arg" .c | tr 'a-z' 'A-Z')" ;;
*.asm) compile_asm "$(basename "$arg" .asm | tr 'a-z' 'A-Z')" ;;
esac
done
compile_asm ENTRY
compile_asm CINIT
compile_asm CINITZ
OUTNAME=${OUTNAME:-$(printf '%s' "$MAIN" | tr 'A-Z' 'a-z')}
CMD="$WORK/$MAIN.CMD"
{
printf '%s\n' '-a'
printf '%s\n' "-o $MAIN.out"
printf '%s\n' "-m $MAIN.map"
printf '%s\n' '-e _start'
printf '%s\n' 'CINIT.obj'
printf '%s\n' 'ENTRY.obj'
for arg in "$@"; do
case "$arg" in
*.c) printf '%s\n' "$(basename "$arg" .c | tr 'a-z' 'A-Z').obj" ;;
*.asm) printf '%s\n' "$(basename "$arg" .asm | tr 'a-z' 'A-Z').obj" ;;
*.obj) printf '%s\n' "$(basename "$arg")" ;;
esac
done
printf '%s\n' '-l rts.lib'
printf '%s\n' 'MEMORY'
printf '%s\n' '{'
printf '%s\n' ' prog: origin = 018A0000h, length = 20000h'
printf '%s\n' ' ram: origin = 01A00000h, length = 40000h'
printf '%s\n' '}'
printf '%s\n' 'SECTIONS'
printf '%s\n' '{'
printf '%s\n' ' .text: {} > prog'
printf '%s\n' ' .data: {} > prog'
printf '%s\n' ' .cinit: {} > prog'
printf '%s\n' ' .bss: {} > ram'
printf '%s\n' '}'
} > "$CMD"
sed -i 's/$/\r/' "$CMD"
rm -f "$WORK/$MAIN.out" "$WORK/$MAIN.map"
run "$MAIN.out" ./gsplnk.exe "$MAIN.CMD"
if [ ! -f "$WORK/$MAIN.map" ]; then
echo "build.sh: no link map" >&2
exit 1
fi
# An empty .cinit makes the linker's `cinit` symbol point at .text; the entry's
# cinit walker would then treat an instruction as a record count. Relink with
# a one-word terminator when the map says the section is empty.
CSIZE=$(awk '$1 == ".cinit" {print $4; exit}' "$WORK/$MAIN.map")
if [ "$CSIZE" = "00000000" ]; then
sed -i 's/CINIT\.obj/CINITZ.obj/' "$CMD"
rm -f "$WORK/$MAIN.out" "$WORK/$MAIN.map"
run "$MAIN.out" ./gsplnk.exe "$MAIN.CMD"
fi
cp "$WORK/$MAIN.out" "$OUT/$OUTNAME.coff"
sed 's/\r$//' "$WORK/$MAIN.map" > "$OUT/$OUTNAME.map"
echo "build.sh: $OUT/$OUTNAME.coff ($(wc -c < "$OUT/$OUTNAME.coff") bytes)"
sed -n '1,40p' "$OUT/$OUTNAME.map"
Binary file not shown.
+141
View File
@@ -0,0 +1,141 @@
*****************************************************
TMS340 COFF Linker Version 5.01
*****************************************************
Thu Nov 12 05:18:17 2026
OUTPUT FILE NAME: <MAIN.out>
ENTRY POINT SYMBOL: "_start" address: 018a0000
MEMORY CONFIGURATION
name origin length attributes
-------- -------- --------- ----------
prog 018a0000 000020000 RWIX
ram 01a00000 000040000 RWIX
SECTION ALLOCATION MAP
output attributes/
section page origin length input sections
-------- ---- ---------- ---------- ----------------
.text 0 018a0000 00014a60
018a0000 00000150 ENTRY.obj (.text)
018a0150 00001db0 MAIN.obj (.text)
018a1f00 0000ce50 GAME.obj (.text)
018aed50 00000d20 SCOREDB.obj (.text)
018afa70 00002a40 X11.obj (.text)
018b24b0 00001640 COMPAT.obj (.text)
018b3af0 00000210 XGFIX.obj (.text)
018b3d00 00000450 rts.lib (.text)
018b4150 000001a0 rts.lib (.text)
018b42f0 00000230 rts.lib (.text)
018b4520 00000150 rts.lib (.text)
018b4670 00000120 rts.lib (.text)
018b4790 00000110 rts.lib (.text)
018b48a0 000001c0 rts.lib (.text)
.cinit 0 018b4a60 000012d0
018b4a60 00000050 MAIN.obj (.cinit)
018b4ab0 00001190 GAME.obj (.cinit)
018b5c40 00000050 SCOREDB.obj (.cinit)
018b5c90 00000050 X11.obj (.cinit)
018b5ce0 00000050 rts.lib (.cinit)
.bss 0 01a00000 0001ec20 UNINITIALIZED
01a00000 00008000 ENTRY.obj (.bss)
01a08000 00000420 MAIN.obj (.bss)
01a08420 00004a40 GAME.obj (.bss)
01a0ce60 00000b80 SCOREDB.obj (.bss)
01a0d9e0 0000f200 X11.obj (.bss)
01a1cbe0 00000020 COMPAT.obj (.bss)
01a1cc00 00002000 XGFIX.obj (.bss)
01a1ec00 00000020 rts.lib (.bss)
.data 0 018a0000 00000000 UNINITIALIZED
GLOBAL SYMBOLS
address name address name
-------- ---- -------- ----
01a00000 .bss 018a0000 .data
018a0000 .data 018a0000 .text
018a0000 .text 018a0000 _start
01a08a20 _blackpf 018a0000 edata
018a3060 _cleanup_game 018a0150 _now_ms
01a083a0 _dbg_events 018a03d0 _vt_rand_seed
01a083e0 _dbg_focus 018a0420 _sleep_ms
01a083c0 _dbg_last 018a0580 _vt_poll_event
01a08380 _dbg_loops 018a0ef0 _vt_handle_event
01a08400 _dbg_rev 018a1820 _wait_display
018b24b0 _fflush 018a1860 _main
018a8600 _game_input 018a1f00 _init_game
018a1f00 _init_game 018a3060 _cleanup_game
018a1860 _main 018a3250 _update
018b3d00 _memcpy 018a8600 _game_input
018b3ea0 _memmove 018af0b0 _read_scores
018b4150 _memset 018af430 _save_score
01a089c0 _monochrome 018af9c0 _print_scores
018a0150 _now_ms 018afa70 _vt_root_window
01a08a00 _onlyascii 018afb50 _vt_x11_init
018af9c0 _print_scores 018b0300 _x11_clearscr
018b2520 _putchar 018b04c0 _x11_reset
01a08960 _quit 018b0500 _x11_cursor
018b42f0 _rand 018b0560 _x11_setcolor
018af0b0 _read_scores 018b0680 _x11_setcursor
01a08a40 _rotstep 018b08d0 _x11_ibmchar
018af430 _save_score 018b0d50 _term_putstr
01a08aa0 _scr 018b0ef0 _vt_x11_flush
018a0420 _sleep_ms 018b2120 _vt_x11_term_init
018b2d30 _sprintf 018b2440 _term_init
018b4450 _srand 018b24b0 _fflush
018a0000 _start 018b2520 _putchar
01a1cbe0 _stdout 018b2d30 _sprintf
018b4520 _strcmp 018b3af0 _vt_install_xg
018b4670 _strcpy 018b3d00 _memcpy
018b4790 _strlen 018b3ea0 _memmove
018b48a0 _strncpy 018b4150 _memset
01a1cb00 _term_clearscr 018b42f0 _rand
01a1cb40 _term_cursor 018b4450 _srand
01a08a80 _term_height 018b4520 _strcmp
01a1cb80 _term_ibmchar 018b4670 _strcpy
018b2440 _term_init 018b4790 _strlen
018b0d50 _term_putstr 018b48a0 _strncpy
01a1cae0 _term_reset 018b4a60 cinit
01a1cb60 _term_setcolor 018b4a60 etext
01a1cb20 _term_setcursor 01a00000 .bss
01a1cbc0 _term_type 01a08380 _dbg_loops
01a08a60 _term_width 01a083a0 _dbg_events
01a1cba0 _termenv 01a083c0 _dbg_last
01a08980 _tick_interval 01a083e0 _dbg_focus
018a3250 _update 01a08400 _dbg_rev
01a089a0 _use_bell 01a08960 _quit
01a089e0 _use_gfxchar 01a08980 _tick_interval
01a0ce60 _username 01a089a0 _use_bell
01a1ca60 _vt_dpy 01a089c0 _monochrome
01a1caa0 _vt_gc 01a089e0 _use_gfxchar
018a0ef0 _vt_handle_event 01a08a00 _onlyascii
018b3af0 _vt_install_xg 01a08a20 _blackpf
018a0580 _vt_poll_event 01a08a40 _rotstep
018a03d0 _vt_rand_seed 01a08a60 _term_width
018afa70 _vt_root_window 01a08a80 _term_height
01a1ca80 _vt_win 01a08aa0 _scr
018b0ef0 _vt_x11_flush 01a0ce60 _username
018afb50 _vt_x11_init 01a1ca60 _vt_dpy
01a1cac0 _vt_x11_progress 01a1ca80 _vt_win
018b2120 _vt_x11_term_init 01a1caa0 _vt_gc
018a1820 _wait_display 01a1cac0 _vt_x11_progress
018b0300 _x11_clearscr 01a1cae0 _term_reset
018b0500 _x11_cursor 01a1cb00 _term_clearscr
018b08d0 _x11_ibmchar 01a1cb20 _term_setcursor
018b04c0 _x11_reset 01a1cb40 _term_cursor
018b0560 _x11_setcolor 01a1cb60 _term_setcolor
018b0680 _x11_setcursor 01a1cb80 _term_ibmchar
018b4a60 cinit 01a1cba0 _termenv
018a0000 edata 01a1cbc0 _term_type
01a1ec20 end 01a1cbe0 _stdout
018b4a60 etext 01a1ec20 end
[77 symbols]
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

+40
View File
@@ -0,0 +1,40 @@
#!/bin/sh
# run.sh -- load termtris into a running VT1200 OS under MAME and play it.
#
# apps/termtris/run.sh # click the window active, then play
# apps/termtris/run.sh --build # rebuild the COFF first
#
# The emulated keyboard and tablet are driven by MAME's host input, so the
# terminal's X cursor follows the host pointer. Quit with Ctrl-C in the
# terminal running MAME, or press 'q' in the game (it stops the game loop).
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$DIR/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
LOADER="$ROOT/dwt/loader/build/loader.bin"
COFF="$DIR/build/termtris.coff"
if [ "${1:-}" = "--build" ]; then
"$DIR/build.sh" "$DIR/src/vt1200/main.c" "$DIR/src/game.c" \
"$DIR/src/vt1200/scoredb.c" "$DIR/src/vt1200/x11.c" \
"$DIR/src/vt1200/compat.c" "$DIR/src/vt1200/xgfix.c"
fi
if [ ! -f "$COFF" ]; then
echo "run.sh: no $COFF (run with --build)" >&2
exit 1
fi
if [ ! -f "$LOADER" ]; then
echo "run.sh: no loader payload at $LOADER" >&2
exit 1
fi
# The game is loaded at frame 2500, once the OS X server is up.
exec env VT1200_SKIP_LEVEL1=1 \
LOADER_AT=2500 LOADER_END=100000000 \
LOADER_BIN="$LOADER" LOADER_COFF="$COFF" LOADER_OUT="$DIR/build" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-skip_gameinfo -debug -debugger none \
-lightgun -lightgun_device mouse \
-autoboot_script "$DIR/tests/play.lua" -autoboot_delay 0
File diff suppressed because it is too large. Load diff
+43
View File
@@ -0,0 +1,43 @@
/*
Termtris - a tetris game for ANSI/VT100 terminals
Copyright (C) 2019-2023 John Tsiombikas <nuclear@member.fsf.org>
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/>.
*/
#ifndef GAME_H_
#define GAME_H_
extern int quit;
extern long tick_interval;
extern int use_bell;
extern int monochrome;
extern int use_gfxchar;
extern int onlyascii;
extern int blackpf;
extern int rotstep;
extern int term_width, term_height;
int init_game(void);
void cleanup_game(void);
long update(long msec);
void game_input(int c);
/* wait for any pending drawing to be completed before proceeding
* implemented in main.c
*/
void wait_display(void);
#endif /* GAME_H_ */
+84
View File
@@ -0,0 +1,84 @@
/*
Termtris - a tetris game for ANSI/VT100 terminals
Copyright (C) 2019-2022 John Tsiombikas <nuclear@member.fsf.org>
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/>.
*/
#ifndef PIECES_H_
#define PIECES_H_
#define BLK(x, y) ((x) | ((y) << 4))
#define BLKX(c) ((unsigned char)(c) & 0xf)
#define BLKY(c) ((unsigned char)(c) >> 4)
#define NUM_PIECES 7
static unsigned char pieces[NUM_PIECES][4][4] = {
/* L block */
{
{BLK(0, 1), BLK(0, 2), BLK(1, 1), BLK(2, 1)},
{BLK(0, 0), BLK(1, 0), BLK(1, 1), BLK(1, 2)},
{BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(2, 0)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(2, 2)}
},
/* J block */
{
{BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(2, 2)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(0, 2)},
{BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(2, 1)},
{BLK(1, 0), BLK(2, 0), BLK(1, 1), BLK(1, 2)}
},
/* I block */
{
{BLK(0, 2), BLK(1, 2), BLK(2, 2), BLK(3, 2)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(1, 3)},
{BLK(0, 2), BLK(1, 2), BLK(2, 2), BLK(3, 2)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(1, 3)}
},
/* O block */
{
{BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)},
{BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)},
{BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)},
{BLK(1, 1), BLK(2, 1), BLK(1, 2), BLK(2, 2)}
},
/* Z block */
{
{BLK(0, 1), BLK(1, 1), BLK(1, 2), BLK(2, 2)},
{BLK(0, 1), BLK(1, 1), BLK(1, 0), BLK(0, 2)},
{BLK(0, 1), BLK(1, 1), BLK(1, 2), BLK(2, 2)},
{BLK(0, 1), BLK(1, 1), BLK(1, 0), BLK(0, 2)}
},
/* S block */
{
{BLK(1, 1), BLK(2, 1), BLK(0, 2), BLK(1, 2)},
{BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(1, 2)},
{BLK(1, 1), BLK(2, 1), BLK(0, 2), BLK(1, 2)},
{BLK(0, 0), BLK(0, 1), BLK(1, 1), BLK(1, 2)}
},
/* T block */
{
{BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(1, 2)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(0, 1)},
{BLK(0, 1), BLK(1, 1), BLK(2, 1), BLK(1, 0)},
{BLK(1, 0), BLK(1, 1), BLK(1, 2), BLK(2, 1)}
}
};
static int piece_spawnpos[NUM_PIECES][2] = {
{-1, -2}, {-1, -2}, {-2, -2}, {-1, -2}, {-1, -2}, {-1, -2}, {-1, -2}
};
#endif /* PIECES_H_ */
+45
View File
@@ -0,0 +1,45 @@
/*
Termtris - a tetris game for ANSI/VT100 terminals
Copyright (C) 2019-2023 John Tsiombikas <nuclear@member.fsf.org>
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/>.
*/
#ifndef SCOREDB_H_
#define SCOREDB_H_
#include <stdio.h>
#if defined(MSDOS) || defined(__COM__)
/* the DOS version of score_entry is simpler, because scores are embedded in
* the executable itself in binary format.
*/
#define MAX_NAME 36
struct score_entry {
char user[MAX_NAME];
long score, lines, level;
struct score_entry *next;
};
#else
struct score_entry {
char *user;
long score, lines, level;
struct score_entry *next;
};
#endif
struct score_entry *read_scores(FILE *fp, int max_scores);
int save_score(struct score_entry *sc);
int print_scores(int num);
#endif /* SCOREDB_H_ */
+62
View File
@@ -0,0 +1,62 @@
/*
Termtris - a tetris game for TERM/VT100 terminals
Copyright (C) 2019-2022 John Tsiombikas <nuclear@member.fsf.org>
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/>.
*/
#ifndef TERM_H_
#define TERM_H_
enum {
G_DIAMOND = 0x04,
G_CHECKER = 0xb1,
G_LR_CORNER = 0xd9,
G_UR_CORNER = 0xbf,
G_UL_CORNER = 0xda,
G_LL_CORNER = 0xc0,
G_CROSS = 0xc5,
G_HLINE = 0xc4,
G_L_TEE = 0xc3,
G_R_TEE = 0xb4,
G_B_TEE = 0xc1,
G_T_TEE = 0xc2,
G_VLINE = 0xb3,
G_CDOT = 0xf8
};
enum { BLACK, BLUE, GREEN, CYAN, RED, MAGENTA, YELLOW, WHITE };
#define BOLD 8
enum {
TERM_ANSI,
TERM_VT52,
TERM_ADM3,
TERM_PCBIOS = 0xb105
};
extern char *termenv;
extern int term_type;
extern void (*term_reset)(void);
extern void (*term_clearscr)(void);
extern void (*term_setcursor)(int row, int col);
extern void (*term_cursor)(int show);
extern void (*term_setcolor)(int fg, int bg);
/* convert a PC cga/ega/vga char+attr to an TERM sequence and write it to stdout */
extern void (*term_ibmchar)(unsigned char c, unsigned char attr);
void term_init(void);
void term_putstr(const char *s, unsigned char attr);
#endif /* TERM_H_ */
+4
View File
@@ -0,0 +1,4 @@
.sect ".cinit"
.globl cinit
cinit:
.end
+5
View File
@@ -0,0 +1,5 @@
.sect ".cinit"
.globl cinit
cinit:
.word 0
.end
+137
View File
@@ -0,0 +1,137 @@
/*
* compat.c -- the small libc surface termtris needs on the VT1200.
*
* The TI r5.01 runtime has no stdio and no sprintf. This implements exactly
* the decimal/string formats game.c uses. Arguments are walked manually
* because the SDK's va_arg macro does not survive the 1990 compiler's
* preprocessor here; the TI ABI lays the varargs contiguously below &fmt.
*/
#include <stdio.h>
FILE *stdout;
extern int strlen(char *);
int fflush(FILE *fp)
{
(void)fp;
return 0;
}
int putchar(int c)
{
(void)c;
return c;
}
static char *fmt_num(char *out, long v, int width, int zero, int left, int unsig)
{
char tmp[24];
int n = 0, neg = 0, pad, i;
unsigned long u;
if(v < 0 && !unsig) {
neg = 1;
u = (unsigned long)(-v);
} else {
u = (unsigned long)v;
}
do {
tmp[n++] = (char)('0' + (u % 10UL));
u /= 10UL;
} while(u);
pad = width - (n + neg);
if(pad < 0) pad = 0;
if(left) {
if(neg) *out++ = '-';
while(n) *out++ = tmp[--n];
for(i = 0; i < pad; i++) *out++ = ' ';
} else if(zero) {
if(neg) *out++ = '-';
for(i = 0; i < pad; i++) *out++ = '0';
while(n) *out++ = tmp[--n];
} else {
for(i = 0; i < pad; i++) *out++ = ' ';
if(neg) *out++ = '-';
while(n) *out++ = tmp[--n];
}
return out;
}
int sprintf(char *buf, char *fmt, ...)
{
char *out = buf;
char *ap = (char *)&fmt;
while(*fmt) {
int left = 0, zero = 0, width = 0, unsig = 0, islong = 0;
if(*fmt != '%') {
*out++ = *fmt++;
continue;
}
fmt++;
if(*fmt == '%') {
*out++ = '%';
fmt++;
continue;
}
if(*fmt == '-') { left = 1; fmt++; }
if(*fmt == '0') { zero = 1; fmt++; }
while(*fmt >= '0' && *fmt <= '9') {
width = width * 10 + (*fmt - '0');
fmt++;
}
if(*fmt == 'l') { islong = 1; fmt++; }
if(*fmt == 'u') { unsig = 1; }
switch(*fmt) {
case 'd':
case 'u':
{
long v;
ap -= sizeof(long);
v = *(long *)ap;
(void)islong;
out = fmt_num(out, v, width, zero, left, unsig);
fmt++;
}
break;
case 's':
{
char *s;
int len, i, pad;
ap -= sizeof(char *);
s = *(char **)ap;
if(!s) s = "(null)";
len = strlen(s);
pad = width - len;
if(pad < 0) pad = 0;
if(!left) for(i = 0; i < pad; i++) *out++ = ' ';
for(i = 0; i < len; i++) *out++ = s[i];
if(left) for(i = 0; i < pad; i++) *out++ = ' ';
fmt++;
}
break;
case 'c':
{
int c;
ap -= sizeof(int);
c = *(int *)ap;
*out++ = (char)c;
fmt++;
}
break;
default:
*out++ = '%';
if(*fmt) *out++ = *fmt++;
break;
}
}
*out = 0;
return (int)(out - buf);
}
+28
View File
@@ -0,0 +1,28 @@
.text
.globl _start
.globl _main
.globl cinit
_start:
setf 32,0,1
movi _cstack,a14
move a14,a13
; process the .cinit table: u16 word count, u32 destination, data words
movi cinit,a9
ci_loop:
setf 16,0,0
move *a9+,a11
jrz ci_done
move *a9+,a10,1
ci_copy:
move *a9+,*a10+
dsjs a11,ci_copy
jruc ci_loop
ci_done:
setf 32,0,1
move a13,a14
calla _main
rets
.bss _cstack,32768,16
.end
+13
View File
@@ -0,0 +1,13 @@
/*
* inttypes.h -- the fixed-width types game.c uses, for the 1990 TI compiler.
*/
#ifndef VT_INTTYPES_H_
#define VT_INTTYPES_H_
typedef unsigned char uint8_t;
typedef signed short int16_t;
typedef unsigned short uint16_t;
typedef signed long int32_t;
typedef unsigned long uint32_t;
#endif
+171
View File
@@ -0,0 +1,171 @@
/*
* main.c -- the VT1200 termtris event loop.
*
* The game's update()/init_game() draw through the backend's term_* callbacks;
* this file owns the X11 event drain, the real-time clock, and the key map.
*/
#include "game.h"
#include "x11.h"
#include "xlib.h"
extern int init_game(void);
extern long update(long msec);
extern void game_input(int c);
extern int vt_install_xg(void);
static vt_u32 evbuf[24];
static long ms_base;
static int ms_last = -1;
static int focused;
static int vt_paused;
volatile vt_u32 dbg_loops, dbg_events, dbg_last, dbg_focus, dbg_rev;
long now_ms(void)
{
int now;
if(ms_last < 0) {
ms_last = *(volatile unsigned short *)0x001E93C0UL;
ms_base = 0;
return 0;
}
now = *(volatile unsigned short *)0x001E93C0UL;
ms_base += (long)(unsigned short)(now - ms_last);
ms_last = now;
return ms_base;
}
/*
* Seed for the game's PRNG. now_ms() returns 0 on its first call (it only
* latches the counter there), so seeding from it gives a constant; read the
* free-running 16-bit tick directly instead.
*/
unsigned vt_rand_seed(void)
{
return (unsigned)*(volatile unsigned short *)0x001E93C0UL;
}
void sleep_ms(int ms)
{
if(ms < 1) ms = 1;
if(ms > 10) ms = 10;
VT_SLEEP(ms);
}
int vt_poll_event(void)
{
return *(vt_u32 *)((char *)vt_dpy + 0x54) != 0;
}
static void map_keysym(vt_u32 ks, char ch, int n)
{
switch(ks) {
case 0xFF51UL: /* Left */
game_input(0x9b); game_input('D');
return;
case 0xFF53UL: /* Right */
game_input(0x9b); game_input('C');
return;
case 0xFF52UL: /* Up */
game_input(0x9b); game_input('A');
return;
case 0xFF54UL: /* Down */
game_input(0x9b); game_input('B');
return;
case 0xFF0DUL: /* Return */
game_input('\n');
return;
case 0xFF08UL: /* BackSpace */
game_input('\b');
return;
case 0xFF09UL: /* Tab */
game_input('\t');
return;
case 0xFF1BUL: /* Escape */
game_input(27);
return;
default:
break;
}
if(n > 0) {
if(ch >= 'A' && ch <= 'Z') ch = (char)(ch + ('a' - 'A'));
game_input((int)(unsigned char)ch);
}
}
void vt_handle_event(void)
{
char buf[8];
vt_u32 ks;
int n;
VT_XNEXT(vt_dpy, (void *)evbuf);
dbg_events = dbg_events + 1;
dbg_last = evbuf[0];
if(evbuf[0] == 2) { /* KeyPress */
ks = 0;
n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0);
map_keysym(ks, n > 0 ? buf[0] : 0, n);
} else if(evbuf[0] == 5) { /* ButtonRelease: sample focus */
vt_u32 fw;
int rv;
VT_XGETFOCUS(vt_dpy, &fw, &rv);
dbg_focus = fw;
dbg_rev = (vt_u32)rv;
} else if(evbuf[0] == 9) { /* FocusIn */
focused = 1;
if(vt_paused) { /* resume only what we paused */
game_input('p');
vt_paused = 0;
}
} else if(evbuf[0] == 10) { /* FocusOut */
if(focused && !vt_paused) {
game_input('p'); /* 'p' toggles pause in game_input */
vt_paused = 1;
}
focused = 0;
}
}
void wait_display(void)
{
vt_x11_flush();
}
int main(void)
{
long d;
/* The display server may still be coming up when this process is
* created; retry for a while before giving up. */
{
int tries;
for(tries = 0; tries < 40; tries++) {
if(vt_x11_init() == 0) break;
VT_SLEEP(50);
}
if(tries == 40) return 1;
}
onlyascii = 1;
use_gfxchar = 0;
monochrome = 1; /* the VT1200 raster is 1 bit deep */
if(init_game() == -1)
return 2;
vt_x11_flush();
for(;;) {
dbg_loops = dbg_loops + 1;
VT_XSYNC(vt_dpy, 0);
while(vt_poll_event()) {
vt_handle_event();
}
if(quit) {
cleanup_game();
break;
}
d = update(now_ms());
sleep_ms((int)d);
vt_x11_flush();
}
return 0;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* scoredb.c -- RAM-only high scores (the VT1200 has no filesystem).
*/
#include <stdio.h>
#include <string.h>
#include "scoredb.h"
#define NENT 10
char *username = "VT1200";
static struct score_entry list[NENT];
static char names[NENT][16];
static int inited;
static void init_list(void)
{
int i;
if(inited) return;
inited = 1;
for(i = 0; i < NENT; i++) {
strcpy(names[i], "VT1200");
list[i].user = names[i];
list[i].score = 0;
list[i].lines = 0;
list[i].level = 0;
list[i].next = (i + 1 < NENT) ? &list[i + 1] : 0;
}
}
struct score_entry *read_scores(FILE *fp, int max_scores)
{
(void)fp;
(void)max_scores;
init_list();
return &list[0];
}
static void copy_entry(struct score_entry *dst, struct score_entry *src)
{
long sc = src->score, ln = src->lines, lv = src->level;
char *u = (src->user && *src->user) ? src->user : username;
strncpy(dst->user, u, 15);
dst->user[15] = 0;
dst->score = sc;
dst->lines = ln;
dst->level = lv;
}
int save_score(struct score_entry *sc)
{
int pos, i;
if(!sc) return 0;
init_list();
for(pos = 0; pos < NENT; pos++) {
if(sc->score > list[pos].score) break;
}
if(pos >= NENT) return 0;
for(i = NENT - 1; i > pos; i--) {
long s = list[i - 1].score, l = list[i - 1].lines, v = list[i - 1].level;
char tmp[16];
strcpy(tmp, list[i - 1].user);
strcpy(list[i].user, tmp);
list[i].score = s;
list[i].lines = l;
list[i].level = v;
}
copy_entry(&list[pos], sc);
return 1;
}
int print_scores(int num)
{
(void)num;
return 0;
}
+16
View File
@@ -0,0 +1,16 @@
/*
* stdio.h -- the tiny subset termtris needs on the VT1200.
* There is no stdio in the TI r5.01 runtime; compat.c provides sprintf.
*/
#ifndef VT_STDIO_H_
#define VT_STDIO_H_
typedef int FILE;
extern FILE *stdout;
int sprintf(char *buf, char *fmt, ...);
int fflush(FILE *fp);
int putchar(int c);
#endif
+207
View File
@@ -0,0 +1,207 @@
/*
* x11.c -- a small character-cell terminal that draws through the VT1200 ROM
* Xlib. The game's term_* callbacks feed an 80x24 grid; vt_x11_flush() repaints
* each dirty row with one XFillRectangle (background) and one XDrawString (the
* row's 80 characters), which keeps the request stream small enough for the
* in-machine X server to drain.
*
* Geometry is the default GC's font, measured on the real server: 6x13 cells,
* baseline +10. Do not change the cell metrics without re-measuring.
*/
#include "xlib.h"
#include "x11.h"
#include "term.h"
#include "game.h"
#define COLS 80
#define ROWS 24
#define CELL_W 6
#define CELL_H 13
#define BASE_OFF 10
struct vt_cell {
unsigned short ch;
unsigned char inv, dirty;
};
static struct vt_cell cells[ROWS * COLS];
static int cur_row, cur_col;
static int cur_fg, cur_bg;
void *vt_dpy;
vt_u32 vt_win;
vt_u32 vt_gc;
volatile vt_u32 vt_x11_progress; /* debug: 0xFFFFFF when a flush finished */
/* term.h's callback objects (term.c is not compiled for the VT1200). */
void (*term_reset)(void);
void (*term_clearscr)(void);
void (*term_setcursor)(int row, int col);
void (*term_cursor)(int show);
void (*term_setcolor)(int fg, int bg);
void (*term_ibmchar)(unsigned char c, unsigned char attr);
char *termenv;
int term_type = TERM_ANSI;
vt_u32 vt_root_window(void *dpy)
{
vt_u32 screen;
screen = *(vt_u32 *)((char *)dpy + 0x90);
return *(vt_u32 *)((char *)screen + 8);
}
int vt_x11_init(void)
{
vt_x11_progress = 11;
if(!vt_install_xg())
return -1;
vt_x11_progress = 12;
vt_dpy = VT_XOPEN("0::0");
if(!vt_dpy)
return -1;
vt_x11_progress = 13;
vt_win = VT_XCREATE(vt_dpy, vt_root_window(vt_dpy), 80, 80, 480, 312, 2, 1, 0);
if(!vt_win)
return -1;
vt_x11_progress = 14;
VT_XSELECT(vt_dpy, vt_win, 0x22000FUL);
vt_gc = VT_XGC(vt_dpy, vt_win, 0, 0);
if(!vt_gc)
return -1;
VT_XMAP(vt_dpy, vt_win);
VT_XSYNC(vt_dpy, 0);
return 0;
}
void x11_clearscr(void)
{
int i;
for(i = 0; i < ROWS * COLS; i++) {
cells[i].ch = ' ';
cells[i].inv = 0;
cells[i].dirty = 1;
}
cur_row = cur_col = 0;
}
void x11_reset(void)
{
x11_clearscr();
}
void x11_cursor(int show)
{
(void)show;
}
void x11_setcolor(int fg, int bg)
{
cur_fg = fg;
cur_bg = bg;
}
void x11_setcursor(int row, int col)
{
if(row < 0) row = 0;
if(row >= ROWS) row = ROWS - 1;
if(col < 0) col = 0;
if(col >= COLS) col = COLS - 1;
cur_row = row;
cur_col = col;
}
void x11_ibmchar(unsigned char c, unsigned char attr)
{
struct vt_cell *p;
if(cur_row < 0 || cur_row >= ROWS) return;
if(cur_col < 0 || cur_col >= COLS) return;
p = &cells[cur_row * COLS + cur_col];
p->ch = c;
/* The VT1200 raster is one bit deep, so follow the game's monochrome
* convention (ansi.c -m): colour is dropped, the bold bit reverses. */
p->inv = (attr & 0x80) ? 1 : 0;
p->dirty = 1;
cur_col++;
if(cur_col >= COLS) {
cur_col = 0;
cur_row++;
}
}
void term_putstr(const char *s, unsigned char attr)
{
while(*s) {
x11_ibmchar((unsigned char)*s++, attr);
}
}
void vt_x11_flush(void)
{
int r, c, any = 0;
char line[COLS + 1];
vt_x11_progress = 1;
for(r = 0; r < ROWS; r++) {
struct vt_cell *row = &cells[r * COLS];
int dirty = 0;
for(c = 0; c < COLS; c++) {
if(row[c].dirty) dirty = 1;
}
if(!dirty) continue;
vt_x11_progress = 0x100 + (vt_u32)r;
any = 1;
/* repaint the whole row: black background, then the text.
* One fill + one string per row keeps the request stream small
* enough that the in-machine X server can drain it. */
VT_XSETFG(vt_dpy, vt_gc, 0);
VT_XFILL(vt_dpy, vt_win, vt_gc, 0, r * CELL_H, COLS * CELL_W, CELL_H);
for(c = 0; c < COLS; c++) {
line[c] = row[c].ch ? (char)row[c].ch : ' ';
}
line[COLS] = 0;
VT_XSETFG(vt_dpy, vt_gc, 1);
VT_XDRAW(vt_dpy, vt_win, vt_gc, 0, r * CELL_H + BASE_OFF, line, COLS);
/* inverse (bold) cells: white block with black text */
for(c = 0; c < COLS; c++) {
int start;
if(!row[c].inv) continue;
start = c;
while(c < COLS && row[c].inv) c++;
VT_XSETFG(vt_dpy, vt_gc, 1);
VT_XFILL(vt_dpy, vt_win, vt_gc, start * CELL_W,
r * CELL_H, (c - start) * CELL_W, CELL_H);
VT_XSETFG(vt_dpy, vt_gc, 0);
VT_XDRAW(vt_dpy, vt_win, vt_gc, start * CELL_W,
r * CELL_H + BASE_OFF, line + start, c - start);
}
for(c = 0; c < COLS; c++) row[c].dirty = 0;
}
if(any) {
VT_XFLUSH(vt_dpy);
}
vt_x11_progress = 0xFFFFFFUL;
}
void vt_x11_term_init(void)
{
term_width = COLS;
term_height = ROWS;
term_reset = x11_reset;
term_clearscr = x11_clearscr;
term_setcursor = x11_setcursor;
term_cursor = x11_cursor;
term_setcolor = x11_setcolor;
term_ibmchar = x11_ibmchar;
}
void term_init(void)
{
vt_x11_term_init();
}
+24
View File
@@ -0,0 +1,24 @@
/*
* x11.h -- the VT1200 cell-grid X11 backend.
*/
#ifndef VT_X11_H_
#define VT_X11_H_
#include "xlib.h"
extern void *vt_dpy;
extern vt_u32 vt_win;
extern vt_u32 vt_gc;
int vt_x11_init(void);
void vt_x11_flush(void);
void vt_x11_term_init(void);
void x11_reset(void);
void x11_clearscr(void);
void x11_setcursor(int row, int col);
void x11_cursor(int show);
void x11_setcolor(int fg, int bg);
void x11_ibmchar(unsigned char c, unsigned char attr);
#endif
+30
View File
@@ -0,0 +1,30 @@
/*
* xgfix.c -- install a per-process Xlib context before XOpenDisplay.
*
* `create_process` leaves the per-process user word at *(proc+0x100) pointing
* at a zeroed cell. The ROM Xlib's _XEnq does
*
* XG = *(*(proc+0x100));
* free_list = *(XG + 0x198 bytes);
*
* so a loader child with XG == NULL reads low-RAM garbage as a queue-node
* pointer and XNextEvent copies unmapped zeros. Giving it a zeroed block
* makes the event queue real. This is a RAM write only; the ROM is untouched.
*/
typedef unsigned long vt_u32;
static vt_u32 vt_xg[256]; /* 1 KiB: > the 0x198-byte free-list field */
int vt_install_xg(void)
{
vt_u32 proc, pp;
proc = *(volatile vt_u32 *)0x001B3900UL;
if(proc < 0x100UL || proc > 0x01FFFFFFUL)
return 0;
pp = *(volatile vt_u32 *)(proc + 0x100UL);
if(pp < 0x100UL || pp > 0x01FFFFFFUL)
return 0;
*(volatile vt_u32 *)pp = (vt_u32)vt_xg;
return 1;
}
+53
View File
@@ -0,0 +1,53 @@
/*
* xlib.h -- the VT1200 ROM Xlib entry points, called with the TI C ABI.
*
* Addresses are TMS34010 bit addresses. The caller reserves the A14 slot and
* pushes arguments last-to-first; the callee `rets 2` restores A14. The C
* compiler emits exactly this sequence for a call through a function pointer.
*/
#ifndef VT_XLIB_H_
#define VT_XLIB_H_
typedef unsigned long vt_u32;
typedef void *(*vt_xopen_t)(const char *);
typedef vt_u32 (*vt_xcreate_t)(void *, vt_u32, int, int, unsigned int,
unsigned int, unsigned int, vt_u32, vt_u32);
typedef int (*vt_xselect_t)(void *, vt_u32, long);
typedef int (*vt_xmap_t)(void *, vt_u32);
typedef int (*vt_xsync_t)(void *, int);
typedef int (*vt_xflush_t)(void *);
typedef int (*vt_xnext_t)(void *, void *);
typedef int (*vt_xlookup_t)(void *, char *, int, vt_u32 *, void *);
typedef vt_u32 (*vt_xgc_t)(void *, vt_u32, vt_u32, vt_u32 *);
typedef int (*vt_xsetfg_t)(void *, vt_u32, vt_u32);
typedef int (*vt_xfill_t)(void *, vt_u32, vt_u32, int, int,
unsigned int, unsigned int);
typedef int (*vt_xdraw_t)(void *, vt_u32, vt_u32, int, int,
const char *, int);
typedef int (*vt_sleep_t)(int);
typedef int (*vt_xgetfocus_t)(void *, vt_u32 *, int *);
#define VT_XOPEN ((vt_xopen_t)0x3F9C2570UL)
#define VT_XCREATE ((vt_xcreate_t)0x3FD41B90UL)
#define VT_XSELECT ((vt_xselect_t)0x3FD97D50UL)
#define VT_XMAP ((vt_xmap_t)0x3FD64F60UL)
#define VT_XSYNC ((vt_xsync_t)0x3FD9F6B0UL)
#define VT_XFLUSH ((vt_xflush_t)0x3F9B1030UL)
#define VT_XNEXT ((vt_xnext_t)0x3F9C1CF0UL)
#define VT_XLOOKUP ((vt_xlookup_t)0x3F9A9C30UL)
#define VT_XGC ((vt_xgc_t)0x3F997000UL)
#define VT_XSETFG ((vt_xsetfg_t)0x3FD99A60UL)
#define VT_XFILL ((vt_xfill_t)0x3F99E8F0UL)
#define VT_XDRAW ((vt_xdraw_t)0x3FDA0000UL)
#define VT_XPENDING ((int (*)(void *))0x3FD67620UL)
#define VT_SLEEP ((vt_sleep_t)0x3FB33A20UL)
#define VT_XGETFOCUS ((vt_xgetfocus_t)0x3FD55210UL)
/* display->screens[] is at bit 0x480 / byte 0x90; screen->root at bit 0x40 /
* byte 8 (see dwt/notes/GUI_APPS.md and dwt/gui/xhello.asm). */
vt_u32 vt_root_window(void *dpy);
int vt_install_xg(void);
#endif /* VT_XLIB_H_ */
+18
View File
@@ -0,0 +1,18 @@
/*
* fmt.c -- Task 2 acceptance: the exact sprintf formats game.c uses.
* Results are NUL-terminated in out[6][16]; tests/fmt.lua prints them.
*/
#include <stdio.h>
char out[6][16];
int main(void)
{
sprintf(out[0], "%10ld", 12345L);
sprintf(out[1], "%2ld", 7L);
sprintf(out[2], "%8ld", 42L);
sprintf(out[3], "%2d. %s %ld", 1, "ab", 99L);
sprintf(out[4], "%-3s", "x");
sprintf(out[5], "%%");
return 0;
}
+354
View File
@@ -0,0 +1,354 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local FMT_MARK = tonumber(os.getenv("FMT_MARK") or "0x01A00000")
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local function rstr(a, n)
local t = {}
for i = 0, n - 1 do
local c = prog:read_u8(a + i * 8)
if c == 0 then break end
t[#t + 1] = string.char(c)
end
return table.concat(t)
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if base:find("fmt") then
for i = 0, 5 do
print(string.format("FMT:%d [%s]", i, rstr(FMT_MARK + i * 0x80, 16)))
end
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+35
View File
@@ -0,0 +1,35 @@
#!/bin/sh
# fmt.sh -- Task 2 acceptance. Builds tests/fmt.c with compat.c, loads it, and
# checks the six formatted strings byte-for-byte.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
LOG="$APP/build/fmt.log"
mkdir -p "$APP/build"
"$APP/build.sh" "$DIR/fmt.c" "$APP/src/vt1200/compat.c" >"$APP/build/fmt-build.log" 2>&1
FMT=$(awk '{for(i=2;i<=NF;i++) if($i=="_out") print $(i-1)}' "$APP/build/fmt.map" | head -1)
VT1200_SKIP_LEVEL1=1 FMT_MARK="0x$FMT" \
LOADER_AT=2500 LOADER_END=2700 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/fmt.coff" \
LOADER_OUT="$APP/build" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/fmt.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep 'FMT:' "$LOG" | tail -8
for want in 'FMT:0 [ 12345]' 'FMT:1 [ 7]' 'FMT:2 [ 42]' \
'FMT:3 [ 1. ab 99]' 'FMT:4 [x ]' 'FMT:5 [%]'; do
grep -Fq "$want" "$LOG" || {
echo "fmt.sh: missing: $want (see $LOG)" >&2
exit 1
}
done
echo "fmt: PASS"
+39
View File
@@ -0,0 +1,39 @@
/*
* input.c -- Task 4 acceptance: link main.c against stubs for the game, inject
* keys, and record the bytes main.c sends to game_input().
*/
#include "game.h"
#include "x11.h"
#include "xlib.h"
int term_width, term_height;
/* game.c globals main.c sets or reads; the real game supplies these */
int onlyascii, use_gfxchar, monochrome;
int quit;
long tick_interval;
volatile vt_u32 mark[128];
void cleanup_game(void) {}
void wait_display(void);
int init_game(void)
{
mark[0] = 0x494E5054UL; /* INPT */
vt_x11_term_init();
x11_clearscr();
return 0;
}
long update(long msec)
{
(void)msec;
mark[1] = mark[1] + 1;
return 100;
}
void game_input(int c)
{
int n = (int)mark[2];
if(n < 100) mark[3 + n] = (vt_u32)(unsigned char)c;
mark[2] = (vt_u32)(n + 1);
}
+387
View File
@@ -0,0 +1,387 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local INPUT_MARK = tonumber(os.getenv("INPUT_MARK") or "0x01A00000")
local ports = machine.ioport.ports
local key_a = ports[":lk401:K26"].fields["A"]
local key_up = ports[":lk401:K77"].fields["Up"]
local key_down = ports[":lk401:K76"].fields["Down"]
local key_left = ports[":lk401:K74"].fields["Left"]
local key_right = ports[":lk401:K75"].fields["Right"]
local key_zero = ports[":lk401:K10"].fields["0 )"]
local key_ret = ports[":lk401:K68"].fields["Return"]
local key_del = ports[":lk401:K67"].fields["Delete"]
local key_q = ports[":lk401:K13"].fields["Q"]
local key_p = ports[":lk401:K22"].fields["P"]
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function key_step(frames, key)
if frames == 3100 then key_a:set_value(1) end
if frames == 3110 then key_a:set_value(0) end
if frames == 3120 then key_up:set_value(1) end
if frames == 3130 then key_up:set_value(0) end
if frames == 3140 then key_down:set_value(1) end
if frames == 3150 then key_down:set_value(0) end
if frames == 3160 then key_left:set_value(1) end
if frames == 3170 then key_left:set_value(0) end
if frames == 3180 then key_right:set_value(1) end
if frames == 3190 then key_right:set_value(0) end
if frames == 3200 then key_zero:set_value(1) end
if frames == 3210 then key_zero:set_value(0) end
if frames == 3220 then key_ret:set_value(1) end
if frames == 3230 then key_ret:set_value(0) end
if frames == 3240 then key_del:set_value(1) end
if frames == 3250 then key_del:set_value(0) end
if frames == 3260 then key_q:set_value(1) end
if frames == 3270 then key_q:set_value(0) end
if frames == 3280 then key_p:set_value(1) end
if frames == 3290 then key_p:set_value(0) end
end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if base:find("input") then
local n = r32(INPUT_MARK + 0x40)
local t = {}
for i = 0, n - 1 do t[#t + 1] = string.format("%02X", r32(INPUT_MARK + (3 + i) * 0x20)) end
print(string.format("INPUT: magic=%08X updates=%d n=%d bytes=%s",
r32(INPUT_MARK), r32(INPUT_MARK + 0x20), n, table.concat(t, " ")))
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("INPUT: tablet move") end
if frames == 3000 then tabb:set_value(1); print("INPUT: click down") end
if frames == 3020 then tabb:set_value(0); print("INPUT: click up") end
key_step(frames, nil)
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+27
View File
@@ -0,0 +1,27 @@
#!/bin/sh
# input.sh -- Task 4 acceptance: main.c's key map against injected keys.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
LOG="$APP/build/input.log"
mkdir -p "$APP/build"
"$APP/build.sh" "$DIR/input.c" "$APP/src/vt1200/main.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/input-build.log" 2>&1
MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/input.map" | head -1)
VT1200_SKIP_LEVEL1=1 INPUT_MARK="0x$MARK" \
LOADER_AT=2500 LOADER_END=3400 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/input.coff" \
LOADER_OUT="$APP/build" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/input.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep 'INPUT:' "$LOG" | tail -3
grep -q 'INPUT: .*bytes=.*61 9B 41 9B 42 9B 44 9B 43 30 0A 7F 71 70' "$LOG" || {
echo "input.sh: mapped key sequence missing (see $LOG)" >&2
exit 1
}
echo "input: PASS"
+350
View File
@@ -0,0 +1,350 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local PTICK = tonumber(os.getenv("DIAG_TICK") or "0")
local PQUIT = tonumber(os.getenv("DIAG_QUIT") or "0")
local ports = machine.ioport.ports
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if PTICK ~= 0 or PQUIT ~= 0 then
print(string.format("DIAG: tick=%d quit=%d", PTICK ~= 0 and r32(PTICK) or -1, PQUIT ~= 0 and r32(PQUIT) or -1))
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAY: tablet move") end
if frames == 3000 then tabb:set_value(1); print("PLAY: click down") end
if frames == 3020 then tabb:set_value(0); print("PLAY: click up") end
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# play.sh -- load termtris, click it active, and snapshot the board.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
EV="$APP/evidence"
LOG="$APP/build/play.log"
mkdir -p "$EV" "$APP/build"
OUTNAME=termtris "$APP/build.sh" "$APP/src/vt1200/main.c" "$APP/src/game.c" "$APP/src/vt1200/scoredb.c" \
"$APP/src/vt1200/x11.c" "$APP/src/vt1200/compat.c" "$APP/src/vt1200/xgfix.c" \
>"$APP/build/play-build.log" 2>&1
rm -f "$EV/loader_frame3300.png"
TICK=$(awk '{for(i=2;i<=NF;i++) if($i=="_tick_interval") print $(i-1)}' "$APP/build/termtris.map" | head -1)
QUIT=$(awk '{for(i=2;i<=NF;i++) if($i=="_quit") print $(i-1)}' "$APP/build/termtris.map" | head -1)
VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 DIAG_TICK="0x$TICK" DIAG_QUIT="0x$QUIT" \
LOADER_AT=2500 LOADER_END=3300 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/termtris.coff" \
LOADER_OUT="$EV" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/play.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep -E 'PLAY:|DIAG:|LOADER: result|LOADER: snapshot' "$LOG" | tail -6
grep -q 'LOADER: result status=0' "$LOG" || { echo "play.sh: the game did not start (see $LOG)" >&2; exit 1; }
DIAG=$(grep -a 'DIAG: tick=' "$LOG" | head -1)
[ -n "$DIAG" ] || { echo "play.sh: no DIAG line (see $LOG)" >&2; exit 1; }
TICKVAL=$(printf '%s' "$DIAG" | sed -n 's/.*tick=\([-0-9]*\).*/\1/p')
QUITVAL=$(printf '%s' "$DIAG" | sed -n 's/.*quit=\([-0-9]*\).*/\1/p')
[ "$TICKVAL" -gt 0 ] || { echo "play.sh: init_game never ran (tick=$TICKVAL)" >&2; exit 1; }
[ "$QUITVAL" = 0 ] || { echo "play.sh: game quit by itself" >&2; exit 1; }
[ -s "$EV/loader_frame3300.png" ] || { echo "play.sh: no snapshot (see $LOG)" >&2; exit 1; }
cp "$EV/loader_frame3300.png" "$EV/termtris_frame3300.png"
echo "play: $EV/termtris_frame3300.png"
+390
View File
@@ -0,0 +1,390 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local PLAY_MODE = os.getenv("PLAY_MODE") or "move"
print("PLAYKEYS: mode="..PLAY_MODE)
local DL = tonumber(os.getenv("DIAG_LOOPS") or "0")
local DE = tonumber(os.getenv("DIAG_EVENTS") or "0")
local DT = tonumber(os.getenv("DIAG_LAST") or "0")
local DF = tonumber(os.getenv("DIAG_FOCUS") or "0")
local DR = tonumber(os.getenv("DIAG_REV") or "0")
local DQ = tonumber(os.getenv("DIAG_QUIT") or "0")
local DTICK = tonumber(os.getenv("DIAG_TICK") or "0")
local ports = machine.ioport.ports
local key_a = ports[":lk401:K26"].fields["A"]
local key_d = ports[":lk401:K28"].fields["D"]
local key_s = ports[":lk401:K27"].fields["S"]
local key_w = ports[":lk401:K14"].fields["W"]
local key_0 = ports[":lk401:K10"].fields["0 )"]
local key_p = ports[":lk401:K22"].fields["P"]
local key_r = ports[":lk401:K16"].fields["R"]
local key_h = ports[":lk401:K31"].fields["H"]
local key_q = ports[":lk401:K13"].fields["Q"]
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if DL ~= 0 then
print(string.format("DIAG: loops=%d events=%d last=%d focus=%08X rev=%d quit=%d", r32(DL), r32(DE), r32(DT), DF ~= 0 and r32(DF) or 0, DR ~= 0 and r32(DR) or 0, DQ ~= 0 and r32(DQ) or 0))
if DTICK ~= 0 then
print(string.format("DIAG: tick=%d", r32(DTICK)))
end
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("PLAYKEYS: move") end
if frames == 3000 then tabb:set_value(1); print("PLAYKEYS: click down") end
if frames == 3020 then tabb:set_value(0); print("PLAYKEYS: click up") end
if PLAY_MODE == "move" then
if frames == 3100 then key_a:set_value(1) end
if frames == 3110 then key_a:set_value(0) end
if frames == 3120 then key_d:set_value(1) end
if frames == 3130 then key_d:set_value(0) end
if frames == 3140 then key_s:set_value(1) end
if frames == 3150 then key_s:set_value(0) end
if frames == 3160 then key_w:set_value(1) end
if frames == 3170 then key_w:set_value(0) end
if frames == 3180 then key_0:set_value(1) end
if frames == 3190 then key_0:set_value(0) end
elseif PLAY_MODE == "panels" then
if frames == 3100 then key_h:set_value(1) end
if frames == 3110 then key_h:set_value(0) end
if frames == 3120 then key_r:set_value(1) end
if frames == 3130 then key_r:set_value(0) end
elseif PLAY_MODE == "quit" then
if frames == 3095 then print("PLAYKEYS: quit arm") end
if frames == 3100 then print("PLAYKEYS: Q down"); key_q:set_value(1) end
if frames == 3110 then print("PLAYKEYS: Q up"); key_q:set_value(0) end
end
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
# play_keys.sh -- run the real game through move, panels and quit paths.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
EV="$APP/evidence"
mkdir -p "$EV" "$APP/build"
OUTNAME=termtris "$APP/build.sh" "$APP/src/vt1200/main.c" "$APP/src/game.c" "$APP/src/vt1200/scoredb.c" \
"$APP/src/vt1200/x11.c" "$APP/src/vt1200/compat.c" "$APP/src/vt1200/xgfix.c" \
>"$APP/build/play_keys-build.log" 2>&1
for mode in ${MODES:-move panels quit}; do
LOG="$APP/build/play_$mode.log"
rm -f "$EV/loader_frame3200.png"
DL=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_loops") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DE=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_events") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DT=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_last") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DF=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_focus") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DR=$(awk '{for(i=2;i<=NF;i++) if($i=="_dbg_rev") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DQ=$(awk '{for(i=2;i<=NF;i++) if($i=="_quit") print $(i-1)}' "$APP/build/termtris.map" | head -1)
DTICK=$(awk '{for(i=2;i<=NF;i++) if($i=="_tick_interval") print $(i-1)}' "$APP/build/termtris.map" | head -1)
VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 PLAY_MODE=$mode DIAG_LOOPS="0x$DL" DIAG_EVENTS="0x$DE" DIAG_LAST="0x$DT" DIAG_FOCUS="0x$DF" DIAG_REV="0x$DR" DIAG_QUIT="0x$DQ" \
DIAG_TICK="0x$DTICK" \
LOADER_AT=2500 LOADER_END=3200 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/termtris.coff" \
LOADER_OUT="$EV" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/play_keys.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep -E 'PLAYKEYS:|LOADER: result|LOADER: snapshot' "$LOG" | tail -5
DIAG=$(grep -a 'DIAG: loops' "$LOG" | sed -n 's/.*loops=\([0-9]*\) events=\([0-9]*\).*quit=\([0-9]*\)/\1 \2 \3/p' | head -1)
[ -n "$DIAG" ] || { echo "play_keys.sh: no DIAG line for $mode (see $LOG)" >&2; exit 1; }
set -- $DIAG
LOOPS=$1; EVENTS=$2; QUIT=$3
echo "play_keys: $mode loops=$LOOPS events=$EVENTS quit=$QUIT"
[ "$LOOPS" -ge 100 ] || { echo "play_keys.sh: $mode main loop barely ran ($LOOPS)" >&2; exit 1; }
[ "$EVENTS" -ge 6 ] || { echo "play_keys.sh: $mode got $EVENTS events, expected the click and keys" >&2; exit 1; }
case $mode in
move|panels) [ "$QUIT" = 0 ] || { echo "play_keys.sh: $mode quit unexpectedly" >&2; exit 1; } ;;
quit) [ "$QUIT" = 1 ] || { echo "play_keys.sh: q did not reach the game (quit=$QUIT)" >&2; exit 1; } ;;
esac
[ -s "$EV/loader_frame3200.png" ] || { echo "play_keys.sh: no $mode snapshot" >&2; exit 1; }
cp "$EV/loader_frame3200.png" "$EV/termtris_${mode}_3200.png"
done
echo "play_keys: $EV/termtris_move_3200.png $EV/termtris_panels_3200.png $EV/termtris_quit_3200.png"
+58
View File
@@ -0,0 +1,58 @@
/*
* render.c -- Task 3 acceptance: draw a border, a playfield outline and
* TERMTRIS through the cell-grid backend, then keep the window alive so the
* loader can snapshot it.
*/
#include "xlib.h"
#include "x11.h"
extern int vt_install_xg(void);
volatile vt_u32 mark[16];
int term_width, term_height;
static const char *word = "TERMTRIS";
static void put(int row, int col, const char *s, unsigned char attr)
{
x11_setcursor(row, col);
while(*s) x11_ibmchar((unsigned char)*s++, attr);
}
int main(void)
{
int r, c;
mark[0] = 0x524E4452UL; /* RNDR */
if(vt_x11_init() != 0) return 1;
mark[1] = (vt_u32)vt_dpy;
mark[2] = vt_win;
mark[3] = vt_gc;
mark[5] = (vt_u32)VT_XPENDING(vt_dpy);
x11_clearscr();
for(c = 0; c < 80; c++) {
x11_setcursor(0, c); x11_ibmchar('#', 0x70);
x11_setcursor(23, c); x11_ibmchar('#', 0x70);
}
for(r = 0; r < 24; r++) {
x11_setcursor(r, 0); x11_ibmchar('#', 0x70);
x11_setcursor(r, 79); x11_ibmchar('#', 0x70);
}
for(r = 2; r < 20; r++) {
for(c = 4; c <= 13; c++) {
int edge = (r == 2 || r == 19 || c == 4 || c == 13);
x11_setcursor(r, c);
x11_ibmchar(edge ? '#' : '.', edge ? 0x70 : 0x07);
}
}
put(1, 20, word, 0x70);
put(21, 20, "0123456789", 0x70);
put(22, 20, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0x70);
vt_x11_flush();
mark[4] = 0x464C5553UL; /* FLUS */
for(;;) {
VT_SLEEP(100);
}
return 0;
}
+347
View File
@@ -0,0 +1,347 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local RENDER_MARK = tonumber(os.getenv("RENDER_MARK") or "0x01A08000")
local X11P = tonumber(os.getenv("X11_PROGRESS") or "0")
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if base:find("render") then
print(string.format("RENDER: magic=%08X dpy=%08X win=%08X gc=%08X flus=%08X progress=%08X",
r32(RENDER_MARK), r32(RENDER_MARK + 0x20), r32(RENDER_MARK + 0x40),
r32(RENDER_MARK + 0x60), r32(RENDER_MARK + 0x80),
X11P ~= 0 and r32(X11P) or 0))
print(string.format("RENDER: pending=%d", r32(RENDER_MARK + 0xA0)))
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# render.sh -- Task 3 acceptance: paint the cell-grid test pattern, then
# snapshot the window. The PNG is the evidence.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
EV="$APP/evidence"
LOG="$APP/build/render.log"
mkdir -p "$EV" "$APP/build"
"$APP/build.sh" "$DIR/render.c" "$APP/src/vt1200/x11.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/render-build.log" 2>&1
rm -f "$EV/loader_frame3400.png"
MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/render.map" | head -1)
PROG=$(awk '{for(i=2;i<=NF;i++) if($i=="_vt_x11_progress") print $(i-1)}' "$APP/build/render.map" | head -1)
VT1200_SKIP_LEVEL1=1 LOADER_SNAP=1 RENDER_MARK="0x$MARK" X11_PROGRESS="0x$PROG" \
LOADER_AT=2500 LOADER_END=3400 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/render.coff" \
LOADER_OUT="$EV" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/render.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep -E 'LOADER: snapshot|LOADER: result' "$LOG" | tail -4
grep -q 'RENDER: magic=524E4452 .*flus=464C5553' "$LOG" || {
echo "render.sh: the renderer did not finish (see $LOG)" >&2
exit 1
}
grep -q 'RENDER: .*progress=00FFFFFF' "$LOG" || {
echo "render.sh: flush did not complete (see $LOG)" >&2
exit 1
}
[ -s "$EV/loader_frame3400.png" ] || { echo "render.sh: no snapshot (see $LOG)" >&2; exit 1; }
cp "$EV/loader_frame3400.png" "$EV/render_frame3400.png"
echo "render: $EV/render_frame3400.png"
+53
View File
@@ -0,0 +1,53 @@
/*
* smoke.c -- Task 1 acceptance: a local Xlib client installs the per-process
* Xlib context, opens the display, maps a window, and receives a real KeyPress.
*/
#include "xlib.h"
extern int vt_install_xg(void);
volatile vt_u32 mark[32];
volatile vt_u32 evbuf[24];
int main(void)
{
void *dpy;
vt_u32 root, win;
vt_u32 ks;
char buf[8];
int n;
mark[0] = 0x534D4F4BUL; /* SMOK */
if(!vt_install_xg()) return 1;
dpy = VT_XOPEN("0::0");
mark[1] = (vt_u32)dpy;
if(!dpy) return 2;
root = *(vt_u32 *)((char *)dpy + 0x90);
root = *(vt_u32 *)((char *)root + 8);
win = VT_XCREATE(dpy, root, 80, 80, 480, 312, 2, 1, 0);
mark[2] = win;
if(!win) return 3;
VT_XSELECT(dpy, win, 0x22000FUL);
VT_XMAP(dpy, win);
VT_XSYNC(dpy, 0);
mark[3] = 0;
for(;;) {
VT_XNEXT(dpy, (void *)evbuf);
mark[3] = mark[3] + 1;
mark[4] = evbuf[0];
mark[5] = evbuf[13];
if(evbuf[0] == 2) {
ks = 0;
n = VT_XLOOKUP((void *)evbuf, buf, 8, &ks, 0);
mark[6] = (vt_u32)n;
mark[7] = (vt_u32)(unsigned char)buf[0];
mark[8] = ks;
mark[9] = 0x4B455950UL; /* KEYP */
break;
}
}
return 0;
}
+355
View File
@@ -0,0 +1,355 @@
-- loader.lua -- debugger-assisted COFF loader acceptance harness.
--
-- Boots the firmware with VT1200_SKIP_LEVEL1=1 (the documented divergence) to
-- the OS, then at frame LOADER_AT pokes:
-- * the loader payload at bit 0x01880000 (raw dwt/loader/build/loader.bin)
-- * a GSPLNK COFF image at LOADER_BUF (default bit 0x01900000)
-- * the loader descriptor at 0x01882C00
-- and sets PC = 0x01880000. It does not use the network, the rdebug service
-- or any firmware transport: this is the debugger-assisted milestone.
--
-- The loader itself copies the COFF sections to their linked bit addresses and
-- calls create_process(entry, "LOADER", 0x6000, 1, 0). This script only pokes
-- the input and reads the result; it never writes the app's code or markers.
--
-- Env:
-- LOADER_BIN loader payload (default dwt/loader/build/loader.bin)
-- LOADER_COFF app COFF (default dwt/loader/build/marker.coff)
-- LOADER_BUF COFF buffer bit address (default 0x01900000)
-- LOADER_AT injection frame (default 2500)
-- LOADER_END inspection/exit frame (default 2700)
-- LOADER_OUT evidence directory (default dwt/loader/evidence)
-- LOADER_SNAP 1 = write a PNG at LOADER_END (default 0)
-- LOADER_VERBOSE 1 = print the result block every 25 frames
local function hex(v, w) return string.format("%0" .. (w or 8) .. "X", v) end
local machine = manager.machine
local MARK = tonumber(os.getenv("SMOKE_MARK") or "0x01A00000")
local ports = machine.ioport.ports
local key_a = ports[":lk401:K26"].fields["A"]
local tabx = ports[":tablet:TABLET_X"].fields["Tablet X"]
local taby = ports[":tablet:TABLET_Y"].fields["Tablet Y"]
local tabb = ports[":tablet:TABLET_BTN"].fields["P1 Button 1"]
local cpu = machine.devices[':maincpu']
local dbg = machine.debugger
local prog = cpu.spaces['program']
if not dbg then
print("LOADER: debugger not enabled; pass -debug")
machine:exit()
return
end
local AT = tonumber(os.getenv("LOADER_AT") or "2500")
local ENDF = tonumber(os.getenv("LOADER_END") or "2700")
local BIN = os.getenv("LOADER_BIN") or "dwt/loader/build/loader.bin"
local COFF = os.getenv("LOADER_COFF") or "dwt/loader/build/marker.coff"
local BUF = tonumber(os.getenv("LOADER_BUF") or "0x01900000")
local OUT = os.getenv("LOADER_OUT") or "dwt/loader/evidence"
local SNAP = os.getenv("LOADER_SNAP") == "1"
local MODE = tonumber(os.getenv("LOADER_MODE") or "0")
local A8DESC = os.getenv("LOADER_A8_DESC") == "1"
local VERBOSE = os.getenv("LOADER_VERBOSE") == "1"
local LOADER_BASE = 0x01880000
local DESC = 0x01882C00
local RESULT = 0x01882D00
local RETURN_STUB = 0x01883100
local RETURN_SP = 0x01883110
local function rd(p) return io.open(p, "rb") end
local function slurp(p)
local f, err = rd(p)
if not f then error("cannot open " .. p .. ": " .. tostring(err)) end
local s = f:read("*a")
f:close()
return s
end
local function u16(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
end
local function u32(s, o)
return s:byte(o + 1) + s:byte(o + 2) * 256
+ s:byte(o + 3) * 65536 + s:byte(o + 4) * 16777216
end
local function parse_coff(d)
local c = { data = d }
c.magic = u16(d, 0)
c.nscns = u16(d, 2)
c.opthdr = u16(d, 16)
c.sections = {}
if c.opthdr > 0 then
c.optmagic = u16(d, 20)
c.entry = u32(d, 20 + 16)
c.textstart = u32(d, 20 + 20)
c.datastart = u32(d, 20 + 24)
end
local base = 20 + c.opthdr
for i = 0, c.nscns - 1 do
local o = base + i * 40
local name = d:sub(o + 1, o + 8):gsub("%z", "")
c.sections[#c.sections + 1] = {
name = name,
paddr = u32(d, o + 8),
size = u32(d, o + 16), -- bits
scnptr = u32(d, o + 20),
flags = u32(d, o + 36),
}
end
return c
end
local function poke_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
prog:write_u16(bitaddr + i * 16, lo + hi * 256)
end
return n
end
local function verify_bytes(bitaddr, data)
local n = math.floor((#data + 1) / 2)
local bad = 0
for i = 0, n - 1 do
local lo = data:byte(i * 2 + 1) or 0
local hi = data:byte(i * 2 + 2) or 0
if prog:read_u16(bitaddr + i * 16) ~= lo + hi * 256 then
bad = bad + 1
end
end
return n, bad
end
local function w32(a, v) prog:write_u32(a, v) end
local function r32(a)
local ok, v = pcall(function() return prog:read_u32(a) end)
if ok then return v end
return 0xDEADBEEF
end
local loader = slurp(BIN)
local coff = slurp(COFF)
local app = parse_coff(coff)
print(string.format("LOADER: payload %s (%d B) -> bit %s", BIN, #loader, hex(LOADER_BASE)))
print(string.format("LOADER: COFF %s (%d B) -> bit %s", COFF, #coff, hex(BUF)))
print(string.format("LOADER: file magic=%04X nscns=%d opthdr=%d optmagic=%s entry=%s textstart=%s",
app.magic, app.nscns, app.opthdr,
app.optmagic and string.format("%04X", app.optmagic) or "none",
hex(app.entry or 0), hex(app.textstart or 0)))
for _, s in ipairs(app.sections) do
print(string.format("LOADER: sec %-8s paddr=%s size=%d bits (%d B) scnptr=%#x flags=%#x",
s.name, hex(s.paddr), s.size, math.floor(s.size / 8), s.scnptr, s.flags))
end
local TRACE = os.getenv("LOADER_TRACE") == "1"
local frames = 0
local injected = false
local done = false
local last_count = -1
local function status_name(v)
local t = {
[0] = "OK",
[1] = "bad descriptor",
[2] = "truncated COFF / section headers",
[3] = "bad COFF magic",
[4] = "section size not whole words",
[5] = "target outside RAM",
[6] = "target overlaps loader page or COFF buffer",
[7] = "relocation records present (unsupported)",
[8] = "target not in a free-list block",
[9] = "no entry point",
}
return t[v] or ("error " .. tostring(v))
end
local function result_dump()
local st = r32(RESULT + 0x000)
print(string.format(
"LOADER: result status=%d (%s) entry=%s text=%s/%d bits data=%s/%d bits bss=%s/%d bits relocs=%d pcb=%s nscns=%d len=%d",
st, status_name(st), hex(r32(RESULT + 0x020)),
hex(r32(RESULT + 0x040)), r32(RESULT + 0x060),
hex(r32(RESULT + 0x080)), r32(RESULT + 0x0A0),
hex(r32(RESULT + 0x0C0)), r32(RESULT + 0x0E0),
r32(RESULT + 0x100), hex(r32(RESULT + 0x120)),
r32(RESULT + 0x140), r32(RESULT + 0x160)))
end
local function inject()
-- 1. loader itself
local n, bad = 0, 0
poke_bytes(LOADER_BASE, loader)
n, bad = verify_bytes(LOADER_BASE, loader)
print(string.format("LOADER: loader poked %d words, readback %d bad at %s",
n, bad, hex(LOADER_BASE)))
-- 2. COFF image
poke_bytes(BUF, coff)
n, bad = verify_bytes(BUF, coff)
print(string.format("LOADER: COFF poked %d words, readback %d bad at %s",
n, bad, hex(BUF)))
-- 3. pre-fill every .bss section with A5A5, so a loaded zero is meaningful
for _, s in ipairs(app.sections) do
if s.flags & 0x80 ~= 0 then
for i = 0, math.floor(s.size / 16) - 1 do
prog:write_u16(s.paddr + i * 16, 0xA5A5)
end
print(string.format("LOADER: pre-filled .bss %s..%s with A5A5",
hex(s.paddr), hex(s.paddr + s.size)))
end
end
-- 4. descriptor: magic "LD" "R1", buf, len, mode=0 (spin)
w32(DESC + 0x00, 0)
prog:write_u16(DESC + 0x00, 0x4C44)
prog:write_u16(DESC + 0x10, 0x5231)
w32(DESC + 0x20, BUF)
w32(DESC + 0x40, #coff)
w32(DESC + 0x60, MODE)
print(string.format("LOADER: descriptor @%s magic=%04X/%04X buf=%s len=%d mode=%d",
hex(DESC), prog:read_u16(DESC + 0x00), prog:read_u16(DESC + 0x10),
hex(r32(DESC + 0x20)), r32(DESC + 0x40), r32(DESC + 0x60)))
-- 5. return stub. mode 0 ignores it (the loader jumps to ORIG_PC
-- itself); mode 1 returns here, and the stub resumes the interrupted
-- PC so the caller yields and the scheduler can run the child.
local orig_pc = cpu.state['PC'].value
if MODE == 1 then
prog:write_u16(RETURN_STUB, 0xC080) -- jauc <orig_pc>
prog:write_u16(RETURN_STUB + 0x10, orig_pc & 0xFFFF)
prog:write_u16(RETURN_STUB + 0x20, math.floor(orig_pc / 0x10000) & 0xFFFF)
else
prog:write_u16(RETURN_STUB, 0xC0FF) -- jruc self
end
-- 6. enter the loader. A8=0 selects the fixed descriptor; SP points at
-- the return slot so mode=1 would return to the spin stub.
-- Leave SP alone for mode 0: the loader pushes onto the current
-- process stack, then restores it and resumes at ORIG_PC. (mode 1
-- would use the return stub.)
w32(0x01882A20, orig_pc) -- LD_ORIG_PC
print(string.format("LOADER: will resume at %s after create_process", hex(orig_pc)))
cpu.state['A8'].value = A8DESC and DESC or 0
if MODE == 1 then
-- Simulate a CALLA from the current process: push the return
-- address on the *firmware* stack (a real transport would do this).
local sp = cpu.state['SP'].value - 0x20
w32(sp, RETURN_STUB)
cpu.state['SP'].value = sp
end
cpu.state['PC'].value = LOADER_BASE
injected = true
print(string.format("LOADER: frame %d: entered loader at %s (caller SP=%s)",
frames, hex(LOADER_BASE), hex(cpu.state['SP'].value)))
end
local function inspect()
result_dump()
-- Verify the placement from RAM: .text/.data must equal the file bytes.
for _, s in ipairs(app.sections) do
if s.scnptr ~= 0 and s.size > 0 then
local nb = math.floor(s.size / 8)
local bytes = coff:sub(s.scnptr + 1, s.scnptr + nb)
local _, bad = verify_bytes(s.paddr, bytes)
print(string.format("LOADER: placed %s at %s: %d bytes, %d differing",
s.name, hex(s.paddr), nb, bad))
end
end
local base = COFF:match("([^/]+)$") or COFF
if base:find("marker") then
print(string.format("LOADER: marker app: start=%s data=%s data2=%s bss=%s count=%d",
hex(r32(0x018E0000)), hex(r32(0x018E0020)), hex(r32(0x018E0040)),
hex(r32(0x018E0060)), r32(0x018E0080)))
elseif base:find("xhello") then
print(string.format("LOADER: xhello: stage=%d strlen=%d display=%s root=%s window=%s sync=%s",
r32(0x01890140), r32(0x01890160), hex(r32(0x01890100)),
hex(r32(0x01890110)), hex(r32(0x01890120)), hex(r32(0x01890130))))
end
if base:find("smoke") then
print(string.format("SMOKE: magic=%08X dpy=%08X win=%08X n=%d type=%d keycode=%d lookup_n=%d ch=%02X ks=%04X keymark=%08X",
r32(MARK + 0x00), r32(MARK + 0x20), r32(MARK + 0x40), r32(MARK + 0x60),
r32(MARK + 0x80), r32(MARK + 0xA0), r32(MARK + 0xC0), r32(MARK + 0xE0),
r32(MARK + 0x100), r32(MARK + 0x120)))
end
print(string.format("LOADER: PC at inspection = %s", hex(cpu.state['PC'].value)))
end
emu.register_frame_done(function()
frames = frames + 1
if frames == 2900 then tabx:set_value(700); taby:set_value(500); print("SMOKE: tablet move") end
if frames == 3000 then tabb:set_value(1); print("SMOKE: click down") end
if frames == 3020 then tabb:set_value(0); print("SMOKE: click up") end
if frames == 3100 then key_a:set_value(1); print("SMOKE: A down") end
if frames == 3120 then key_a:set_value(0); print("SMOKE: A up") end
if frames == AT and not injected then
inject()
if TRACE then dbg:command("trace /tmp/opencode/ld1.tr") end
end
if injected and TRACE and frames == AT + 1 then
dbg:command("trace off")
print("LOADER: trace written to /tmp/opencode/ld.tr")
end
if injected and not done then
if TRACE and frames <= AT + 40 then
print(string.format("LOADER: trace f=%d PC=%s SP=%s A14=%s status=%s entry=%s",
frames, hex(cpu.state['PC'].value), hex(cpu.state['SP'].value),
hex(cpu.state['A14'].value), hex(r32(RESULT + 0x000)),
hex(r32(RESULT + 0x020))))
end
local st = r32(RESULT + 0x000)
-- status 0 means OK; nonzero is final too once parsing has begun
if st ~= 0xFFFFFFFF and not done then
-- The loader writes 0 on success. A nonzero status is final
-- only after it has been written once (it is 0 from power-on,
-- so require the loader to have run: PC no longer at entry or
-- the app counter moved).
end
if VERBOSE and frames % 25 == 0 then
result_dump()
end
if base_contains_marker then
local c = r32(0x018E0080)
if c ~= last_count then
if last_count ~= -1 then
print(string.format("LOADER: marker counter %d -> %d (child running)",
last_count, c))
end
last_count = c
end
end
end
if frames >= ENDF then
if injected then
done = true
inspect()
if SNAP then
local screen = nil
for _, s in pairs(machine.screens) do screen = s break end
if screen then
local out = OUT .. "/loader_frame" .. frames .. ".png"
local ok, err = pcall(function() return screen:snapshot(out) end)
print(string.format("LOADER: snapshot %s ok=%s %s",
out, tostring(ok), tostring(err or "")))
end
end
end
machine:exit()
end
end, "vt1200-loader")
-- (Lua has no forward use of a name defined later; set a small flag here.)
base_contains_marker = (COFF:match("([^/]+)$") or ""):find("marker") ~= nil
dbg.visible_cpu = cpu
dbg.execution_state = 'run'
+37
View File
@@ -0,0 +1,37 @@
#!/bin/sh
# smoke.sh -- Task 1 acceptance. Builds the local Xlib smoke payload, loads it
# into the emulated VT1200 RAM, clicks the window, presses A, and requires a
# real KeyPress + XLookupString('a').
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
APP=$(cd "$DIR/.." && pwd)
ROOT=$(cd "$APP/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
LOG="$APP/build/smoke.log"
mkdir -p "$APP/build"
"$APP/build.sh" "$DIR/smoke.c" "$APP/src/vt1200/xgfix.c" >"$APP/build/smoke-build.log" 2>&1
MARK=$(awk '{for(i=2;i<=NF;i++) if($i=="_mark") print $(i-1)}' "$APP/build/smoke.map" | head -1)
VT1200_SKIP_LEVEL1=1 SMOKE_MARK="0x$MARK" \
LOADER_AT=2500 LOADER_END=3400 \
LOADER_BIN="$ROOT/dwt/loader/build/loader.bin" \
LOADER_COFF="$APP/build/smoke.coff" \
LOADER_OUT="$APP/build" \
"$M" vt1200 -rompath "$ROOT/harness/mame/roms" \
-video none -sound none -nothrottle -skip_gameinfo \
-debug -debugger none \
-autoboot_script "$DIR/smoke.lua" -autoboot_delay 0 \
>"$LOG" 2>&1 || true
grep 'SMOKE:' "$LOG" | tail -5
grep -q 'SMOKE: .*type=2 .*keymark=4B455950' "$LOG" || {
echo "smoke.sh: no real KeyPress (see $LOG)" >&2
exit 1
}
grep -q 'SMOKE: .*lookup_n=1 ch=61 ks=0061' "$LOG" || {
echo "smoke.sh: XLookupString did not return 'a' (see $LOG)" >&2
exit 1
}
echo "smoke: PASS"
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
# verify.sh -- one-command acceptance for the VT1200 termtris port.
#
# apps/termtris/verify.sh # build MAME, then run every test
# apps/termtris/verify.sh --skip-mame # use the emulator already installed
#
# Exits non-zero on the first failed check. Each test's full output is kept in
# apps/termtris/build/verify_<name>.log.
set -eu
DIR=$(cd "$(dirname "$0")" && pwd)
ROOT=$(cd "$DIR/../.." && pwd)
M=${MAME:-/home/arti/code/k-space/mame0289/vt1200}
ROM_MD5=b46a3e442ccb86134d2356bd70d22fd2
skip_mame=0
for arg in "$@"; do
case $arg in
--skip-mame) skip_mame=1 ;;
-h|--help)
sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
*)
echo "verify.sh: unknown argument: $arg" >&2
exit 2
;;
esac
done
mkdir -p "$DIR/build"
echo "== ROM checksum"
got=$(md5sum "$ROOT/vt1200_v22.bin" | cut -d' ' -f1)
if [ "$got" != "$ROM_MD5" ]; then
echo "FAIL: vt1200_v22.bin md5 is $got, expected $ROM_MD5" >&2
exit 1
fi
echo "ok: $got"
echo "== 2ine toolchain"
LX="${NINE:-/home/arti/os2-work/2ine/build}/lx_loader"
if [ ! -x "$LX" ]; then
echo "FAIL: no 2ine lx_loader at $LX (set NINE=/path/to/2ine/build)" >&2
exit 1
fi
echo "ok: $LX"
echo "== emulator"
if [ "$skip_mame" -eq 0 ]; then
JOBS=${JOBS:-8} "$ROOT/harness/mame/build.sh"
fi
if [ ! -x "$M" ]; then
echo "FAIL: no vt1200 binary at $M (set MAME=... or drop --skip-mame)" >&2
exit 1
fi
echo "ok: $M"
export MAME="$M"
fail=0
run() {
name=$1
shift
printf '== %-9s ' "$name"
if "$@" >"$DIR/build/verify_$name.log" 2>&1; then
echo "PASS"
else
echo "FAIL (apps/termtris/build/verify_$name.log)"
tail -20 "$DIR/build/verify_$name.log" | sed 's/^/ /'
fail=1
fi
}
run smoke "$DIR/tests/smoke.sh"
run fmt "$DIR/tests/fmt.sh"
run render "$DIR/tests/render.sh"
run input "$DIR/tests/input.sh"
run play "$DIR/tests/play.sh"
run play_keys "$DIR/tests/play_keys.sh"
echo
echo "== play acceptance"
if [ -f "$DIR/build/play_move.log" ]; then
for mode in move panels quit; do
grep -a -h 'DIAG: loops' "$DIR/build/play_$mode.log" 2>/dev/null |
head -1 | sed "s/^/ $mode /"
done
fi
echo
echo "== evidence"
ls -1 "$DIR"/evidence/*.png 2>/dev/null | sed 's/^/ /'
echo
echo "== ROM checksum (after the suite)"
got=$(md5sum "$ROOT/vt1200_v22.bin" | cut -d' ' -f1)
[ "$got" = "$ROM_MD5" ] || { echo "FAIL: ROM changed during the run" >&2; exit 1; }
echo "ok: $got"
if [ "$fail" -ne 0 ]; then
echo
echo "verify: FAILED" >&2
exit 1
fi
echo
echo "verify: PASS"
@@ -0,0 +1,153 @@
# termtris VT1200 X11 port — implementation plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a playable `termtris` that runs as a VT1200 `create_process` child, renders through the ROM X11 server, and accepts the real X keyboard events.
**Architecture:** Vendored upstream game logic + a small VT1200 backend. The backend installs a zeroed per-process Xlib context (`XG`), opens `"0::0"`, maps a 480×312 window, and emulates an 80×24 cell terminal with 6×13 cells. `XNextEvent`/`XLookupString` feed `game_input`; `0x1E93C0` + `sleep(0x3FB33A20)` pace `update`. A 1990 r5.01/2ine build produces absolute COFF for `dwt/loader`.
**Tech Stack:** 1990 TI TMS34010 C tools (r5.01) under 2ine; VT1200 ROM Xlib; MAME `vt1200` harness; GSPLNK COFF.
**Spec:** `docs/superpowers/specs/2026-11-03-termtris-vt1200-design.md`
## Global Constraints
- ROM `vt1200_v22.bin` stays byte-identical (`md5 b46a3e442ccb86134d2356bd70d22fd2`); no MAME or ROM edits.
- The game runs from VT1200 main RAM via `dwt/loader`; no network and no host-side game client.
- Link `.text`/`.data`/`.cinit` at bit `0x018A0000`, `.bss` at `0x01A00000`; entry `_start` from `entry.asm`; the `cinit` table is processed by our `var_init`.
- All Xlib calls use the TI C ABI and the addresses in the spec; re-establish the CPU field state after calls if the compiler's generated code requires it.
- The per-process Xlib context fix is mandatory **before** `XOpenDisplay`:
`proc = *(u32 *)0x1B3900; pp = *(u32 *)(proc + 0x100); *(u32 *)pp = (u32)zeroed_xg_block;`
- Build stages are retried: 2ine exits 139 after producing output; check artifacts, not exit codes.
- Vendor `termtris` under `apps/termtris/` with `COPYING` and the GPLv3 headers intact.
- Commit only `apps/termtris/**`, `harness/mame/TERMTRIS_REPORT.md`, and `docs/superpowers/**` in this work.
## Review Focus
- **A late client's event queue:** the XG block must be installed before `XOpenDisplay`; if it is not, KeyPress events are zero. The acceptance test must inject a key and assert `type=2`.
- **Cell coordinates:** 6×13, baseline +10; a wrong pitch shears the whole board. The screenshot must show aligned columns.
- **Timer wrap:** `0x1E93C0` is 16-bit; use `(u16)(now-last)` deltas, never a raw monotonic assumption.
- **Toolchain output loss:** `gspcg`/`gspa` can crash after writing; the build must detect missing artifacts and retry, not treat exit 139 as failure.
- **No stdio:** `sprintf`/`fflush`/`stdout` are absent; the compat layer must cover every format/use the game actually has.
---
### Task 1: Vendor and smoke-test the toolchain
**Files:**
- Create: `apps/termtris/` (upstream `src/`, `COPYING`, `README.md`)
- Create: `apps/termtris/src/vt1200/entry.asm`
- Create: `apps/termtris/src/vt1200/xlib.h`
- Create: `apps/termtris/src/vt1200/xgfix.c`
- Create: `apps/termtris/build.sh`
- Test: `apps/termtris/tests/smoke.c` + `apps/termtris/tests/smoke.sh`
**Interfaces:**
- Produces: `vt_install_xg(void)` in `xgfix.c`; `xlib.h` with the ROM addresses and `vt_xopen`, `vt_xcreate`, `vt_xselect`, `vt_xmap`, `vt_xsync`, `vt_xflush`.
- Produces: `build.sh` usage `build.sh <source.c> [extra.obj...]` -> `apps/termtris/build/<name>.coff` and `.map`.
- [ ] **Step 1: Copy upstream** `termtris` into `apps/termtris/` (keep `COPYING`, `src/game.c`, `pieces.h`, `game.h`, `term.h`, `scoredb.h`; do not copy the DOS/Windows/unix backends). Record the upstream commit hash in `apps/termtris/UPSTREAM`.
- [ ] **Step 2: Write `entry.asm`** — set `A14` to `_cstack`, `A13=A14`; run the cinit loop (length word at `*A9++`, destination long at `*A9++`, copy `length` words), then `CALLA _main`, `RETS`; `.bss _cstack, 32768, 16`.
- [ ] **Step 3: Write `xlib.h`** with the addresses from the spec and TI-ABI prototypes:
`XOpenDisplay 0x3F9C2570`, `XCreateSimpleWindow 0x3FD41B90`, `XSelectInput 0x3FD97D50`, `XMapWindow 0x3FD64F60`, `XSync 0x3FD9F6B0`, `XFlush 0x3F9B1030`, `XNextEvent 0x3F9C1CF0`, `XLookupString 0x3F9A9C30`, `XCreateGC 0x3F997000`, `XSetForeground 0x3FD99A60`, `XFillRectangle 0x3F99E8F0`, `XDrawString 0x3FDA0000`, `sleep 0x3FB33A20`.
- [ ] **Step 4: Write `xgfix.c`** exactly as the spec's fix; return 0 if `proc`/`pp` is outside RAM, else install a static zeroed `u32 xg[256]` and return 1.
- [ ] **Step 5: Write `build.sh`** — extract the r5.01 tools/libs into `apps/termtris/work/`; run `gspac -k`, `gspopt -k -O2`, `gspcg -m`, `gspa`, `gsplnk`; retry each stage up to 5 times and verify its output before proceeding; `gsplnk` command file with `.text/.data/.cinit` at `018A0000h` and `.bss` at `01A00000h`; fail if `termtris.out`/`.map` absent.
- [ ] **Step 6: Smoke test** — `tests/smoke.c` calls `vt_install_xg`, `XOpenDisplay("0::0")`, creates/maps a 480×312 window, selects KeyPress, loops `XNextEvent`, stores `type/keycode` in `.bss` markers, and breaks on `type==2`. `tests/smoke.sh` builds it and runs `dwt/loader/loader.lua` with `LOADER_AT=2500 LOADER_END=3400`, injecting a tablet click at 3000 and `A` at 3100 via a copy of the validated input script.
- [ ] **Step 7: Verify smoke** — the run prints `type=2`, `keycode=194`, `XLookupString` `n=1, ch=0x61, ks=0x0061`. If not, do not proceed.
- [ ] **Step 8: Commit** `apps/termtris` scaffold + smoke.
### Task 2: Compatibility layer
**Files:**
- Create: `apps/termtris/src/vt1200/stdio.h`
- Create: `apps/termtris/src/vt1200/inttypes.h`
- Create: `apps/termtris/src/vt1200/compat.c`
- Test: `apps/termtris/tests/fmt.c` + loader run
**Interfaces:**
- Produces: `int sprintf(char *buf, const char *fmt, ...)` supporting `%d`, `%ld`, `%u`, `%lu`, `%s`, `%c`, `%%`, optional `-`/`0`, decimal width; `int fflush(void *f)` returns 0; `extern void *stdout` is a null pointer; `time_t time(time_t *)` returns `now_ms()/1000`.
- Produces: `uint16_t`/`uint32_t` typedefs in `inttypes.h`.
- [ ] **Step 1: Write the test** `fmt.c` producing the exact strings used by `game.c`: `"%10ld"` 12345 -> `" 12345"`, `"%2ld"` 7 -> `" 7"`, `"%8ld"` 42 -> `" 42"`, `"%2d. %s %ld"` (1,"ab",99) -> `" 1. ab 99"`, `"%-3s"` "x" -> `"x "`, `"%%"` -> `"%"`. Store a checksum/bytes in markers.
- [ ] **Step 2: Implement `compat.c`** with a manual varargs walk (`char *ap = (char *)&fmt; ap -= 4;` per argument) and a decimal/string formatter; no `stdio.h` from the SDK.
- [ ] **Step 3: Write the shims** so `game.c`'s `<stdio.h>`/`<inttypes.h>` resolve to them; `FILE` is an `int`, `stdout` is 0.
- [ ] **Step 4: Run the loader test** and compare the marker bytes to the expected strings; fix until exact.
- [ ] **Step 5: Commit** compat + test.
### Task 3: Cell-grid X11 backend
**Files:**
- Create: `apps/termtris/src/vt1200/x11.c`
- Create: `apps/termtris/src/vt1200/x11.h`
- Test: `apps/termtris/tests/render.c` + screenshot
**Interfaces:**
- Produces: `int vt_x11_init(void)` — open display, install XG, create 480×312 window at (80,80), select input, map, create GC, `XSync`; returns 0 on success.
- Produces: `void vt_x11_flush(void)` — draw dirty cells.
- Produces: `term_*` callback implementations: `x11_reset`, `x11_clearscr`, `x11_setcursor(row,col)`, `x11_cursor(show)`, `x11_setcolor(fg,bg)`, `x11_ibmchar(c,attr)`; `term_putstr` loops `x11_ibmchar`.
- Produces: `vt_x11_key(int *ks, int *ch)`? No — input is in Task 4; this task only exposes the window/display globals via `x11.h`.
- [ ] **Step 1: Write `render.c`** that calls `vt_x11_init`, fills an 80×24 cell buffer with a border pattern and `TERMTRIS` text, marks all cells dirty, flushes, and exits via a marker.
- [ ] **Step 2: Implement the cell grid** with `unsigned short ch; unsigned char fg, bg;` per cell; `x11_ibmchar` writes at the cursor and advances; `attr` decode `fg=(attr>>4)&0xf`, `bg=attr&0xf`; mono pixel `(fg==0?0:1)`, `(bg==0?0:1)`.
- [ ] **Step 3: Implement flush** per dirty cell: `XSetForeground(bg)`, `XFillRectangle(x,y,6,13)`, `XSetForeground(fg)`, `XDrawString(x,y+10,ch,1)`; then `XFlush`.
- [ ] **Step 4: Metrics test** — render a row of `'#'` at columns 0..79, a 20×18 playfield outline, and the word `TERMTRIS`; capture the frame at 3300 with `LOADER_SNAP=1`; visually verify 6-px columns and 13-px rows, no shear.
- [ ] **Step 5: Commit** backend + screenshot evidence.
### Task 4: Input, timer, and main loop
**Files:**
- Create: `apps/termtris/src/vt1200/main.c`
- Test: `apps/termtris/tests/input.c` + loader run with injected keys
**Interfaces:**
- Produces: `long now_ms(void)` — 32-bit accumulator over the 16-bit `0x1E93C0`; `void sleep_ms(int ms)` — `sleep(0x3FB33A20)`.
- Produces: `int vt_poll_event(void)` — `XPending` (`0x3FD67620`) > 0; `void vt_handle_event(void)` — `XNextEvent`, `XLookupString`, map to `game_input`.
- Produces: `int main(void)` — `vt_install_xg`, `vt_x11_init`, `init_game`, loop: drain events, `now=now_ms()`, `next=update(now)`, `sleep_ms(min(next-now,10))`; Expose -> `full_redraw` via `game_input('`')`; FocusOut -> `game_input('p')`.
- [ ] **Step 1: Write `input.c`** that initializes the game/window, then on the first `KeyPress` calls `XLookupString` and stores `keycode`, `keysym`, and the mapped `game_input` byte in markers; inject `A`, Up, Down, Left, Right, `0`, Enter, Backspace.
- [ ] **Step 2: Implement the key map**: letters lower-cased via `XLookupString`; `XK_Left 0xFF51` -> `game_input(0x9b); game_input('A')`; Right 0xFF53 -> `'D'`; Up 0xFF52 -> `'A'?` (use `'w'`); Down 0xFF54 -> `'B'`? Use the game's CSI codes: `0x9b` + `A/B/C/D` for up/down/right/left. Return/Enter -> `'\n'`; Backspace -> `'\b'`; space -> `' '`; Escape -> `27`; digits/letters direct.
- [ ] **Step 3: Implement the timer** and idle sleep; test that the game's `update` is called ~every 50–900 ms by recording a counter in markers.
- [ ] **Step 4: Run with the injected keys**; verify the mapped bytes (A -> 'a', Up -> CSI 'A', etc.) and that the process keeps running.
- [ ] **Step 5: Commit** main/input.
### Task 5: Game integration, score table, and COFF
**Files:**
- Modify: `apps/termtris/src/game.c` (only `#ifdef VT1200` includes/compat if needed; game logic unchanged)
- Create: `apps/termtris/src/vt1200/scoredb.c`
- Modify: `apps/termtris/build.sh` to compile `game.c`, `pieces` (header), backend, compat, scoredb, entry
- Test: loader run + screenshot + play
**Interfaces:**
- Consumes: all Task 2–4 interfaces.
- Produces: `build/termtris.coff`, `build/termtris.map`.
- `scoredb.c` implements `read_scores`, `save_score`, `print_scores` with a static 10-entry list; `username` default `"VT1200"`.
- [ ] **Step 1: Compile `game.c`** with the 1990 compiler; fix only portability issues (missing `fflush`/`sprintf` via shims, no `unistd`/`fcntl`); do not change game rules.
- [ ] **Step 2: Implement `scoredb.c`** with a static array; `read_scores` returns it; `save_score` sorts/inserts; `print_scores` no-op/fills the list.
- [ ] **Step 3: Build the full COFF**; verify the map: `.text/.data/.cinit` at `018A0000`, `.bss` at `01A00000`, entry `_start`, no undefined symbols.
- [ ] **Step 4: Load and screenshot** at frame 3000 with `dwt/loader/loader.lua`; the Terminal Manager is replaced/covered by the termtris board window.
- [ ] **Step 5: Play** — inject `A/D/S/W`, `0`, `P`, `R`, `H`, `Q`; capture frames showing piece movement, score update, help/score panels; verify quit/restart.
- [ ] **Step 6: Commit** game + scores.
### Task 6: Documentation and real-unit path
**Files:**
- Create: `harness/mame/TERMTRIS_REPORT.md`
- Modify: `apps/termtris/README.md` (build/run/real-unit)
- Maybe: `apps/termtris/run.sh` convenience wrapper
- [ ] **Step 1: Write the report** with the toolchain, the XG root cause/fix, the 6×13/80×24 geometry, the timer, the keyboard map, the loader command, evidence screenshots, and the ROM md5 check.
- [ ] **Step 2: Document the real unit** — same COFF via `dwt/loader/hostload.py`; the XG fix is required; the X server must accept the local connection (default); click the window to focus.
- [ ] **Step 3: Add a `run.sh`** that builds (optional) and runs MAME with the loader and a deterministic input script for CI.
- [ ] **Step 4: Final verification** — `md5sum vt1200_v22.bin` unchanged; no MAME/ROM edits; `git status` only our paths.
- [ ] **Step 5: Commit** docs/report.
---
## Self-review notes
- Spec coverage: vendoring, build, XG fix, renderer, input, timer, scores, acceptance, docs are each a task.
- The one open implementation choice left to the builder is the exact `sprintf` formatter internals; the required formats are pinned by Task 2's tests.
- The XG block size (1 KiB) is a deliberate value; if the Xlib writes beyond it, enlarge the static buffer.
- The input map uses `XLookupString` for letters/digits and keysyms for arrows/editing keys; the exact game escape codes are the game's existing `game_input` codes.
@@ -0,0 +1,138 @@
# termtris for the VT1200 — design
Date: 2026-11-03. Status: ready for implementation (input path proven).
## Intent
Run `jtsiomb/termtris` as a **local VT1200 program**: loaded into main RAM by the
existing COFF loader, displayed through the terminal's **ROM X11 server**, and
controlled from the real X keyboard events. End-to-end proof is a playable game in
MAME, with the real-unit path documented.
## What is already proven (2026-11-03)
- **Compiler**: the 1990 r5.01 OS/2 toolchain under the patched 2ine fork builds
ANSI C. Pipeline: `gspac -> gspopt -O2 -> gspcg -> gspa -> gsplnk` -> absolute
COFF, linked at bit `0x018A0000` (`.bss` at `0x01A00000`).
- **Loader**: `dwt/loader` pokes the COFF, `create_process`es the entry, and the
program runs. `.cinit` static initialisers run via our own `var_init` in
`entry.asm`; the linker's `cinit` symbol is at the start of `.cinit`.
- **ROM Xlib**: `XOpenDisplay`, `XCreateSimpleWindow`, `XSelectInput`,
`XMapWindow` (`0x3FD64F60`; `0x3FD645B0` is `XMapRaised`), `XSync`,
`XCreateGC`, `XSetForeground`, `XFillRectangle` (`0x3F99E8F0`),
`XDrawString` (`0x3FDA0000`), `XNextEvent` (`0x3F9C1CF0`),
`XLookupString` (`0x3F9A9C30`) all work from C with the TI ABI.
- **Rendering**: the default GC's font is **6×13** (baseline +10); an 80×24 cell
grid is 480×312. White rectangle + text were rendered in a WM-framed window.
- **Input (the blocker, now solved)**: a `create_process` child has an
**uninitialised per-process Xlib context**. `create_process` leaves
`*(proc+0x100)` pointing at a zeroed word; `_XEnq` derives
`XG = *(*(proc+0x100))`, so `XG == NULL` and its event free list at
`XG+0xCC0` (bit) reads low-RAM garbage. KeyPress nodes were therefore invalid
and `XNextEvent` copied unmapped zeros.
**Fix**: before `XOpenDisplay`, set
`*(u32 *)(*(u32 *)(proc + 0x100)) = (u32)zeroed_block`, where `proc` is
`*(u32 *)0x1B3900` and `zeroed_block` is a static `.bss` buffer (1 KiB is
enough). With that, the local client receives real events:
KeyPress `type=2`, `keycode=194` (LK401 `C2`), `window=0x00400001`, and
`XLookupString` returns `n=1, char='a', keysym=0x61`.
- **Timing**: `sleep(ms)` at `0x3FB33A20` blocks and yields; the raw millisecond
counter is the 16-bit `0x1E93C0` (+17 per display interrupt). Use a 32-bit
accumulator with `(u16)(now - last)` to avoid the 84 s wrap.
- **TCP X** also works (`xdpyinfo`/`xev` against `100.103.100.103:0` with the
real unit's NVRAM security settings); it is useful for debugging, but the game
uses the local service (`"0::0"`), so it needs no network.
## Goals
- `termtris` game logic (pieces, scoring, levels, spawn, collision) unchanged in
behaviour.
- X11 cell-grid renderer: 80×24 characters, 6×13 cells, monochrome pens.
- Real X11 input: `KeyPress` -> `XLookupString`/keysym -> `game_input`; arrows
mapped to the game's CSI sequences, letters lower-cased.
- Runs from local RAM under `dwt/loader`, with a screenshot and a playable run.
- A build script and a short report documenting the toolchain, the leak/loader
addresses, the XG fix, and the limits.
## Non-goals
- No host-side X client for the game (the x86 `xev` was a diagnostic only).
- No terminal/ANSI backend on the VT1200 (the game is X11).
- No modification of the ROM or MAME. All fixes are runtime RAM writes.
- No file-backed high-score table (the VT1200 has no filesystem); scores live in
RAM for the session.
## Approaches considered
1. **Local Xlib + XG initialisation (chosen).** Proven; uses the terminal's real
X server and keyboard; no network; the game is a local `create_process` child.
2. Local Xlib + OS pointer/keycode polling. Reliable fallback, but the keyboard
global is lossy (no release/repeat) and the pointer is an odd controller.
Keep as a compile-time fallback only.
3. Host x86 X client over TCP. Works and was used for diagnosis, but does not
satisfy "run from VT1200 RAM".
4. Terminal/ANSI backend. Not X11 and not the request.
## Architecture / files
```
apps/termtris/ vendored upstream (GPLv3, COPYING kept)
src/game.c, pieces.h upstream, small VT1200 patches
src/vt1200/
main.c X11 event loop, timer, key mapping
x11.c term_* callbacks -> cell grid -> XFillRectangle/XDrawString
xlib.h ROM entry-point addresses + TI-ABI prototypes
xgfix.c per-process Xlib context installation
scoredb.c RAM-only high scores
compat.c sprintf subset, fflush/stdout stubs
entry.asm _start + cinit var_init + rets
build.sh 2ine/r5.01 pipeline with stage retries
README.md build/run/real-unit instructions
```
Rendering is a small virtual terminal: `term_setcursor`/`term_putstr`/
`term_setcolor`/`term_ibmchar` update a cell buffer; `wait_display()` flushes
changed cells with `XFillRectangle` (background) and `XDrawString` (glyph).
The 6×13 metrics are measured, not assumed. A full redraw is 80×24 cells.
Input: `XSelectInput` with `KeyPressMask|KeyReleaseMask|ButtonPressMask|
ExposureMask|StructureNotifyMask|FocusChangeMask`; the event loop drains
`XNextEvent`; `XLookupString` gives the character/keysym. Arrow keys become
`ESC [ A..D` for `game_input`; `FocusOut` pauses.
Timing: `now_ms()` accumulates `0x1E93C0` deltas; the idle path is
`sleep(min(next_tick - now, 10))`, so the CPU yields to the X server and the OS.
## Build
`entry.asm` supplies the process entry and runs the `cinit` table (the 1990
runtime's `var_init` is local, so the entry implements the 16-bit-length/32-bit-
destination copy itself). The linker command file places `.text`/`.data`/`.cinit`
at `0x018A0000` and `.bss` at `0x01A00000`. The build script retries the 2ine
stage crashes (they exit 139 after producing output) and checks for outputs, not
exit codes.
## Acceptance
In MAME, with the loader:
- the window appears with the termtris board and score panels;
- `A/D/S/W`, arrows, `0`/Enter, `P`, `R`, `H`, `Q`/Backspace behave as upstream;
- a screenshot of the playfield and a short play session are captured;
- the ROM md5 is unchanged.
Real unit: the same COFF can be delivered with `dwt/loader/hostload.py` once the
OS is up; the document will state that the XG fix is required there too and that
the real unit's X server supplies the same keycodes (LK401).
## Risks
- The 2ine toolchain intermittently segfaults after writing outputs; the builder
retries and verifies every artifact.
- The per-process XG block size is inferred (we used 1 KiB); if the Xlib uses
more, enlarge the buffer. It worked through 9 events in the test.
- `sprintf` is absent from the 1990 runtime; `compat.c` implements only the
formats the game uses (`%d`, `%ld`, `%s`, `%c`, width, left-justify, `%%`).
- Font metrics are the ROM default GC's 6×13; do not use a different GC/font
without re-measuring.
- GPLv3: the vendored tree keeps `COPYING` and the copyright headers; the repo's
own license is unchanged.
+1
View File
@@ -76,6 +76,7 @@ Reports in this directory:
| `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically | | `BRINGUP_LOG.md` | the whole bring-up narrative, **including withdrawn claims** — read it sceptically |
| `ROMPATCH_REPORT.md` | runtime ROM patching (debug only — see the caveat) | | `ROMPATCH_REPORT.md` | runtime ROM patching (debug only — see the caveat) |
| `HELLO_WORLD_REPORT.md` | the TMS34010 SDK assembles an 82-byte payload of **our own** code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism | | `HELLO_WORLD_REPORT.md` | the TMS34010 SDK assembles an 82-byte payload of **our own** code; it runs, writes RAM markers and paints a VRAM block. Debugger entry, not a delivery mechanism |
| `TERMTRIS_REPORT.md` | the upstream tetris game built with the 1990 r5.01 C toolchain and run **inside** the terminal: ROM Xlib rendering, real X11 key events, the XG-context fix and the row-batched flush |
## Address model (the important part) ## Address model (the important part)
+125
View File
@@ -0,0 +1,125 @@
# TERMTRIS_REPORT — the upstream tetris game running inside the VT1200
`apps/termtris/` builds **our own TMS34010 program** from John Tsiombikas'
`termtris` (upstream commit `7c5435dd4e93ffc10f3b5c68fe1ae9718a365258`) with the
1990 TI r5.01 C toolchain under 2ine, loads it into VT1200 RAM and runs it on
the emulated terminal's own X server. It draws through the ROM Xlib and reads
real X11 `KeyPress` events. No host client, no terminal escape output, no ROM
patches.
Full port documentation: `apps/termtris/VT1200.md`.
## Build
```
apps/termtris/build.sh src/vt1200/main.c src/game.c src/vt1200/scoredb.c \
src/vt1200/x11.c src/vt1200/compat.c src/vt1200/xgfix.c
```
`build.sh` runs `gspac`/`gspopt`/`gspcg`/`gspa` and `gsplnk` through
`/home/arti/os2-work/2ine/build/lx_loader` against the r5.01 SDK zips under
`tools/tms34010-sdk/1990-11 TMS340 Family SDK/`. Those 2ine-hosted tools
frequently segfault after writing their output, so the wrapper treats an output
file as success and retries a missing one. Two quirks are handled there:
* `gspac` needs `-i.` for the angle-bracket shim headers;
* an empty `.cinit` makes `cinit` point at `.text`, whose first instruction the
entry walker would read as a record count — when the map reports `.cinit`
length 0 the link is repeated with the one-word `CINITZ.obj` terminator.
Result (map):
```
.text 018A0000 .cinit 018B4B10 .bss 01A00000
ENTRY POINT SYMBOL: "_start" address: 018a0000
```
These are **bit** addresses, as everywhere on the TMS34010.
## The one required Xlib fix
A child created by `create_process` has a NULL per-process Xlib context
(`*(*(proc + 0x100))`). The ROM Xlib's `_XEnq` treats that as the queue head, so
`XNextEvent` copies unmapped garbage and never returns an event.
`src/vt1200/xgfix.c` writes a zeroed 1 KiB block into that per-process word
before `XOpenDisplay`. RAM only; the ROM is unchanged.
Smoke test evidence (`apps/termtris/tests/smoke.sh`):
`type=2 keycode=194 lookup_n=1 ch=61 ks=0061` — a real `KeyPress` and
`XLookupString` returning `a`.
## Rendering and the flush deadlock
80×24 cells of 6×13 px (the default GC font, baseline +10) → a 480×312 window.
The raster is 1 bit deep, so the backend drops colour (like the game's `-m`
monochrome mode) and treats the bold bit (`0x80`) as reverse video. That
reversal is this backend's choice; upstream `ansi.c -m` only emits SGR bold. The playfield
is black spaces; tetrominoes are the `[`/`]` tiles.
`vt_x11_flush()` repaints **whole rows** — one fill for the row background, one
`XDrawString` for its 80 characters, fill/string pairs for reversed spans, then
a single `XFlush`. This is not an optimisation for looks:
* the per-cell version (4 requests/cell) outran the in-machine X server;
* an `XSync` per row **deadlocked the client** — the debug marker
`_vt_x11_progress` froze at `0x00010014` (row 20), the main-loop counter
stayed 0 and the game window never got a preview piece. Batching a row makes
a full redraw a few hundred bytes and `loops` reached 300+ between frames
2500 and 3200.
## Timing and keys
`now_ms()` accumulates the 16-bit counter at bit `0x1E93C0` into 32 bits;
`sleep_ms()` calls the ROM `sleep` (`0x3FB33A20`) with a 1–10 ms clamp. The
game's own `update()` picks the tick (887 ms at level 0).
`KeyPress` → `XLookupString` → lower-cased letters to `game_input()`; arrows as
the game's CSI codes (`0x9b` + `A`/`B`/`C`/`D`), which it maps back to
`w`/`s`/`d`/`a`; `0x7F` is Backspace/Delete. `ExposureMask` is not selected:
a map-time `Expose` fires the game's full-redraw before it has a current piece,
which draws a bogus piece over the top row (blank border plus stray `X` tiles).
## Reproduce
```sh
cd /home/arti/.local/share/opencode/worktree/26e92e/termtris
apps/termtris/tests/play.sh # load, click, snapshot
apps/termtris/tests/play_keys.sh # move / panels / quit
apps/termtris/run.sh # interactive MAME session
```
The run is `VT1200_SKIP_LEVEL1=1 LOADER_AT=2500` with the loader payload at bit
`0x01880000` and the COFF at `0x01900000`; at frame 2500 the script enters
`dwt/loader/build/loader.bin`, which places the sections and calls
`create_process`.
Last full acceptance run:
```
move DIAG: loops=354 events=18 focus=00400001 quit=0
panels DIAG: loops=346 events=12 focus=00400001 quit=0
quit DIAG: loops=300 events=10 focus=00400001 quit=1
```
`focus=00400001` is the termtris window, and `quit=1` is the game's global after
`q` — so the keys reached the game, not just the window.
| evidence | file |
|---|---|
| board, panels, preview | `apps/termtris/evidence/termtris_frame3300.png` |
| after a/d/s/w/0 | `apps/termtris/evidence/termtris_move_3200.png` |
| after h and r | `apps/termtris/evidence/termtris_panels_3200.png` |
| after q (`quit=1`) | `apps/termtris/evidence/termtris_quit_3200.png` |
| renderer test pattern | `apps/termtris/evidence/render_frame3400.png` |
## Limits
* No sound, no filesystem: scores live in `.bss` and vanish with the process.
* `q` sets the game's `quit` and the loop exits, but the window stays on screen
— the created process does not tear down its X connection.
* Startup is retried (up to 40 × 50 ms) because `XOpenDisplay`/window creation
intermittently fails while the display server settles.
* Tetrominoes are text tiles; a 1-bit cell has no other shape to offer.
The ROM `vt1200_v22.bin` is never modified — `md5 b46a3e442ccb86134d2356bd70d22fd2`
before and after.