ivanizag / iz-cpm Goto Github PK
View Code? Open in Web Editor NEWPortable CP/M emulation to run CP/M 2.2 binaries for Z80
License: BSD 3-Clause "New" or "Revised" License
Portable CP/M emulation to run CP/M 2.2 binaries for Z80
License: BSD 3-Clause "New" or "Revised" License
The documentation for Function 10: Read Console Buffer says, in the keybinding section:
That's something I've been used to pressing at the CCP prompt, because it can trigger the execution of a $$$.SUB
file (and also allowed you to swap disks back in the day)
However your emulator doesn't reload - while #13 is open you could see the difference in "running" vs "restart" by watching the change of user-number, or drive, and of course until #16 is resolved SUBMIT is a non-issue too.
You catch Ctrl-C for your "exit emulation" so I guess this might need some reworking. I did the same thing for a long time, but eventually patched in a HALT, EXIT, and QUIT command to the CCP - they just run HALT
instructions which is enough for my emulator to terminate.
I also defaulted to requiring two CtrlCs in a row to trigger the reboot, but later relented and made this configurable at run-time, via a bios extension and/or CLI flag.
I guess this is a minor issue as reloads happen after control returns from child processes, but I admit I've become very familiar with pressing the key as a matter of habit nowadays.
First of all thank-you for sharing your code. I've been slowly working on a CP/M emulator of my own over the past month or two - mine in golang, and I appreciate the comments you left in your code which were a helpful resource at times - to the extent I linked to your project from my README.
Anyway I figured I should finally actually test your project, and I found an interesting issue. Sorry!
So I've put together a repository of "standard" software, much like you setup with your download.sh
script. My repository is here:
Clone that to follow along. My initial attempt was to compile a hello-world assembly-program, and run it:
$ RUST_BACKTRACE=1 ./iz-cpm --disk-a /home/skx/Repos/github.com/skx/cpm-dist/A/ --disk-b /home/skx/Repos/github.com/skx/cpm-dist/B/
iz-cpm https://github.com/ivanizag/iz-cpm
CP/M 2.2 Emulation
Press ctrl-c ctrl-c Y to return to host
A>asm hello
CP/M ASSEMBLER - VER 2.0
011B
000H USE FACTOR
END OF ASSEMBLY
A>load hello
FIRST ADDRESS 0100
LAST ADDRESS 011A
BYTES READ 001B
RECORDS WRITTEN 01
A>hello
Hello, World!
Looks good.
Now I run DDT.COM
and using that JUMP to address 0x0000, which is a boot:
A>ddt
DDT VERS 2.2
-g0000
A>asm hello MI
Hello, World!
$
CP/M ASSEMBLER - VER 2.0
011B
000H USE FACTOR
END OF ASSEMBLY
Notice there that there is some Hello, world!
output? And yet the previous time we compiled that we just got the compilation as expected.
Let us try to load the .COM
file from the generated .HEX
- again starting from the top:
frodo ~/Downloads/iz-cpm-linux-v1.3.1 $ RUST_BACKTRACE=full ./iz-cpm --disk-a /home/skx/Repos/github.com/skx/cpm-dist/A/ --disk-b /home/skx/Repos/github.com/skx/cpm-dist/B/
iz-cpm https://github.com/ivanizag/iz-cpm
CP/M 2.2 Emulation
Press ctrl-c ctrl-c Y to return to host
A>asm hello
CP/M ASSEMBLER - VER 2.0
011B
000H USE FACTOR
END OF ASSEMBLY
A>load hello
FIRST ADDRESS 0100
LAST ADDRESS 011A
BYTES READ 001B
RECORDS WRITTEN 01
A>ddt
DDT VERS 2.2
-g0000
A>asm hello MI
Hello, World!
$
CP/M ASSEMBLER - VER 2.0
011B
000H USE FACTOR
END OF ASSEMBLY
A>load hello :100100000E09110901CD0500C90D0A48656C6C6F17
:0B0110002C20576F726C64210D0A2434
:0000000000
thread 'main' panicked at src/bdos_environment.rs:107:19:
index out of bounds: the len is 16 but the index is 204
stack backtrace:
0: 0x7f76720b52dc - std::backtrace_rs::backtrace::libunwind::trace::ha752d28ad8b59776
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/../../backtrace/src/backtrace/libunwind.rs:104:5
1: 0x7f76720b52dc - std::backtrace_rs::backtrace::trace_unsynchronized::h19085f9ccfa8c39c
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
2: 0x7f76720b52dc - std::sys_common::backtrace::_print_fmt::h7fa8e07ec2f6097e
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys_common/backtrace.rs:67:5
3: 0x7f76720b52dc - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::hf031f84ba50c0cfa
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys_common/backtrace.rs:44:22
4: 0x7f76720ea2e0 - core::fmt::rt::Argument::fmt::h41c4ec8113ce6748
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/fmt/rt.rs:142:9
5: 0x7f76720ea2e0 - core::fmt::write::h9d6dcce53fee8498
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/fmt/mod.rs:1120:17
6: 0x7f76720b2fdf - std::io::Write::write_fmt::h1592172be29051fa
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/io/mod.rs:1762:15
7: 0x7f76720b50c4 - std::sys_common::backtrace::_print::h077b25e4211c79a3
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys_common/backtrace.rs:47:5
8: 0x7f76720b50c4 - std::sys_common::backtrace::print::h0992ac2bfe77cad2
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys_common/backtrace.rs:34:9
9: 0x7f76720b65d7 - std::panicking::default_hook::{{closure}}::h2cf07aea02dfbfab
10: 0x7f76720b633f - std::panicking::default_hook::h85b79716976ec2d8
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:292:9
11: 0x7f76720b6a58 - std::panicking::rust_panic_with_hook::h8687ef8cbe256fb2
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:779:13
12: 0x7f76720b693e - std::panicking::begin_panic_handler::{{closure}}::ha182011c2febc20e
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:657:13
13: 0x7f76720b57a6 - std::sys_common::backtrace::__rust_end_short_backtrace::h8ea229012037d9c8
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/sys_common/backtrace.rs:170:18
14: 0x7f76720b66a2 - rust_begin_unwind
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:645:5
15: 0x7f7672012ae5 - core::panicking::panic_fmt::h4a1a6d8dbc505935
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/panicking.rs:72:14
16: 0x7f7672012ca2 - core::panicking::panic_bounds_check::h7ccafe65706d62d1
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/panicking.rs:190:5
17: 0x7f767201b8e2 - iz_cpm::bdos_environment::BdosEnvironment::get_directory::h47ac382745bda551
18: 0x7f76720152c4 - iz_cpm::bdos_file::find_host_files::hc275de410d62360d
19: 0x7f76720136bf - iz_cpm::bdos_file::open::h052ea4f54962a05d
20: 0x7f767201cf9b - iz_cpm::bdos::Bdos::execute::hed741525d0a22a20
21: 0x7f7672019ba4 - iz_cpm::main::hcddeec579b7c7c85
22: 0x7f767201d2f3 - std::sys_common::backtrace::__rust_begin_short_backtrace::h54006916c45c37ab
23: 0x7f767201cb19 - std::rt::lang_start::{{closure}}::he9f5b6b38ffdf303
24: 0x7f76720ae96e - core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once::hf47534de04578a83
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/ops/function.rs:284:13
25: 0x7f76720ae96e - std::panicking::try::do_call::he270afe99f162c7b
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:552:40
26: 0x7f76720ae96e - std::panicking::try::hc551f5c5a344ecad
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:516:19
27: 0x7f76720ae96e - std::panic::catch_unwind::h6790e138972db2df
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panic.rs:142:14
28: 0x7f76720ae96e - std::rt::lang_start_internal::{{closure}}::haa35f81c7641c384
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/rt.rs:148:48
29: 0x7f76720ae96e - std::panicking::try::do_call::h4e42cde918b15a44
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:552:40
30: 0x7f76720ae96e - std::panicking::try::hb3bfb1061a5e948b
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panicking.rs:516:19
31: 0x7f76720ae96e - std::panic::catch_unwind::h4789ec49cbf5f558
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/panic.rs:142:14
32: 0x7f76720ae96e - std::rt::lang_start_internal::h141a6b3fc711a41c
at /rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/std/src/rt.rs:148:20
33: 0x7f767201cb0e - std::rt::lang_start::haf5dbd08b5b7a937
I'm not going to try to guess what is going on there, but it looks like the boot, via "JMP 0x0000" puts things into a bad state, after which there is oddity in many commands.
I've got good news and bad news...
The bad news is that DR's CP/M isn't open source. It was semi-freed back in 2001 with a mostly-but-not-quite license (excerpted from http://www.gaby.de/cpm/license.html):
Let this email represent a right to use, distribute, modify, enhance and
otherwise make available in a nonexclusive manner the CP/M technology as
part of the "Unofficial CP/M Web Site" with its maintainers, developers and
community.
So, it can't be legally distributed anywhere other that www.gaby.de/cpm. It's very unlikely that anyone would be sued for distributing it, but it is a license violation and so will prevent distribution in, for example, Debian. (I've been looking into the licensing of CP/M software for a project of mine where I've been trying to produce a properly open source CP/M distribution, at http://github.com/davidgiven/cpmish.)
The good news is that ZSDOS and ZCPR1 are drop-in replacements and are distributable. At least, they do to the best of my knowledge; licensing back then was a mess. But I'm pretty confident that ZCPR1 is public domain (but not subsequent versions) and ZSDOS is actually GPLd. I've got copies that have been patched to build with zmac at https://github.com/davidgiven/cpmish/tree/master/third_party/zcpr1 and https://github.com/davidgiven/cpmish/tree/master/third_party/zsdos if you want them.
[ugly PR coming shortly. Feel free to ignore/close]
The version of BBC BASIC you have in your download script doesn't fully work - specifically SAVE
and LOAD
both fail:
$ ./target/debug/iz-cpm --disk-a ./software/bbcbasic/
iz-cpm https://github.com/ivanizag/iz-cpm
CP/M 2.2 Emulation
Press ctrl-c ctrl-c Y to return to host
A>bbcbasic
BBC BASIC (Z80) Version 3.00
(C) Copyright R.T.Russell 1987
>10 PRINT "STEVE"
>SAVE "FOO"
CP/M Error
>SAVE "FOO.BBC"
CP/M Error
>*CPM
A>
Press Y to exit iz-cpm. Any other key to continue.
I had the same issue in my emulator, which I worked around but it looks like I'm going to have to go back and fix it properly.
Looking at what happens when a successfull result is achieved we end up
with the following series of calls:
>SAVE "FOO"
[[BDOS command 19: F_DELETE(005c)]][[Delete file FOO.BBC]][[=>0000]]
[[BDOS command 22: F_MAKE(005c)]][[Create file FOO.BBC]][[=>0000]]
[[BDOS command 26: F_DMAOFF(3e00)]][[BDOS command 40: F_WRITEZ(005c)]][Write random record 0 into 3e00][[=>0000]]
[[BDOS command 16: F_CLOSE(005c)]][[=>0000]]
If the F_DELETE fails then the F_MAKE is skipped and we get an error. Similarly the rest of the calls - if one of them fails the chain is broken and the save fails.
I eventually worked out that the issue here seems to be that H/L are tested rather than A, and that meant we needed to use your 16-bit return value.
Additionally a failure to delete the file, if it didn't exist, killed things dead too. So I faked an OK result in that in my PR.
Hi,
File size -> It returns -> What it should return
(wrong) 0 bytes -> 1 record -> 0 records
(correct) 1 byte -> 1 record -> 1 record (correct)
(correct) 127 bytes -> 1 record -> 1 record (correct)
(wrong) 128 bytes -> 3 records -> 1 records
(wrong) 129 bytes -> 3 records -> 2 records
(wrong) 255 bytes -> 3 records -> 2 records
(wrong) 256 bytes -> 4 records -> 2 records
(wrong) 257 bytes -> 4 records -> 3 records
I looked at the function and it looks like it adds 1 twice for some reason, but It should only add a 1 a single time and then only if the filesize % RECORD_SIZE is non-zero (% being a modulus operator).
[Reported by email]
I would like to report bug in this project. I tried compile something in PascalMt+ so I called iz-cpm from Window's command line like this:
iz-cpm mtplus.com test.pas
It doesn't works. Files mtplus.com and test.pas are in the same directory with iz-cpm.exe. It looks like iz-cpm provides incorrect CP/M command tail to mtplus.com. When I tried compile my code directly from iz-cpm command line it worked OK. I have attached screenshot. I marked issues by red rectangles. Can you solve it?
I suspect resolving this will involve a number of changes, but the short version is simple enough:
There is a related utility I found called "slash", source/binary available within my distribution of utilities:
The short version is that both of these utilities work the same way, they allow the submission of a number of sequential commands to be automatically executed.
SUBMIT.COM will read a text file containing commands, with optional parameter expansion. It will then generate a file $$$.SUB
on A:, which should be processed the next time the CCP is reloaded.
SLASH.COM is similar, but it allows you to create the $$$.SUB
file without the need for a temporary file, for example:
SLASH C:HELLO ; A:HELLO ; C:ECHO TEST
Both of these utilities should produce a file named $$$.SUB
. That file will contain N 128-byte records, one record for each command. The records will be in reverse order:
I played around with fixing this but I hit a bunch of errors which both annoyed and confused me, so I think this will be better placed for you to attack if you wish!
The first issue is that the drive-reset function 13, DRV_ALLRESET, is supposed to return 0xFF if there is a $$$.SUB file present, but ast the moment you hard-code the return value as 0x00. I thought that updating that function to return 0xFF would be OK - because if the file were present it would then be processed, and in the case where opening failed the CCP would move on. But unfortunately it wasn't that simple.
If I make this change:
@@ -166,6 +166,7 @@ impl Bdos {
},
13 => { // DRV_ALLRESET - Reset disk system
env.state.reset();
+ res8 = Some(0xff)
},
14 => { // DRV_SET - Select disk
bdos_drive::select(env, arg8);
@@ -293,4 +294,4 @@ fn get_version() -> u16 {
and random access functions.
*/
0x0022 // CP/M 2.2 for Z80
-}
\ No newline at end of file
+}
The end result is actually a failure to open the file:
$ ./target/debug/iz-cpm --disk-a /home/skx/Repos/github.com/skx/cpm-dist/A
iz-cpm https://github.com/ivanizag/iz-cpm
CP/M 2.2 Emulation
Press ctrl-c ctrl-c Y to return to host
Athread 'main' panicked at 'attempt to add with overflow', src/fcb.rs:162:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Running with tracing that's trying to open the file, which does exist:
[[BDOS command 32: F_USERNUM(0000)]][[=>00]]
[[BDOS command 13: DRV_ALLRESET(0000)]][[=>ff]]
[[BDOS command 17: F_SFIRST(f07a)]][[DIR start A:$$$.SUB]][[=>00]]
[[BDOS command 25: DRV_GET(f00a)]][[=>00]]
A[[BDOS command 32: F_USERNUM(f0ff)]][[=>00]]
[[BDOS command 15: F_OPEN(f07a)]][[Open file A:$$$.SUB]][[=>00]]
thread 'main' panicked at 'attempt to add with overflow', src/fcb.rs:162:18
stack backtrace:
I stopped investigating there.
However I suspect if you resolve this problem you'll hit another, I see you don't truncate files when they're closed, and so I suspect that if you return the value to indicate the SUB-file is present, and fix the opening you'll end up with an infinite loop:
$$$.SUB
Unless/until the truncation of the last record happens it'll just run forever, until cancelled.
[I have a patch ready for this, which I will submit shortly]
When I was writing my emulator I wrote some test programs to cement my understanding of some of the expected behaviour. One simple script I wrote was ret.z80, which is a trivial binary which just tries to exit in a couple of different ways:
There is a binary which is located within that same directory which can be used for testing - but note that you really want to map this to a non-default drive. In my case I see this when I test:
$ ./target/debug/iz-cpm --disk-a /home/skx/Repos/github.com/skx/cpm-dist/A/ --disk-b /home/skx/Repos/github.com/skx/cpmulator/samples/
iz-cpm https://github.com/ivanizag/iz-cpm
CP/M 2.2 Emulation
Press ctrl-c ctrl-c Y to return to host
A>b:
B>dir ret.*
RET .COM | RET .Z80
B>ret 1
B>ret 2
B>ret 3
B>ret 4
A>
Here you see that the execution of the binary succeeded in each of the invokations, each time the binary was launched and then terminated. However The last run switched back to the default A-drive.
You correctly pass this value along when reseting via the boot:
let user_drive = machine.peek(CCP_USER_DRIVE_ADDRESS);
cpu.registers().set8(Reg8::C, user_drive);
However that value is never set. I've made a couple of other changes to remove the "= 0" from the drive/user setting in the rest/state, and added the missing stores.
That allows things to work as expected.
Installing on Debian bullseye. Had to enable the sid repository in order to get a version of cargo that could compile. Nonetheless, getting the following error:
error: unknown --json
option future-incompat
error: unknown --json
option future-incompat
error: unknown --json
option future-incompat
error: could not compile ansi_term
warning: build failed, waiting for other jobs to finish...
error: could not compile unicode-width
error: could not compile libc
error: unknown --json
option future-incompat
error: could not compile strsim
Hi,
Thanks for the interesting emulator! :)
Is there a chance to get instructions how to get iz-cpm compiled under a ARM linux OS (armbian)?
When I try to cargo check/build I do get the error-message:
root@npi-neo2-23(192.168.6.23):/toshiba/iz-cpm-1.0# cargo check
error: failed to parse lock file at: /toshiba/iz-cpm-1.0/Cargo.lock
Caused by:
invalid serialized PackageId for key package.dependencies
Which dependencies do I need and how do I install them?
Kind Regards
Guido
Using Calcstar on an exising file with 3072 bytes:
CalcStar Dump vr_1.45
CalcStar file name (<Return> to quit): demo.csd
[[BDOS command 26: F_DMAOFF(0080)]]
[[BDOS command 15: F_OPEN(cd1a)]][[Open file A:DEMO.CSD]][[=>00]]
[[BDOS command 35: F_SIZE(cd1a)]][[Size of A:DEMO.CSD]]
[[BDOS command 26: F_DMAOFF(cbbe)]]
[[BDOS command 33: F_READRAND(cd1a)]][Read random record 18 into cbbe][[=>01]]
** demo.csd not found on disk
CalcStar file name (<Return> to quit):
Reading record 0x18 goes just to the end of the file
I haven't run it down yet, but for some reason WordStar always saves to drive a: -- could be disk handling or could be some problem with the WS configuration. Try this:
../../target/debug/iz-cpm --disk-a . --disk-b ../wordstar33 --disk-c ../zork --disk-d ..
b:
ws
N (new non document)
B:TEST.TXT (or just TEST.TXT)
Anything (type anything)
^KX
The file TEST.TXT will now be on A: Even stranger, it tries to read from B. So if you do a ^KS instead of ^KX the file will be saved but it will reread from B where the file is blank. The same thing happens on other drives. The save winds up on A and the reread comes from the drive you intended. So if you save to drive A it works fine. Any other drive, no.
A related problem is a side effect. If you are testing and do ^KS to save, your file goes blank (because it saves to A and rereads from B). If you now save again (say a ^KX to get out), you will write a blank file over your original file on A so now when you type the file on drive A it will be blank. Not really a bug because it would not happen if the reread were picking up the file which it would if the save had occurred on that drive.
I'll let you know if I find anything else out. This is on a clone of the repo, by the way. I have not tried it with the release version.
I put together a simple input-testing binary, because I spent a lot of time fighting with console input, the source and binary can be found here:
Your emulator fails two tests, I think. This bug report is for the first failure.
Not true - second issue is a non-issue - I thought you were behaving incorrectly by terminating input of function 10: Read Console Buffer when the buffer is full, but I think on reflection that if the input size is 10 bytes you should return when the user enters 10 characters - without the need for RETURN. I guess I did the wrong thing in mine :)
I think reading the documentation that Function 3 / A_READ should not echo input. I'm basing that upon reading this guide:
Here we see:
Specifically note that in the second entry there is no reference to "echo".
Similarly in the bigger manual, http://www.gaby.de/cpm/manuals/archive/cpm22htm/ch5.htm#Section_5.2 we see a similar writeup. There is a reference to echo in C_READ but not in A_READ.
So I think my behaviour is correct, but I appreciate this might be a matter of interpretation.
Output from my emulator:
./cpmulator samples/intest.com
Simple input-test program, by Steve.
C_READ Test:
This test allows you to enter FIVE characters, one by one.
The characters SHOULD be echoed as you type them.
12345
Test complete - you entered '12345'.
A_READ Test:
This test allows you to enter FIVE characters, one by one.
The characters should NOT be echoed as you type them.
Test complete - you entered '12345'.
C_RAWIO Test:
This uses polling to read characters.
Echo should NOT be enabled.
Press 'q' to proceed/complete this test.
+
Output from yours:
$ ./target/debug/iz-cpm /home/skx/Repos/github.com/skx/cpmulator/samples/intest.com
Simple input-test program, by Steve.
C_READ Test:
This test allows you to enter FIVE characters, one by one.
The characters SHOULD be echoed as you type them.
12345
Test complete - you entered '12345'.
A_READ Test:
This test allows you to enter FIVE characters, one by one.
The characters should NOT be echoed as you type them.
12345 Test complete - you entered '12345'.
C_RAWIO Test:
This uses polling to read characters.
Echo should NOT be enabled.
Press 'q' to proceed/complete this test.
+
Note the echo'd characters in the section section.
how to redirect modem and printer ports to something?
COM port software for CP/M
https://github.com/mecparts/zmp
useful for file transfer
also this
http://www.z80.eu/kermit.html
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.