From 728d090815b25e4a824ecf3ee76bda4ac561448a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 10 Sep 2026 23:09:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(vqt6/vipermath/vipersimd/w32/requests):=20?= =?UTF-8?q?=E8=87=AA=E7=BB=98=20Canvas/Graph3D/ChartView=20=E4=B8=8E=20Qt6?= =?UTF-8?q?=20Charts=E3=80=81=E6=96=B0=E5=A2=9E=20gzip=20=E4=B8=8E=20torch?= =?UTF-8?q?-lite=20=E6=A8=A1=E5=9D=97,=20=E4=BF=AE=E5=8C=85=E8=A3=85?= =?UTF-8?q?=E7=B1=BB=E6=82=AC=E5=9E=82=E6=8C=87=E9=92=88=E4=B8=8E=E4=B8=AD?= =?UTF-8?q?=E6=96=87=E8=B7=AF=E5=BE=84/exp/=E5=A4=A7=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vqt6: 包装类统一补 __new__ 堆分配, 修"创建函数栈帧 alloca"被存进字段/跨帧返回后 _ptr 悬垂(传给 QLayout::addWidget 等即崩溃); 菜单类按依赖顺序前移, menuBar()/statusBar() 改返回裸指针; 接入 Qt6 Charts 并新增 _bridge_canvas(Canvas 手绘板 / Graph3D 软件 3D 网络图 / ChartView 折线图); 新增自绘 QRadioButton(圆环+内点, 过渡动画)、剪贴板、应用级字体族 API - 新增模块: gzip.vp(RFC1952/1951 解码器)、torch/(torch-lite 稠密张量 + conv/pool 前反向) - vipersimd: 新增 scalar_fma/add/mul/sqrt_array 未向量化对照内核(与对应 SIMD 版逐位一致) - w32: 新增 UTF-8/UTF-16 路径转换, CreateFile/CreateDirectory 走 W 系列, 修中文路径被按 GBK 误读; read_all 参数 max_count -> buf_size 并文档化"容量含终止符"契约 - requests: https 响应缓冲按需翻倍扩容(上限 256MB), 修 MB 级正文(如 MNIST .gz)被 4KB 静默截断 - vipermath: 重写 exp 为范围归约 + 短泰勒, 修负值发散(x=-30 得 3.7e10)与 NaN 触发 2^31 次缩放导致的卡死 - builtins: 新增 sizet/uzt 别名; stdarg: va_arg 参数 CType -> CPtr - chore: 删除 vendored jpeg change.log --- builtins.vp | 2 + fltk/cfltk/fltk/jpeg/change.log | 527 -------------- gzip.vp | 663 +++++++++++++++++ os/_win32.vp | 19 +- requests/https.vp | 30 +- stdarg.vp | 2 +- torch/__init__.vp | 925 ++++++++++++++++++++++++ vipermath.vp | 80 +-- vipersimd.vp | 103 +++ vqt6/CMakeLists.txt | 4 + vqt6/__init__.vp | 686 +++++++++++++----- vqt6/_bridge.h | 7 + vqt6/_bridge_canvas.cpp | 1192 +++++++++++++++++++++++++++++++ vqt6/_bridge_canvas.h | 99 +++ vqt6/_bridge_core.cpp | 26 + vqt6/_bridge_widgets.cpp | 136 +++- vqt6/_bridge_widgets.h | 1 + vqt6/_qtcore.vp | 7 + vqt6/_qtwidgets.vp | 62 ++ vqt6/_types.vp | 6 + w32/fileio.vp | 63 +- w32/win32base.vp | 42 ++ 22 files changed, 3921 insertions(+), 761 deletions(-) delete mode 100644 fltk/cfltk/fltk/jpeg/change.log create mode 100644 gzip.vp create mode 100644 torch/__init__.vp create mode 100644 vqt6/_bridge_canvas.cpp create mode 100644 vqt6/_bridge_canvas.h diff --git a/builtins.vp b/builtins.vp index 5fdf431..30f1fcd 100644 --- a/builtins.vp +++ b/builtins.vp @@ -28,6 +28,8 @@ def max(a: double, b: double) -> double: # rowptr 已内置于编译器(TPC/TPV 双端),作为语法保留字,不可重定义: # rowptr[T] 固定表示"裸指针,指向原始类型 T"(等价 t.CPtr[T])。 size_t: t.CTypedef = t.CSizeT +uzt: t.CTypedef = t.CSizeT +sizet: t.CTypedef = t.CSizeT int8: t.CTypedef = t.CInt8T int16: t.CTypedef = t.CInt16T int32: t.CTypedef = t.CInt32T diff --git a/fltk/cfltk/fltk/jpeg/change.log b/fltk/cfltk/fltk/jpeg/change.log deleted file mode 100644 index eae0113..0000000 --- a/fltk/cfltk/fltk/jpeg/change.log +++ /dev/null @@ -1,527 +0,0 @@ -CHANGE LOG for Independent JPEG Group's JPEG software - - -Version 9f 14-Jan-2024 ------------------------ - -Add build system for C++Builder/RAD Studio. - -Add build system for Xcode (beside configure). - -Add ARM64EC (Emulation Compatible) platform support in the -Visual Studio build. - - -Version 9e 16-Jan-2022 ------------------------ - -Include alternate user interface files for cjpeg/djpeg. - -jcparam.c: change default chrominance DC quantization factor -for lossless support. Note: Requires rebuild of test images. - -rdgif.c, cderror.h: add sanity check for GIF image dimensions. -Thank to Casper Sun for cjpeg potential vulnerability report. - -Add ARM and ARM64 platform support in the Visual Studio build. - - -Version 9d 12-Jan-2020 ------------------------ - -Optimize the optimal Huffman code table generation to produce -slightly smaller files. Thank to John Korejwa for suggestion. -Note: Requires rebuild of testimgp.jpg. - -Decoding Huffman: Use default tables if tables are not defined. -Thank to Simone Azzalin for report (Motion JPEG), -and to Martin Strunz for hint. - -Add sanity check in optimal Huffman code table generation. -Thank to Adam Farley for suggestion. - -rdtarga.c: use read_byte(), with EOF check, instead of getc() -in read_*_pixel(). -Thank to Chijin Zhou for cjpeg potential vulnerability report. - -jmemnobs.c: respect the max_memory_to_use setting in -jpeg_mem_available() computation. Thank to Sheng Shu and -Dongdong She for djpeg potential vulnerability report. - -jdarith.c, jdhuff.c: avoid left shift of negative value -compiler warning in decode_mcu_AC_refine(). -Thank to Indu Bhagat for suggestion. - -Add x64 (64-bit) platform support, avoid compiler warnings. -Thank to Jonathan Potter, Feiyun Wang, and Sheng Shu for suggestion. - -Adjust libjpeg version specification for pkg-config file. -Thank to Chen Chen for suggestion. - -Restore GIF read and write support from libjpeg version 6a. -Thank to Wolfgang Werner (W.W.) Heinz for suggestion. - -Improve consistency in raw (downsampled) image data processing mode. -Thank to Zhongyuan Zhou for hint. - -Avoid out of bounds array read (AC derived table pointers) -in start pass in jdhuff.c. Thank to Peng Li for report. - -Improve code sanity (jdhuff.c). -Thank to Reza Mirzazade farkhani for reports. - -Add jpegtran -drop option; add options to the crop extension and wipe -to fill the extra area with content from the source image region, -instead of gray out. - - -Version 9c 14-Jan-2018 ------------------------ - -jpegtran: add an option to the -wipe switch to fill the region -with the average of adjacent blocks, instead of gray out. -Thank to Caitlyn Feddock and Maddie Ziegler for inspiration. - -Make range extension bits adjustable (in jpegint.h). -Thank to Robin Watts for suggestion. - -Provide macros for fflush() and ferror() in jinclude.h in order -to facilitate adaption by applications using an own FILE class. -Thank to Gerhard Huber for suggestion. - -Add libjpeg pkg-config file. Thank to Mark Lavi, Vincent Torri, -Patrick McMunn, and Huw Davies for suggestion. - -Add sanity checks in cjpeg image reader modules. -Thank to Bingchang, Liu for reports. - - -Version 9b 17-Jan-2016 ------------------------ - -Improvements and optimizations in DCT and color calculations. -Normalize range limit array composition and access pattern. -Thank to Sia Furler and Maddie Ziegler for inspiration. - -Use merged upsample with scaled DCT sizes larger than 8. -Thank to Taylor Hatala for inspiration. - -Check for excessive comment lengths in argument parsing in wrjpgcom.c. -Thank to Julian Cohen for hint. - -Add makefile.b32 for use with Borland C++ 32-bit (bcc32). -Thank to Joe Slater for contribution. - -Document 'f' specifier for jpegtran -crop specification. -Thank to Michele Martone for suggestion. - -Use defined value from header instead of hardwired number in rdswitch.c. -Thank to Robert Sprowson for hint. - - -Version 9a 19-Jan-2014 ------------------------ - -Add support for wide gamut color spaces (JFIF version 2). -Improve clarity and accuracy in color conversion modules. -Note: Requires rebuild of test images. - -Extend the bit depth support to all values from 8 to 12 -(BITS_IN_JSAMPLE configuration option in jmorecfg.h). -jpegtran now supports N bits sample data precision with all N from 8 to 12 -in a single instance. Thank to Roland Fassauer for inspiration. - -Try to resolve issues with new boolean type definition. -Thank also to v4hn for suggestion. - -Enable option to use default Huffman tables for lossless compression -(for hardware solution), and in this case improve lossless RGB compression -with reversible color transform. Thank to Benny Alexandar for hint. - -Extend the entropy decoding structure, so that extraneous bytes between -compressed scan data and following marker can be reported correctly. -Thank to Nigel Tao for hint. - -Add jpegtran -wipe option and extension for -crop. -Thank to Andrew Senior, David Clunie, and Josef Schmid for suggestion. - - -Version 9 13-Jan-2013 ----------------------- - -Add cjpeg -rgb1 option to create an RGB JPEG file, and insert -a simple reversible color transform into the processing which -significantly improves the compression. -The recommended command for lossless coding of RGB images is now -cjpeg -rgb1 -block 1 -arithmetic. -As said, this option improves the compression significantly, but -the files are not compatible with JPEG decoders prior to IJG v9 -due to the included color transform. -The used color transform and marker signaling is compatible with -other JPEG standards (e.g., JPEG-LS part 2). - -Remove the automatic de-ANSI-fication support (Automake 1.12). -Thank also to Nitin A Kamble for suggestion. - -Add remark for jpeg_mem_dest() in jdatadst.c. -Thank to Elie-Gregoire Khoury for the hint. - -Support files with invalid component identifiers (created -by Adobe PDF). Thank to Robin Watts for the suggestion. - -Adapt full buffer case in jcmainct.c for use with scaled DCT. -Thank to Sergii Biloshytskyi for the suggestion. - -Add type identifier for declaration of noreturn functions. -Thank to Brett L. Moore for the suggestion. - -Correct argument type in format string, avoid compiler warnings. -Thank to Vincent Torri for hint. - -Add missing #include directives in configuration checks, avoid -configuration errors. Thank to John Spencer for the hint. - - -Version 8d 15-Jan-2012 ------------------------ - -Add cjpeg -rgb option to create RGB JPEG files. -Using this switch suppresses the conversion from RGB -colorspace input to the default YCbCr JPEG colorspace. -This feature allows true lossless JPEG coding of RGB color images. -The recommended command for this purpose is currently -cjpeg -rgb -block 1 -arithmetic. -SmartScale capable decoder (introduced with IJG JPEG 8) required. -Thank to Michael Koch for the initial suggestion. - -Add option to disable the region adjustment in the transupp crop code. -Thank to Jeffrey Friedl for the suggestion. - -Thank to Richard Jones and Edd Dawson for various minor corrections. - -Thank to Akim Demaille for configure.ac cleanup. - - -Version 8c 16-Jan-2011 ------------------------ - -Add option to compression library and cjpeg (-block N) to use -different DCT block size. -All N from 1 to 16 are possible. Default is 8 (baseline format). -Larger values produce higher compression, -smaller values produce higher quality. -SmartScale capable decoder (introduced with IJG JPEG 8) required. - - -Version 8b 16-May-2010 ------------------------ - -Repair problem in new memory source manager with corrupt JPEG data. -Thank to Ted Campbell and Samuel Chun for the report. - -Repair problem in Makefile.am test target. -Thank to anonymous user for the report. - -Support MinGW installation with automatic configure. -Thank to Volker Grabsch for the suggestion. - - -Version 8a 28-Feb-2010 ------------------------ - -Writing tables-only datastreams via jpeg_write_tables works again. - -Support 32-bit BMPs (RGB image with Alpha channel) for read in cjpeg. -Thank to Brett Blackham for the suggestion. - -Improve accuracy in floating point IDCT calculation. -Thank to Robert Hooke for the hint. - - -Version 8 10-Jan-2010 ----------------------- - -jpegtran now supports the same -scale option as djpeg for "lossless" resize. -An implementation of the JPEG SmartScale extension is required for this -feature. A (draft) specification of the JPEG SmartScale extension is -available as a contributed document at ITU and ISO. Revision 2 or later -of the document is required (latest document version is Revision 3). -The SmartScale extension will enable more features beside lossless resize -in future implementations, as described in the document (new compression -options). - -Add sanity check in BMP reader module to avoid cjpeg crash for empty input -image (thank to Isaev Ildar of ISP RAS, Moscow, RU for reporting this error). - -Add data source and destination managers for read from and write to -memory buffers. New API functions jpeg_mem_src and jpeg_mem_dest. -Thank to Roberto Boni from Italy for the suggestion. - - -Version 7 27-Jun-2009 ----------------------- - -New scaled DCTs implemented. -djpeg now supports scalings N/8 with all N from 1 to 16. -cjpeg now supports scalings 8/N with all N from 1 to 16. -Scaled DCTs with size larger than 8 are now also used for resolving the -common 2x2 chroma subsampling case without additional spatial resampling. -Separate spatial resampling for those kind of files is now only necessary -for N>8 scaling cases. -Furthermore, separate scaled DCT functions are provided for direct resolving -of the common asymmetric subsampling cases (2x1 and 1x2) without additional -spatial resampling. - -cjpeg -quality option has been extended for support of separate quality -settings for luminance and chrominance (or in general, for every provided -quantization table slot). -New API function jpeg_default_qtables() and q_scale_factor array in library. - -Added -nosmooth option to cjpeg, complementary to djpeg. -New variable "do_fancy_downsampling" in library, complement to fancy -upsampling. Fancy upsampling now uses direct DCT scaling with sizes -larger than 8. The old method is not reversible and has been removed. - -Support arithmetic entropy encoding and decoding. -Added files jaricom.c, jcarith.c, jdarith.c. - -Straighten the file structure: -Removed files jidctred.c, jcphuff.c, jchuff.h, jdphuff.c, jdhuff.h. - -jpegtran has a new "lossless" cropping feature. - -Implement -perfect option in jpegtran, new API function -jtransform_perfect_transform() in transupp. (DP 204_perfect.dpatch) - -Better error messages for jpegtran fopen failure. -(DP 203_jpegtran_errmsg.dpatch) - -Fix byte order issue with 16bit PPM/PGM files in rdppm.c/wrppm.c: -according to Netpbm, the de facto standard implementation of the PNM formats, -the most significant byte is first. (DP 203_rdppm.dpatch) - -Add -raw option to rdjpgcom not to mangle the output. -(DP 205_rdjpgcom_raw.dpatch) - -Make rdjpgcom locale aware. (DP 201_rdjpgcom_locale.dpatch) - -Add extern "C" to jpeglib.h. -This avoids the need to put extern "C" { ... } around #include "jpeglib.h" -in your C++ application. Defining the symbol DONT_USE_EXTERN_C in the -configuration prevents this. (DP 202_jpeglib.h_c++.dpatch) - - -Version 6b 27-Mar-1998 ------------------------ - -jpegtran has new features for lossless image transformations (rotation -and flipping) as well as "lossless" reduction to grayscale. - -jpegtran now copies comments by default; it has a -copy switch to enable -copying all APPn blocks as well, or to suppress comments. (Formerly it -always suppressed comments and APPn blocks.) jpegtran now also preserves -JFIF version and resolution information. - -New decompressor library feature: COM and APPn markers found in the input -file can be saved in memory for later use by the application. (Before, -you had to code this up yourself with a custom marker processor.) - -There is an unused field "void * client_data" now in compress and decompress -parameter structs; this may be useful in some applications. - -JFIF version number information is now saved by the decoder and accepted by -the encoder. jpegtran uses this to copy the source file's version number, -to ensure "jpegtran -copy all" won't create bogus files that contain JFXX -extensions but claim to be version 1.01. Applications that generate their -own JFXX extension markers also (finally) have a supported way to cause the -encoder to emit JFIF version number 1.02. - -djpeg's trace mode reports JFIF 1.02 thumbnail images as such, rather -than as unknown APP0 markers. - -In -verbose mode, djpeg and rdjpgcom will try to print the contents of -APP12 markers as text. Some digital cameras store useful text information -in APP12 markers. - -Handling of truncated data streams is more robust: blocks beyond the one in -which the error occurs will be output as uniform gray, or left unchanged -if decoding a progressive JPEG. The appearance no longer depends on the -Huffman tables being used. - -Huffman tables are checked for validity much more carefully than before. - -To avoid the Unisys LZW patent, djpeg's GIF output capability has been -changed to produce "uncompressed GIFs", and cjpeg's GIF input capability -has been removed altogether. We're not happy about it either, but there -seems to be no good alternative. - -The configure script now supports building libjpeg as a shared library -on many flavors of Unix (all the ones that GNU libtool knows how to -build shared libraries for). Use "./configure --enable-shared" to -try this out. - -New jconfig file and makefiles for Microsoft Visual C++ and Developer Studio. -Also, a jconfig file and a build script for Metrowerks CodeWarrior -on Apple Macintosh. makefile.dj has been updated for DJGPP v2, and there -are miscellaneous other minor improvements in the makefiles. - -jmemmac.c now knows how to create temporary files following Mac System 7 -conventions. - -djpeg's -map switch is now able to read raw-format PPM files reliably. - -cjpeg -progressive -restart no longer generates any unnecessary DRI markers. - -Multiple calls to jpeg_simple_progression for a single JPEG object -no longer leak memory. - - -Version 6a 7-Feb-96 --------------------- - -Library initialization sequence modified to detect version mismatches -and struct field packing mismatches between library and calling application. -This change requires applications to be recompiled, but does not require -any application source code change. - -All routine declarations changed to the style "GLOBAL(type) name ...", -that is, GLOBAL, LOCAL, METHODDEF, EXTERN are now macros taking the -routine's return type as an argument. This makes it possible to add -Microsoft-style linkage keywords to all the routines by changing just -these macros. Note that any application code that was using these macros -will have to be changed. - -DCT coefficient quantization tables are now stored in normal array order -rather than zigzag order. Application code that calls jpeg_add_quant_table, -or otherwise manipulates quantization tables directly, will need to be -changed. If you need to make such code work with either older or newer -versions of the library, a test like "#if JPEG_LIB_VERSION >= 61" is -recommended. - -djpeg's trace capability now dumps DQT tables in natural order, not zigzag -order. This allows the trace output to be made into a "-qtables" file -more easily. - -New system-dependent memory manager module for use on Apple Macintosh. - -Fix bug in cjpeg's -smooth option: last one or two scanlines would be -duplicates of the prior line unless the image height mod 16 was 1 or 2. - -Repair minor problems in VMS, BCC, MC6 makefiles. - -New configure script based on latest GNU Autoconf. - -Correct the list of include files needed by MetroWerks C for ccommand(). - -Numerous small documentation updates. - - -Version 6 2-Aug-95 -------------------- - -Progressive JPEG support: library can read and write full progressive JPEG -files. A "buffered image" mode supports incremental decoding for on-the-fly -display of progressive images. Simply recompiling an existing IJG-v5-based -decoder with v6 should allow it to read progressive files, though of course -without any special progressive display. - -New "jpegtran" application performs lossless transcoding between different -JPEG formats; primarily, it can be used to convert baseline to progressive -JPEG and vice versa. In support of jpegtran, the library now allows lossless -reading and writing of JPEG files as DCT coefficient arrays. This ability -may be of use in other applications. - -Notes for programmers: -* We changed jpeg_start_decompress() to be able to suspend; this makes all -decoding modes available to suspending-input applications. However, -existing applications that use suspending input will need to be changed -to check the return value from jpeg_start_decompress(). You don't need to -do anything if you don't use a suspending data source. -* We changed the interface to the virtual array routines: access_virt_array -routines now take a count of the number of rows to access this time. The -last parameter to request_virt_array routines is now interpreted as the -maximum number of rows that may be accessed at once, but not necessarily -the height of every access. - - -Version 5b 15-Mar-95 ---------------------- - -Correct bugs with grayscale images having v_samp_factor > 1. - -jpeg_write_raw_data() now supports output suspension. - -Correct bugs in "configure" script for case of compiling in -a directory other than the one containing the source files. - -Repair bug in jquant1.c: sometimes didn't use as many colors as it could. - -Borland C makefile and jconfig file work under either MS-DOS or OS/2. - -Miscellaneous improvements to documentation. - - -Version 5a 7-Dec-94 --------------------- - -Changed color conversion roundoff behavior so that grayscale values are -represented exactly. (This causes test image files to change.) - -Make ordered dither use 16x16 instead of 4x4 pattern for a small quality -improvement. - -New configure script based on latest GNU Autoconf. -Fix configure script to handle CFLAGS correctly. -Rename *.auto files to *.cfg, so that configure script still works if -file names have been truncated for DOS. - -Fix bug in rdbmp.c: didn't allow for extra data between header and image. - -Modify rdppm.c/wrppm.c to handle 2-byte raw PPM/PGM formats for 12-bit data. - -Fix several bugs in rdrle.c. - -NEED_SHORT_EXTERNAL_NAMES option was broken. - -Revise jerror.h/jerror.c for more flexibility in message table. - -Repair oversight in jmemname.c NO_MKTEMP case: file could be there -but unreadable. - - -Version 5 24-Sep-94 --------------------- - -Version 5 represents a nearly complete redesign and rewrite of the IJG -software. Major user-visible changes include: - * Automatic configuration simplifies installation for most Unix systems. - * A range of speed vs. image quality tradeoffs are supported. - This includes resizing of an image during decompression: scaling down - by a factor of 1/2, 1/4, or 1/8 is handled very efficiently. - * New programs rdjpgcom and wrjpgcom allow insertion and extraction - of text comments in a JPEG file. - -The application programmer's interface to the library has changed completely. -Notable improvements include: - * We have eliminated the use of callback routines for handling the - uncompressed image data. The application now sees the library as a - set of routines that it calls to read or write image data on a - scanline-by-scanline basis. - * The application image data is represented in a conventional interleaved- - pixel format, rather than as a separate array for each color channel. - This can save a copying step in many programs. - * The handling of compressed data has been cleaned up: the application can - supply routines to source or sink the compressed data. It is possible to - suspend processing on source/sink buffer overrun, although this is not - supported in all operating modes. - * All static state has been eliminated from the library, so that multiple - instances of compression or decompression can be active concurrently. - * JPEG abbreviated datastream formats are supported, ie, quantization and - Huffman tables can be stored separately from the image data. - * And not only that, but the documentation of the library has improved - considerably! - - -The last widely used release before the version 5 rewrite was version 4A of -18-Feb-93. Change logs before that point have been discarded, since they -are not of much interest after the rewrite. diff --git a/gzip.vp b/gzip.vp new file mode 100644 index 0000000..ab50d04 --- /dev/null +++ b/gzip.vp @@ -0,0 +1,663 @@ +# -*- coding: utf-8 -*- +# ============================================================ +# gzip - 单文件 gzip 解码器(RFC 1952 + RFC 1951 deflate) +# +# OOP 范式对齐 zipfile: 数据由 GzipFile 对象持有并负责释放; +# 内部位流/Huffman 助手为模块级私有函数。 +# +# 用法: +# gz: gzip.GzipFile | t.CPtr = gzip.GzipFile("train-images-idx3-ubyte.gz") +# if gz is None or gz.ok == 0: ... +# buf: bytes = gz.read_all() # 内部 malloc, read_all 返回即解压完成 +# n: t.CSizeT = gz.dst_len # 原始长度(由对象持有, close 时释放) +# gz.close() +# +# 覆盖 stored/fixed/dynamic 三种 deflate 块; 不解 CRC。 +# ============================================================ +import t, c +import stdlib +import string +import w32.fileio as fileio +from stdint import * + + +# ============================================================ +# GzipFile +# ============================================================ +class GzipFile: + ok: t.CInt # 1 = 可正常使用 + err: t.CInt # 错误码(0 无, <0 出错) + src: BYTEPTR # 整个 .gz 文件的内存副本(构造时读入, close 释放) + src_len: t.CSizeT + dst: BYTEPTR # 解压输出缓冲(read_all 生成, close 释放) + dst_len: t.CSizeT # 解压结果长度 + _need_close_src: t.CInt # 是否持有 src(读失败时为 0) + + def __init__(self, path: str): + self.ok = 0 + self.err = 0 + self.src = None + self.src_len = 0 + self.dst = None + self.dst_len = 0 + self._need_close_src = 0 + if path is None: + self.err = -1 + return + f: fileio.File | t.CPtr = fileio.File(path, fileio.MODE.R) + if f.closed: + self.err = -2 # 打不开文件 + return + sz: LONGLONG = f.size() + if sz < 18: + f.close() + self.err = -3 # 文件过小 + return + self.src_len = t.CSizeT(sz) + # read_all 契约: 最多读 max_count-1 字节并补 NUL; 故按 src_len+1 申请与请求 + buf: BYTEPTR = stdlib.malloc(self.src_len + 1) + if buf is None: + f.close() + self.err = -4 + return + got: LONG = f.read_all(buf, ULONG(self.src_len + 1)) + f.close() + if got <= 0 or t.CSizeT(got) != self.src_len: + stdlib.free(buf) + self.err = -5 # 读取失败或被截断 + return + # gzip 魔数校验 + ub: t.CUnsignedChar | t.CPtr = buf + if ub[0] != 0x1F or ub[1] != 0x8B or ub[2] != 8: + stdlib.free(buf) + self.err = -6 + return + self.src = buf + self._need_close_src = 1 + self.ok = 1 + + def read_all(self) -> BYTEPTR: + """解压整个成员。返回内部持有的输出缓冲(或 None); 长度见 self.dst_len。""" + if self.ok == 0 or self.src is None: + return None + if self.dst is not None: + return self.dst # 已解压过 + want: t.CUInt32T = gzip_isize(self.src, self.src_len) + cap: t.CSizeT = t.CSizeT(want) + if cap == 0: + cap = self.src_len * 64 # 无 ISIZE 时保守初值 + if cap < 1024: + cap = 1024 + # 保险起见: ISIZE 与实际不符时报"目标缓冲不足", 就翻倍重试(正常情况下一次成功) + out: BYTEPTR = None + n: LONG = 0 + while True: + out = stdlib.malloc(cap) + if out is None: + self.err = -7 + return None + n = _gzip_decode(self.src, self.src_len, out, cap) + if n == -9 or n == -18 or n == -23: + stdlib.free(out) + out = None + if cap >= 1073741824: + self.err = t.CInt(n) + return None + cap = cap * 2 + continue + break + if n <= 0: + stdlib.free(out) + self.err = t.CInt(n) # n 为负错误码, 原样保存 + return None + self.dst = out + self.dst_len = t.CSizeT(n) + return out + + def close(self): + if self._need_close_src != 0 and self.src is not None: + stdlib.free(self.src) + if self.dst is not None: + stdlib.free(self.dst) + self.src = None + self.dst = None + self.src_len = 0 + self.dst_len = 0 + self._need_close_src = 0 + self.ok = 0 + + +# ============================================================ +# 公共访问器(跨模块读字段用函数, 避免 stub 字段访问陷阱) +# ============================================================ +def gzip_open(path: str) -> GzipFile | t.CPtr: + return GzipFile(path) + + +def gzip_ok(gz: GzipFile | t.CPtr) -> t.CInt: + if gz is None: + return 0 + return gz.ok + + +def gzip_err(gz: GzipFile | t.CPtr) -> t.CInt: + if gz is None: + return -1 + return gz.err + + +def gzip_dst_len(gz: GzipFile | t.CPtr) -> t.CSizeT: + if gz is None: + return 0 + return gz.dst_len + + +def gzip_read(gz: GzipFile | t.CPtr) -> BYTEPTR: + if gz is None: + return None + return gz.read_all() + + +def gzip_close(gz: GzipFile | t.CPtr): + if gz is not None: + gz.close() + + +def gzip_decode_bytes(src: t.CPtr, src_len: t.CSizeT, + dst: t.CPtr, dst_cap: t.CSizeT) -> LONG: + """原始缓冲到缓冲的解压(不读文件), 供下载流程直接使用""" + return _gzip_decode(src, src_len, dst, dst_cap) + + +# ============================================================ +# 位流状态(模块级, 单线程; 每次 _gzip_decode 重置) +# ============================================================ +_g_bits: t.CUInt64T = 0 +_g_bitcnt: t.CInt = 0 +_g_pos: t.CSizeT = 0 +_g_src: t.CPtr = None +_g_src_len: t.CSizeT = 0 + + +def gzip_isize(src: t.CPtr, src_len: t.CSizeT) -> t.CUInt32T: + """gzip 尾部 ISIZE(小端 u32), 即原始长度提示""" + if src is None or src_len < 4: + return 0 + ub: t.CUnsignedChar | t.CPtr = src + p: t.CSizeT = src_len - 4 + # 小端 u32 拼装(64 位中间量后收窄) + v64: t.CUInt64T = t.CUInt64T(ub[p]) \ + + t.CUInt64T(ub[p + 1]) * 256 \ + + t.CUInt64T(ub[p + 2]) * 65536 \ + + t.CUInt64T(ub[p + 3]) * 16777216 + return t.CUInt32T(v64) + + +def _reset(src: t.CPtr, src_len: t.CSizeT): + global _g_bits, _g_bitcnt, _g_pos, _g_src, _g_src_len + _g_bits = 0 + _g_bitcnt = 0 + _g_pos = 0 + _g_src = src + _g_src_len = src_len + + +def _fill(n: t.CInt) -> t.CInt: + global _g_bits, _g_bitcnt, _g_pos, _g_src, _g_src_len + sb: t.CUnsignedChar | t.CPtr = _g_src + while _g_bitcnt < n: + if _g_pos >= _g_src_len: + return -1 + _g_bits = _g_bits | (t.CUInt64T(sb[_g_pos]) << _g_bitcnt) + _g_pos += 1 + _g_bitcnt += 8 + return 0 + + +def _bits(n: t.CInt) -> t.CInt: + global _g_bits, _g_bitcnt + if _fill(n) != 0: + return -1 + v: t.CUInt64T = _g_bits & ((t.CUInt64T(1) << n) - 1) + _g_bits = _g_bits >> n + _g_bitcnt -= n + return t.CInt(v) + + +def _byte() -> t.CInt: + return _bits(8) + + +def _align(): + global _g_bits, _g_bitcnt + _g_bits = 0 + _g_bitcnt = 0 + + +def _decode(count: t.CInt | t.CPtr, first: t.CInt | t.CPtr, + off: t.CInt | t.CPtr, sorted: t.CInt | t.CPtr) -> t.CInt: + code: t.CInt = 0 + ln: t.CInt = 1 + while ln <= 15: + b: t.CInt = _bits(1) + if b < 0: + return -1 + code = (code << 1) | b + if count[ln] != 0: + d: t.CInt = code - first[ln] + if d >= 0 and d < count[ln]: + return sorted[off[ln] + d] + ln += 1 + return -1 + + +def _build(first: t.CInt | t.CPtr, off: t.CInt | t.CPtr, + count: t.CInt | t.CPtr) -> t.CInt: + anylen: t.CInt = 0 + l1: t.CInt = 1 + while l1 <= 15: + if count[l1] != 0: + anylen = 1 + l1 += 1 + if anylen == 0: + return -1 + left: t.CInt = 1 + l2: t.CInt = 1 + while l2 <= 15: + left = left << 1 + if count[l2] > left: + return -1 + left = left - count[l2] + l2 += 1 + code: t.CInt = 0 + l3: t.CInt = 1 + while l3 <= 15: + code = (code + count[l3 - 1]) << 1 + first[l3] = code + l3 += 1 + offsum: t.CInt = 0 + l4: t.CInt = 1 + while l4 <= 15: + off[l4] = offsum + offsum += count[l4] + l4 += 1 + return 0 + + +def _collect(len_arr: t.CInt | t.CPtr, cnt: t.CInt | t.CPtr, + sorted_arr: t.CInt | t.CPtr, nsym: t.CInt, + pos: t.CInt | t.CPtr): + string.memset(cnt, 0, 17 * 4) + s1: t.CInt = 0 + while s1 < nsym: + if len_arr[s1] != 0: + cnt[len_arr[s1]] += 1 + s1 += 1 + run: t.CInt = 0 + l5: t.CInt = 1 + while l5 <= 15: + pos[l5] = run + run += cnt[l5] + l5 += 1 + s2: t.CInt = 0 + while s2 < nsym: + if len_arr[s2] != 0: + sorted_arr[pos[len_arr[s2]]] = s2 + pos[len_arr[s2]] += 1 + s2 += 1 + + +def _len_extra(code: t.CInt) -> t.CInt: + if code == 285: + return 0 + if code >= 281: + return 5 + if code >= 277: + return 4 + if code >= 273: + return 3 + if code >= 269: + return 2 + if code >= 265: + return 1 + return 0 + + +def _len_base(code: t.CInt) -> t.CInt: + if code == 285: + return 258 + if code >= 281: + return 131 + ((code - 281) << 5) + if code >= 277: + return 67 + ((code - 277) << 4) + if code >= 273: + return 35 + ((code - 273) << 3) + if code >= 269: + return 19 + ((code - 269) << 2) + if code >= 265: + return 11 + ((code - 265) << 1) + return 3 + (code - 257) + + +def _dist_extra(code: t.CInt) -> t.CInt: + if code < 4: + return 0 + return (code - 4) // 2 + 1 + + +def _dist_base(code: t.CInt) -> t.CInt: + if code < 4: + return code + 1 + e: t.CInt = (code - 4) // 2 + 1 + f: t.CInt = (1 << (e + 1)) + 1 + if code % 2 == 0: + return f + return f + (1 << e) + + +def _cl_order_set(arr: t.CInt | t.CPtr, idx: t.CInt, val: t.CInt): + arr[idx] = val + + +# ============================================================ +# 核心解压(模块级私有) +# ============================================================ +def _gzip_decode(src: t.CPtr, src_len: t.CSizeT, + dst: t.CPtr, dst_cap: t.CSizeT) -> LONG: + if src is None or dst is None or src_len < 18 or dst_cap == 0: + return -1 + ub: t.CUnsignedChar | t.CPtr = src + if ub[0] != 0x1F or ub[1] != 0x8B or ub[2] != 8: + return -2 + _reset(src, src_len) + flg: t.CInt = t.CInt(ub[3]) + hdr_len: t.CSizeT = 10 + if flg & 0x04 != 0: + xlen: t.CSizeT = t.CSizeT(ub[hdr_len]) | (t.CSizeT(ub[hdr_len + 1]) << 8) + hdr_len += 2 + xlen + if flg & 0x08 != 0: + while hdr_len < src_len and ub[hdr_len] != 0: + hdr_len += 1 + hdr_len += 1 + if flg & 0x10 != 0: + while hdr_len < src_len and ub[hdr_len] != 0: + hdr_len += 1 + hdr_len += 1 + if flg & 0x02 != 0: + hdr_len += 2 + if hdr_len >= src_len: + return -3 + global _g_pos + _g_pos = hdr_len + + dout: t.CUnsignedChar | t.CPtr = dst + wpos: t.CSizeT = 0 + + lit_len: t.CInt | t.CPtr = stdlib.malloc(288 * 4) + lit_cnt: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + lit_first: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + lit_off: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + lit_sorted: t.CInt | t.CPtr = stdlib.malloc(288 * 4) + dst_len: t.CInt | t.CPtr = stdlib.malloc(32 * 4) + dst_cnt: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + dst_first: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + dst_off: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + dst_sorted: t.CInt | t.CPtr = stdlib.malloc(32 * 4) + cl_len: t.CInt | t.CPtr = stdlib.malloc(19 * 4) + cl_order: t.CInt | t.CPtr = stdlib.malloc(19 * 4) + cl_cnt: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + cl_first: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + cl_off: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + cl_sorted: t.CInt | t.CPtr = stdlib.malloc(19 * 4) + pos_tmp: t.CInt | t.CPtr = stdlib.malloc(17 * 4) + if lit_len is None or lit_cnt is None or lit_first is None \ + or lit_off is None or lit_sorted is None or dst_len is None \ + or dst_cnt is None or dst_first is None or dst_off is None \ + or dst_sorted is None or cl_len is None or cl_order is None \ + or cl_cnt is None or cl_first is None or cl_off is None \ + or cl_sorted is None or pos_tmp is None: + return -4 + + # 全部表缓冲清零(初始 malloc 未清零; 防个别路径读到垃圾表导致提前/错乱收尾) + string.memset(lit_len, 0, 288 * 4) + string.memset(lit_cnt, 0, 17 * 4) + string.memset(lit_first, 0, 17 * 4) + string.memset(lit_off, 0, 17 * 4) + string.memset(lit_sorted, 0, 288 * 4) + string.memset(dst_len, 0, 32 * 4) + string.memset(dst_cnt, 0, 17 * 4) + string.memset(dst_first, 0, 17 * 4) + string.memset(dst_off, 0, 17 * 4) + string.memset(dst_sorted, 0, 32 * 4) + string.memset(cl_len, 0, 19 * 4) + string.memset(cl_order, 0, 19 * 4) + string.memset(cl_cnt, 0, 17 * 4) + string.memset(cl_first, 0, 17 * 4) + string.memset(cl_off, 0, 17 * 4) + string.memset(cl_sorted, 0, 19 * 4) + string.memset(pos_tmp, 0, 17 * 4) + + _cl_order_set(cl_order, 0, 16) + _cl_order_set(cl_order, 1, 17) + _cl_order_set(cl_order, 2, 18) + _cl_order_set(cl_order, 3, 0) + _cl_order_set(cl_order, 4, 8) + _cl_order_set(cl_order, 5, 7) + _cl_order_set(cl_order, 6, 9) + _cl_order_set(cl_order, 7, 6) + _cl_order_set(cl_order, 8, 10) + _cl_order_set(cl_order, 9, 5) + _cl_order_set(cl_order, 10, 11) + _cl_order_set(cl_order, 11, 4) + _cl_order_set(cl_order, 12, 12) + _cl_order_set(cl_order, 13, 3) + _cl_order_set(cl_order, 14, 13) + _cl_order_set(cl_order, 15, 2) + _cl_order_set(cl_order, 16, 14) + _cl_order_set(cl_order, 17, 1) + _cl_order_set(cl_order, 18, 15) + + err: t.CInt = 0 + final: t.CInt = 0 + + while final == 0 and err == 0: + bfinal: t.CInt = _bits(1) + btype: t.CInt = _bits(2) + if bfinal < 0 or btype < 0: + err = -5 + break + final = bfinal + if btype == 0: + _align() + slen: t.CInt = _bits(16) + snlen: t.CInt = _bits(16) + if slen < 0 or snlen < 0: + err = -6 + break + if (slen ^ snlen) != 0xFFFF: + err = -7 + break + i: t.CSizeT = 0 + while i < t.CSizeT(slen): + ch: t.CInt = _byte() + if ch < 0: + err = -8 + break + if wpos >= dst_cap: + err = -9 + break + dout[wpos] = t.CUnsignedChar(ch) + wpos += 1 + i += 1 + if err != 0: + break + elif btype == 1: + string.memset(lit_cnt, 0, 17 * 4) + j: t.CInt = 0 + while j < 144: + lit_len[j] = 8 + j += 1 + while j < 256: + lit_len[j] = 9 + j += 1 + while j < 280: + lit_len[j] = 7 + j += 1 + while j < 288: + lit_len[j] = 8 + j += 1 + _collect(lit_len, lit_cnt, lit_sorted, 288, pos_tmp) + if _build(lit_first, lit_off, lit_cnt) != 0: + err = -10 + break + else: + hlit: t.CInt = _bits(5) + 257 + hdist: t.CInt = _bits(5) + 1 + hclen: t.CInt = _bits(4) + 4 + if hlit < 0 or hdist < 0 or hclen < 0: + err = -11 + break + string.memset(cl_len, 0, 19 * 4) + k: t.CInt = 0 + while k < hclen: + vv: t.CInt = _bits(3) + if vv < 0: + err = -12 + break + cl_len[cl_order[k]] = vv + k += 1 + if err != 0: + break + _collect(cl_len, cl_cnt, cl_sorted, 19, pos_tmp) + if _build(cl_first, cl_off, cl_cnt) != 0: + err = -13 + break + string.memset(lit_len, 0, 288 * 4) + string.memset(dst_len, 0, 32 * 4) + sym_i: t.CInt = 0 + while sym_i < hlit + hdist: + sym: t.CInt = _decode(cl_cnt, cl_first, cl_off, cl_sorted) + if sym < 0: + err = -14 + break + if sym < 16: + if sym_i < hlit: + lit_len[sym_i] = sym + else: + dst_len[sym_i - hlit] = sym + sym_i += 1 + elif sym == 16: + prev: t.CInt = 0 + if sym_i > 0: + if sym_i - 1 < hlit: + prev = lit_len[sym_i - 1] + else: + prev = dst_len[sym_i - hlit - 1] + rp: t.CInt = _bits(2) + 3 + r2: t.CInt = 0 + while r2 < rp and sym_i < hlit + hdist: + if sym_i < hlit: + lit_len[sym_i] = prev + else: + dst_len[sym_i - hlit] = prev + sym_i += 1 + r2 += 1 + elif sym == 17: + rp2: t.CInt = _bits(3) + 3 + r3: t.CInt = 0 + while r3 < rp2 and sym_i < hlit + hdist: + if sym_i < hlit: + lit_len[sym_i] = 0 + else: + dst_len[sym_i - hlit] = 0 + sym_i += 1 + r3 += 1 + else: + rp3: t.CInt = _bits(7) + 11 + r4: t.CInt = 0 + while r4 < rp3 and sym_i < hlit + hdist: + if sym_i < hlit: + lit_len[sym_i] = 0 + else: + dst_len[sym_i - hlit] = 0 + sym_i += 1 + r4 += 1 + if err != 0: + break + _collect(lit_len, lit_cnt, lit_sorted, hlit, pos_tmp) + if _build(lit_first, lit_off, lit_cnt) != 0: + err = -15 + break + _collect(dst_len, dst_cnt, dst_sorted, hdist, pos_tmp) + if _build(dst_first, dst_off, dst_cnt) != 0: + err = -16 + break + if err != 0: + break + + run: t.CInt = 1 + while run == 1 and err == 0: + sym: t.CInt = _decode(lit_cnt, lit_first, lit_off, lit_sorted) + if sym < 0: + err = -17 + break + if sym < 256: + if wpos >= dst_cap: + err = -18 + break + dout[wpos] = t.CUnsignedChar(sym) + wpos += 1 + elif sym == 256: + run = 0 + else: + le: t.CInt = _len_extra(sym) + ev: t.CInt = _bits(le) + if ev < 0: + err = -19 + break + length: t.CSizeT = t.CSizeT(_len_base(sym) + ev) + ds: t.CInt = _decode(dst_cnt, dst_first, dst_off, dst_sorted) + if ds < 0: + err = -20 + break + de: t.CInt = _dist_extra(ds) + dv: t.CInt = _bits(de) + if dv < 0: + err = -21 + break + dist: t.CSizeT = t.CSizeT(_dist_base(ds) + dv) + if dist == 0 or dist > wpos: + err = -22 + break + ci: t.CSizeT = 0 + while ci < length: + if wpos >= dst_cap: + err = -23 + break + dout[wpos] = dout[wpos - dist] + wpos += 1 + ci += 1 + if err != 0: + break + + stdlib.free(lit_len) + stdlib.free(lit_cnt) + stdlib.free(lit_first) + stdlib.free(lit_off) + stdlib.free(lit_sorted) + stdlib.free(dst_len) + stdlib.free(dst_cnt) + stdlib.free(dst_first) + stdlib.free(dst_off) + stdlib.free(dst_sorted) + stdlib.free(cl_len) + stdlib.free(cl_order) + stdlib.free(cl_cnt) + stdlib.free(cl_first) + stdlib.free(cl_off) + stdlib.free(cl_sorted) + stdlib.free(pos_tmp) + + if err != 0: + return LONG(err) # err 本身为负错误码, 不能再取负(否则被上层当成"长度>0 成功") + return LONG(wpos) diff --git a/os/_win32.vp b/os/_win32.vp index 6292e53..a322062 100644 --- a/os/_win32.vp +++ b/os/_win32.vp @@ -104,9 +104,22 @@ def chdir(path: str) -> int: # ============================================================================= def mkdir(path: str) -> int: - """创建目录。成功返回 0,失败返回 -1。""" - result: BOOL = w32.win32file.CreateDirectoryA(path, t.CPtr(0)) - if result == 0: + """创建目录。成功返回 0,失败返回 -1。 + 路径按 UTF-8 解释时转 UTF-16 走 CreateDirectoryW, 否则退回 A 系列""" + if w32.win32base.utf8_is_valid(path) == 0: + result: BOOL = w32.win32file.CreateDirectoryA(path, t.CPtr(0)) + if result == 0: + return -1 + return 0 + wp: w32.win32base.WCHARPTR = w32.win32base.utf8_to_utf16_dup(path) + if wp is None: + result_a: BOOL = w32.win32file.CreateDirectoryA(path, t.CPtr(0)) + if result_a == 0: + return -1 + return 0 + result_w: BOOL = w32.win32file.CreateDirectoryW(t.CPtr(wp), t.CPtr(0)) + stdlib.free(wp) + if result_w == 0: return -1 return 0 diff --git a/requests/https.vp b/requests/https.vp index 9a3ecc5..5dbd319 100644 --- a/requests/https.vp +++ b/requests/https.vp @@ -431,8 +431,9 @@ def _HttpsRequest(method: str, url: str, verify: bool, ca_file: str, data: str, requests._mbuddy.free(req_buf) - # ---- 接收响应 ---- - resp_buf: str = requests._mbuddy.alloc(requests.HTTP_BUFSIZ) + # ---- 接收响应(缓冲自动翻倍增长, 支持 MB 级正文, 如 MNIST .gz 下载) ---- + resp_cap: INT = requests.HTTP_BUFSIZ + resp_buf: str = requests._mbuddy.alloc(t.CSizeT(resp_cap)) if resp_buf is None: _RecordErrorText("alloc response buffer failed") openssl.SSL_free(ssl) @@ -443,9 +444,28 @@ def _HttpsRequest(method: str, url: str, verify: bool, ca_file: str, data: str, return None total: INT = 0 - while total < requests.HTTP_BUFSIZ - 1: - n: INT = openssl.SSL_read(ssl, resp_buf + total, requests.HTTP_BUFSIZ - 1 - total) - if n <= 0: break + while True: + room: INT = resp_cap - 1 - total + if room <= 0: + # 缓冲已满仍可能有数据: 翻倍扩容(硬上限 256MB, 防失控流) + if resp_cap >= 268435456: + break + resp_cap = resp_cap * 2 + nb: t.CVoid | t.CPtr = requests._mbuddy.realloc(resp_buf, t.CSizeT(resp_cap)) + if nb is None: + _RecordErrorText("realloc response buffer failed") + requests._mbuddy.free(resp_buf) + openssl.SSL_free(ssl) + openssl.SSL_CTX_free(ctx) + sock.close() + requests._mbuddy.free(errmsg) + requests.release(resp) + return None + resp_buf = t.CChar(t.CUInt64T(nb), t.CPtr) + room = resp_cap - 1 - total + n: INT = openssl.SSL_read(ssl, resp_buf + total, room) + if n <= 0: + break total += n resp_buf[total] = 0 diff --git a/stdarg.vp b/stdarg.vp index 68586b3..fff5fe5 100644 --- a/stdarg.vp +++ b/stdarg.vp @@ -6,4 +6,4 @@ extern def va_end(args: t.CPtr) va_list: t.CTypedef = t.CUnsignedChar | t.CPtr -extern def arg(type: t.CType) +extern def arg(type: t.CPtr) diff --git a/torch/__init__.vp b/torch/__init__.vp new file mode 100644 index 0000000..0d92b22 --- /dev/null +++ b/torch/__init__.vp @@ -0,0 +1,925 @@ +# -*- coding: utf-8 -*- +# ============================================================ +# torch-lite: 仿 PyTorch 的极简张量/神经网络库(Viper) +# +# 设计原则: +# * "足够用就行": 只覆盖 MNIST 两层 MLP 训练/推理所需的原语。 +# * API 命名贴近 PyTorch: Tensor / linear_fwd(bias 融合) / relu / +# relu_backward / softmax_crossentropy(前向+梯度一体) / sgd_step。 +# * 张量为 2D 稠密 double (rows x cols, 行主序), 权重形状同 PyTorch: +# Linear(in, out) -> weight (out, in) / bias (out,), forward = x @ W^T + b。 +# * 计算热点结合 vipersimd: +# - linear_fwd 内积按 k%4 用 simd_dot4d 累加(纯 AVX2 点积); +# - relu_fwd 用 simd_max4d(x, 0) 四路取大; +# - 向量乘标量用 simd_mul_scalar4d(归一化 /255 等)。 +# backward / softmax / sgd 等按标量写(数据量小, 可读性优先)。 +# * 内存: 所有 Tensor 及 data/grad 都从调用方传入的 memhub.MemBuddy +# 分配(与 includes 既有约定一致, pool.alloc 免手动 free)。 +# ============================================================ +from stdint import * +import t, c +import stdlib +import string +import stdio +import vipermath +import vipersimd +import memhub + + +# ------------------------------------------------------------ +# Tensor +# ------------------------------------------------------------ +@t.NoVTable +class Tensor: + Data: t.CPtr # rows*cols*8 字节连续 double + Grad: t.CPtr # 同尺寸梯度(可为 None) + Rows: sizet + Cols: sizet + +# 字段: Data/Grad/Rows/Cols = 32 字节(无 vtable) +TENSOR_SIZE: t.CDefine = 32 + + +def _zeros_block(pool: rowptr[memhub.MemBuddy], bytes_n: sizet) -> t.CPtr: + """分配并清零一块内存""" + p: t.CPtr = pool.alloc(bytes_n) + if p is not None: + string.memset(p, 0, bytes_n) + return p + + +def zeros(pool: rowptr[memhub.MemBuddy], rows: sizet, + cols: sizet, with_grad: int = 1) -> Tensor | t.CPtr: + """torch.zeros: 创建 rows x cols 的全零 Tensor, 可选分配 Grad 缓冲""" + if pool is None: + return None + t0: rowptr[Tensor] = pool.alloc(TENSOR_SIZE) + if t0 is None: + return None + string.memset(t0, 0, TENSOR_SIZE) + nb: sizet = rows * cols * sizet(8) + t0.Data = _zeros_block(pool, nb) + if t0.Data is None: + return None + if with_grad != 0: + t0.Grad = _zeros_block(pool, nb) + t0.Rows = rows + t0.Cols = cols + return t0 + + +def from_raw(pool: rowptr[memhub.MemBuddy], data: t.CPtr, + rows: sizet, cols: sizet, + with_grad: int = 0) -> Tensor | t.CPtr: + """torch.from_raw: 包装一块已有内存为 Tensor(不复制, 不拥有)""" + if pool is None: + return None + t0: rowptr[Tensor] = pool.alloc(TENSOR_SIZE) + if t0 is None: + return None + string.memset(t0, 0, TENSOR_SIZE) + t0.Data = data + t0.Grad = None + t0.Rows = rows + t0.Cols = cols + return t0 + + +def tensor_free(t0: Tensor | t.CPtr): + """释放 Tensor 外壳(数据由 MemBuddy 池统一回收, 无需手动 free)""" + pass + + +def tensor_data(t0: Tensor | t.CPtr) -> t.CPtr: + if t0 is None: + return None + return t0.Data + + +def tensor_grad(t0: Tensor | t.CPtr) -> t.CPtr: + if t0 is None: + return None + return t0.Grad + + +def tensor_rows(t0: Tensor | t.CPtr) -> sizet: + if t0 is None: + return 0 + return t0.Rows + + +def tensor_cols(t0: Tensor | t.CPtr) -> sizet: + if t0 is None: + return 0 + return t0.Cols + + +def fill(t0: Tensor | t.CPtr, val: double): + """torch.fill: 全部元素置为 val""" + if t0 is None or t0.Data is None: + return + d: double | t.CPtr = t0.Data + n: sizet = t0.Rows * t0.Cols + i: sizet = 0 + while i < n: + d[i] = val + i += 1 + + +def zero_grad(t0: Tensor | t.CPtr): + """torch.zero_grad: Grad 缓冲清零""" + if t0 is None or t0.Grad is None: + return + string.memset(t0.Grad, 0, t0.Rows * t0.Cols * sizet(8)) + + +# ------------------------------------------------------------ +# 确定性伪随机(LCG, minstd)与 Kaiming 均匀初始化 +# ------------------------------------------------------------ +_rng_state: double = 12345.0 +LCG_M: double = 2147483647.0 +LCG_A: double = 48271.0 + + +def manual_seed(seed: int64): + global _rng_state + _rng_state = double(seed % int64(2147483647)) + if _rng_state <= double(0.0): + _rng_state = double(12345.0) + + +def _next_rand() -> double: + """(0,1) 均匀随机""" + global _rng_state + _rng_state = vipermath.fmod(_rng_state * LCG_A, LCG_M) + return _rng_state / LCG_M + + +def uniform_fill(t0: Tensor | t.CPtr, lo: double, hi: double): + """U(lo, hi) 填充""" + if t0 is None or t0.Data is None: + return + d: double | t.CPtr = t0.Data + n: sizet = t0.Rows * t0.Cols + span: double = hi - lo + i: sizet = 0 + while i < n: + d[i] = lo + _next_rand() * span + i += 1 + + +def kaiming_uniform_fill(w: Tensor | t.CPtr, fan_in: sizet, + fan_out: sizet): + """PyTorch Linear 默认初始化: U(-sqrt(6/(fan_in+fan_out)), +...)""" + bound: double = vipermath.sqrt( + double(6.0) / double(fan_in + fan_out)) + uniform_fill(w, -bound, bound) + + +# ------------------------------------------------------------ +# 基础元素级运算(结合 vipersimd 4 宽指令) +# ------------------------------------------------------------ +def vector_mul_scalar4(src: t.CPtr, scalar_ptr: t.CPtr, dst: t.CPtr, + n: sizet): + """dst = src * scalar (逐 4 元素 simd_mul_scalar4d + 标量尾数)""" + if src is None or scalar_ptr is None or dst is None: + return + i: sizet = 0 + while i + 4 <= n: + vipersimd.simd_mul_scalar4d( + t.CPtr(uint64(src) + uint64(i) * 8), + scalar_ptr, + t.CPtr(uint64(dst) + uint64(i) * 8)) + i += 4 + sd: double | t.CPtr = src + dd: double | t.CPtr = dst + # 尾部标量 + sp: double | t.CPtr = scalar_ptr + sval: double = sp[0] + while i < n: + dd[i] = sd[i] * sval + i += 1 + + +def vector_add_inplace(a: t.CPtr, b: t.CPtr, n: sizet): + """a += b (逐 4 元素 simd_add_array 尾部标量)""" + if a is None or b is None: + return + tmp: t.CPtr = stdlib.malloc(8) + if tmp is None: + return + i: sizet = 0 + while i + 4 <= n: + vipersimd.simd_add_array( + t.CPtr(uint64(a) + uint64(i) * 8), + t.CPtr(uint64(b) + uint64(i) * 8), + t.CPtr(uint64(a) + uint64(i) * 8)) + i += 4 + da: double | t.CPtr = a + db: double | t.CPtr = b + while i < n: + da[i] = da[i] + db[i] + i += 1 + stdlib.free(tmp) + + +# ------------------------------------------------------------ +# 线性层: forward = x @ W^T + b (W: out x in, x: m x in, out: m x out) +# 内积按 k%4 用 vipersimd.simd_dot4d —— MNIST 主计算热点 +# ------------------------------------------------------------ +def linear_fwd(w: Tensor | t.CPtr, b: Tensor | t.CPtr, + x: Tensor | t.CPtr, out: Tensor | t.CPtr): + if w is None or b is None or x is None or out is None: + return + m: sizet = x.Rows + k: sizet = x.Cols + n: sizet = w.Rows + wd: double | t.CPtr = w.Data + bd: double | t.CPtr = b.Data + xd: double | t.CPtr = x.Data + od: double | t.CPtr = out.Data + acc_buf: t.CPtr = stdlib.malloc(8) + if acc_buf is None: + return + accp: double | t.CPtr = acc_buf + i: sizet = 0 + while i < m: + j: sizet = 0 + while j < n: + acc: double = bd[j] + kk: sizet = 0 + while kk + 4 <= k: + vipersimd.simd_dot4d( + t.CPtr(uint64(xd) + uint64(i * k + kk) * 8), + t.CPtr(uint64(wd) + uint64(j * k + kk) * 8), + acc_buf) + acc = acc + accp[0] + kk += 4 + while kk < k: + acc = acc + xd[i * k + kk] * wd[j * k + kk] + kk += 1 + od[i * n + j] = acc + j += 1 + i += 1 + stdlib.free(acc_buf) + + +# ------------------------------------------------------------ +# 线性层反向: 输入 w(out,in) x(m,in) g(m,out) +# dw[j,i] += sum_b g[b,j]*x[b,i] ; db[j] += sum_b g[b,j] +# dx[b,i] = sum_j g[b,j]*w[j,i] +# 注意 dw/db 按"累加"语义(调用前清零 Grad) +# ------------------------------------------------------------ +def linear_bwd(w: Tensor | t.CPtr, x: Tensor | t.CPtr, + g: Tensor | t.CPtr, dw: Tensor | t.CPtr, + db: Tensor | t.CPtr, dx: Tensor | t.CPtr, + accum: int = 0): + if w is None or x is None or g is None: + return + if dw is None or db is None or dx is None: + return + m: sizet = x.Rows + k: sizet = x.Cols + n: sizet = w.Rows + wd: double | t.CPtr = w.Data + xd: double | t.CPtr = x.Data + gd: double | t.CPtr = g.Data + dwd: double | t.CPtr = dw.Data + dbd: double | t.CPtr = db.Data + dxd: double | t.CPtr = dx.Data + + # dw/db/dx 清零(dx 恒清零: 它是本样本的输入梯度, 不跨样本累积) + # accum=1 时保留 dw/db 现值 —— 逐样本反向累积整批参数梯度的调用方用 + if accum == 0: + string.memset(dw.Data, 0, n * k * sizet(8)) + string.memset(db.Data, 0, n * sizet(8)) + string.memset(dx.Data, 0, m * k * sizet(8)) + + b: sizet = 0 + while b < m: + j: sizet = 0 + while j < n: + gj: double = gd[b * n + j] + if gj != double(0.0): + dbd[j] = dbd[j] + gj + i: sizet = 0 + while i < k: + dwd[j * k + i] = dwd[j * k + i] + gj * xd[b * k + i] + dxd[b * k + i] = dxd[b * k + i] + gj * wd[j * k + i] + i += 1 + j += 1 + b += 1 + + +# ------------------------------------------------------------ +# ReLU (simd_max4d 版本) 与反向 +# ------------------------------------------------------------ +def relu_fwd(x: Tensor | t.CPtr, out: Tensor | t.CPtr): + if x is None or out is None: + return + n: sizet = x.Rows * x.Cols + zero4: t.CPtr = stdlib.malloc(32) + if zero4 is None: + return + string.memset(zero4, 0, 32) + xd: double | t.CPtr = x.Data + od: double | t.CPtr = out.Data + i: sizet = 0 + while i + 4 <= n: + vipersimd.simd_max4d( + t.CPtr(uint64(xd) + uint64(i) * 8), + zero4, + t.CPtr(uint64(od) + uint64(i) * 8)) + i += 4 + while i < n: + if xd[i] > double(0.0): + od[i] = xd[i] + else: + od[i] = double(0.0) + i += 1 + stdlib.free(zero4) + + +def relu_backward(x: Tensor | t.CPtr, g: Tensor | t.CPtr, + gx: Tensor | t.CPtr): + """gx = g * (x > 0)""" + if x is None or g is None or gx is None: + return + n: sizet = x.Rows * x.Cols + xd: double | t.CPtr = x.Data + gd: double | t.CPtr = g.Data + od: double | t.CPtr = gx.Data + i: sizet = 0 + while i < n: + if xd[i] > double(0.0): + od[i] = gd[i] + else: + od[i] = double(0.0) + i += 1 + + +# ------------------------------------------------------------ +# softmax + CrossEntropy(融合, 数值稳定) —— MNIST 输出层 +# logits: (m, C), labels: m 个 int (0..C-1) +# loss 写入 *loss, 梯度(已按 1/m 归一)写入 grad (m, C) +# ------------------------------------------------------------ +def softmax_crossentropy(logits: Tensor | t.CPtr, + labels: int | t.CPtr, + n_labels: sizet, + loss: double | t.CPtr, + grad: Tensor | t.CPtr): + if logits is None or labels is None or grad is None: + return + if loss is None: + return + m: sizet = logits.Rows + c: sizet = logits.Cols + ld: double | t.CPtr = logits.Data + gd: double | t.CPtr = grad.Data + lb: int | t.CPtr = labels + total: double = double(0.0) + inv_m: double = double(1.0) / double(m) + b: sizet = 0 + while b < m: + # 数值稳定 softmax + mx: double = ld[b * c] + j: sizet = 1 + while j < c: + if ld[b * c + j] > mx: + mx = ld[b * c + j] + j += 1 + s: double = double(0.0) + j = 0 + while j < c: + s = s + vipermath.exp(ld[b * c + j] - mx) + j += 1 + y: int = lb[b] + # loss += -(logits[lab] - max - log(s)) + total = total - (ld[b * c + sizet(y)] - mx - vipermath.log(s)) + # 梯度: (softmax - onehot) / m + j = 0 + while j < c: + p: double = vipermath.exp(ld[b * c + j] - mx) / s + o: double = double(0.0) + if sizet(y) == j: + o = double(1.0) + gd[b * c + j] = (p - o) * inv_m + j += 1 + b += 1 + loss[0] = total + + +# ------------------------------------------------------------ +# 每行 argmax (分类预测) +# ------------------------------------------------------------ +def row_argmax(x: Tensor | t.CPtr, pred: int | t.CPtr): + if x is None or pred is None: + return + m: sizet = x.Rows + c: sizet = x.Cols + d: double | t.CPtr = x.Data + i: sizet = 0 + while i < m: + best: sizet = 0 + bv: double = d[i * c] + j: sizet = 1 + while j < c: + if d[i * c + j] > bv: + bv = d[i * c + j] + best = j + j += 1 + pred[i] = int(best) + i += 1 + + +# ------------------------------------------------------------ +# SGD: data -= lr * grad(全量梯度) / 重球法: v = m*v+g, data -= lr*v +# ------------------------------------------------------------ +def sgd_step(param: Tensor | t.CPtr, lr: double): + if param is None or param.Data is None or param.Grad is None: + return + d: double | t.CPtr = param.Data + g: double | t.CPtr = param.Grad + n: sizet = param.Rows * param.Cols + i: sizet = 0 + while i < n: + d[i] = d[i] - lr * g[i] + i += 1 + + +def sgd_step_momentum(param: Tensor | t.CPtr, vel: t.CPtr, + lr: double, momentum: double): + """带动量(重球法)的 SGD: v = m*v + g ; w -= lr*v + + vel 是与 param 同元素个数的 double 缓冲, 由调用方分配并清零(跨 step 持有); + vel 传 None 时退化为普通 sgd_step。动量是 CNN/MLP 训练收敛速度的关键 —— + 纯 SGD 在固定 lr 下前几个 epoch 很慢且容易卡在低准确率。""" + if param is None or param.Data is None or param.Grad is None: + return + if vel is None: + sgd_step(param, lr) + return + d: double | t.CPtr = param.Data + g: double | t.CPtr = param.Grad + v: double | t.CPtr = vel + n: sizet = param.Rows * param.Cols + i: sizet = 0 + while i < n: + vv: double = momentum * v[i] + g[i] + v[i] = vv + d[i] = d[i] - lr * vv + i += 1 + + +# ------------------------------------------------------------ +# 逐元素乘加 (调试/扩展用, vipersimd fma_array) +# a = a*b + c +# ------------------------------------------------------------ +def fma_array_inplace(a: Tensor | t.CPtr, b: t.CPtr, c: t.CPtr): + if a is None or a.Data is None: + return + vipersimd.simd_fma_array(a.Data, b, c, a.Data, a.Rows * a.Cols) + + +# ------------------------------------------------------------ +# Conv2d / MaxPool2d —— 轻量 CNN 用的原始缓冲算子 +# +# 这些算子直接吃裸缓冲(不用 Tensor: torch.Tensor 只有 2D 形状), +# 布局一律 NCHW 连续 double; 卷积权重 [cout][cin][k][k]。 +# 约定: +# *_fwd 覆盖写输出 +# conv2d_bwd_dx 先清零再累加(逐样本输入梯度, 不跨样本累积) +# maxpool2d_bwd 同上 +# conv2d_bwd_dw += 累加参数梯度(跨样本累积, 批次开始由调用方清零) +# +# SIMD: 三个卷积算子的内层都按"输出列"做 4 路 AVX2 double。 +# 依据是 stride==1 时相邻输出列恰好对应相邻输入列, 故一列 4 个 double +# 在内存里连续, 可直接 vmovupd + vfmadd213pd, 权重标量 vbroadcastsd。 +# 代价是需要一个"安全列区间": 4 条 lane 的输出列都在 [0,wout) 内, 且它们 +# 触及的输入列 [ox-pad, ox+3+k-1-pad] 全在 [0,win) 内 —— 即 +# ox ∈ [pad, win+pad-k-3]。区间外的列与 stride!=1 一律走原标量分支。 +# (maxpool2d 不做 SIMD: k==stride==2 时 4 个窗口起点间隔 2 个 double, +# 不是连续地址, 无 gather 原语; 且它占总计算量不到 1%。) +# ------------------------------------------------------------ +def _dptr(base: t.CPtr, elem: sizet) -> t.CPtr: + """裸 double 缓冲的元素下标 → 字节地址 + + 裸 t.CPtr 是 i8*, `p + n` 走的是字节偏移; 而 vipersimd 的 4 路原语按 + "4 个 double = 32 字节"读写, 所以下标必须显式 ×8。 + (与 linear_fwd 里的 t.CPtr(uint64(p) + i * 8) 同一写法。)""" + return t.CPtr(uint64(base) + uint64(elem) * 8) + + +def conv2d_fwd(x: t.CPtr, w: t.CPtr, b: t.CPtr, out: t.CPtr, + n: sizet, cin: sizet, hin: sizet, win: sizet, + cout: sizet, k: sizet, stride: sizet, + pad: sizet): + """out[n][cout][hout][wout] = conv2d(x, w) + b + hout = (hin + 2*pad - k)/stride + 1(wout 同理, 整除向下取整)""" + if x is None or w is None or out is None: + return + hout: sizet = (hin + pad * 2 - k) / stride + 1 + wout: sizet = (win + pad * 2 - k) / stride + 1 + xd: double | t.CPtr = x + wd: double | t.CPtr = w + od: double | t.CPtr = out + kk: sizet = cin * k * k + # SIMD 安全区间(见文件头说明) + simd_on: int = 0 + smax: sizet = 0 + if stride == 1 and win + pad >= k + 3: + simd_on = 1 + smax = win + pad - (k + 3) + wb: t.CPtr = None # 权重标量 × 4 路广播副本(kk 个标量) + t4: t.CPtr = None # 4 路累加器 + if simd_on != 0: + wb = stdlib.malloc(kk * 32) + t4 = stdlib.malloc(32) + if wb is None or t4 is None: + if wb is not None: + stdlib.free(wb) + if t4 is not None: + stdlib.free(t4) + wb = None + t4 = None + simd_on = 0 + wbd: double | t.CPtr = wb + t4d: double | t.CPtr = t4 + + bi: sizet = 0 + while bi < n: + co: sizet = 0 + while co < cout: + bias: double = double(0.0) + if b is not None: + bd: double | t.CPtr = b + bias = bd[co] + # 权重标量预广播: 每个 (ci,ky,kx) 一个 4 路副本, 本通道所有输出 + # 行/列共用。否则 vbroadcastsd(4 次 store) 会跟着 4 路循环重复, + # 把 SIMD 的收益吃掉一半。 + if simd_on != 0: + wb4: sizet = 0 + while wb4 < kk: + wq: double = wd[co * kk + wb4] + wbd[wb4 * 4] = wq + wbd[wb4 * 4 + 1] = wq + wbd[wb4 * 4 + 2] = wq + wbd[wb4 * 4 + 3] = wq + wb4 += 1 + oy: sizet = 0 + while oy < hout: + oy0: sizet = oy * stride + ox: sizet = 0 + while ox < wout: + if simd_on != 0 and ox >= pad and ox <= smax and ox + 4 <= wout: + # ---- 4 路: 一次算 4 个相邻输出列 ---- + t4d[0] = bias + t4d[1] = bias + t4d[2] = bias + t4d[3] = bias + ci: sizet = 0 + while ci < cin: + ky: sizet = 0 + while ky < k: + iy: sizet = oy0 + ky + if iy >= pad: + yy: sizet = iy - pad + if yy < hin: + xrow: sizet = ((bi * cin + ci) * hin + yy) * win + kx: sizet = 0 + while kx < k: + vipersimd.simd_fma4d( + _dptr(x, xrow + ox + kx - pad), + _dptr(wb, ((ci * k + ky) * k + kx) * 4), + t4, t4) + kx += 1 + ky += 1 + ci += 1 + ob: sizet = ((bi * cout + co) * hout + oy) * wout + ox + od[ob] = t4d[0] + od[ob + 1] = t4d[1] + od[ob + 2] = t4d[2] + od[ob + 3] = t4d[3] + ox += 4 + else: + # ---- 标量: 边界列 / stride != 1 ---- + ox0: sizet = ox * stride + acc: double = bias + ci2: sizet = 0 + while ci2 < cin: + ky2: sizet = 0 + while ky2 < k: + iy2: sizet = oy0 + ky2 + if iy2 >= pad: + yy2: sizet = iy2 - pad + if yy2 < hin: + kx2: sizet = 0 + while kx2 < k: + ix2: sizet = ox0 + kx2 + if ix2 >= pad: + xx2: sizet = ix2 - pad + if xx2 < win: + acc = acc + \ + wd[co * kk + (ci2 * k + ky2) * k + kx2] * \ + xd[((bi * cin + ci2) * hin + yy2) * win + xx2] + kx2 += 1 + ky2 += 1 + ci2 += 1 + od[((bi * cout + co) * hout + oy) * wout + ox] = acc + ox += 1 + oy += 1 + co += 1 + bi += 1 + if wb is not None: + stdlib.free(wb) + if t4 is not None: + stdlib.free(t4) + + +def conv2d_bwd_dw(x: t.CPtr, dout: t.CPtr, dw: t.CPtr, db: t.CPtr, + n: sizet, cin: sizet, hin: sizet, win: sizet, + cout: sizet, k: sizet, stride: sizet, + pad: sizet): + """累加: dw[co][ci][ky][kx] += sum dout*x ; db[co] += sum dout + (+= 语义, 调用方在批次开始前清零) + + 循环顺序为 (ci,ky,kx) 在外、(oy,ox) 在内: 每个权重元素只做一次 + 读-改-写; 内层按输出列 4 路 SIMD 累加, 最后 hsum 成标量。""" + if x is None or dout is None or dw is None: + return + hout: sizet = (hin + pad * 2 - k) / stride + 1 + wout: sizet = (win + pad * 2 - k) / stride + 1 + xd: double | t.CPtr = x + gd: double | t.CPtr = dout + dwd: double | t.CPtr = dw + dbd: double | t.CPtr = None + if db is not None: + dbd = db + kk: sizet = cin * k * k + simd_on: int = 0 + smax: sizet = 0 + if stride == 1 and win + pad >= k + 3: + simd_on = 1 + smax = win + pad - (k + 3) + acc: t.CPtr = None + if simd_on != 0: + acc = stdlib.malloc(32) + if acc is None: + simd_on = 0 + accd: double | t.CPtr = acc + + bi: sizet = 0 + while bi < n: + co: sizet = 0 + while co < cout: + gp: sizet = (bi * cout + co) * hout * wout + # ---- db[co] += sum(g): g 的整个通道平面是连续的 ---- + if dbd is not None: + tot0: double = double(0.0) + if simd_on != 0: + accd[0] = double(0.0) + accd[1] = double(0.0) + accd[2] = double(0.0) + accd[3] = double(0.0) + gcnt: sizet = hout * wout + gof: sizet = 0 + while gof + 4 <= gcnt: + if simd_on != 0: + vipersimd.simd_add4d(_dptr(dout, gp + gof), acc, acc) + else: + tot0 = tot0 + gd[gp + gof] + tot0 = tot0 + gd[gp + gof + 1] + tot0 = tot0 + gd[gp + gof + 2] + tot0 = tot0 + gd[gp + gof + 3] + gof += 4 + while gof < gcnt: + tot0 = tot0 + gd[gp + gof] + gof += 1 + if simd_on != 0: + vipersimd.simd_hsum4d(acc, acc) + tot0 = tot0 + accd[0] + dbd[co] = dbd[co] + tot0 + # ---- dw[co][ci][ky][kx] ---- + ci: sizet = 0 + while ci < cin: + ky: sizet = 0 + while ky < k: + kx: sizet = 0 + while kx < k: + tot: double = double(0.0) + if simd_on != 0: + accd[0] = double(0.0) + accd[1] = double(0.0) + accd[2] = double(0.0) + accd[3] = double(0.0) + oy: sizet = 0 + while oy < hout: + iy: sizet = oy * stride + ky + if iy >= pad: + yy: sizet = iy - pad + if yy < hin: + xrow: sizet = ((bi * cin + ci) * hin + yy) * win + goff: sizet = gp + oy * wout + ox: sizet = 0 + while ox < wout: + if simd_on != 0 and ox >= pad and ox <= smax and ox + 4 <= wout: + vipersimd.simd_fma4d( + _dptr(dout, goff + ox), + _dptr(x, xrow + ox + kx - pad), + acc, acc) + ox += 4 + else: + ix: sizet = ox * stride + kx + if ix >= pad: + xx: sizet = ix - pad + if xx < win: + tot = tot + gd[goff + ox] * \ + xd[xrow + xx] + ox += 1 + oy += 1 + if simd_on != 0: + vipersimd.simd_hsum4d(acc, acc) + tot = tot + accd[0] + wj: sizet = co * kk + (ci * k + ky) * k + kx + dwd[wj] = dwd[wj] + tot + kx += 1 + ky += 1 + ci += 1 + co += 1 + bi += 1 + if acc is not None: + stdlib.free(acc) + + +def conv2d_bwd_dx(w: t.CPtr, dout: t.CPtr, dx: t.CPtr, + n: sizet, cin: sizet, hin: sizet, win: sizet, + cout: sizet, k: sizet, stride: sizet, + pad: sizet): + """dx 先清零再累加(逐样本输入梯度): dx[ci][iy][ix] += sum dout*w + + stride==1 时内层按输出列 4 路 SIMD: 4 条 lane 对应的 dx 单元也是连续的, + 故可直接对 dx 做 in-place fma(g4 * w_broadcast + dx4)。""" + if w is None or dout is None or dx is None: + return + hout: sizet = (hin + pad * 2 - k) / stride + 1 + wout: sizet = (win + pad * 2 - k) / stride + 1 + wd: double | t.CPtr = w + gd: double | t.CPtr = dout + dxd: double | t.CPtr = dx + string.memset(dx, 0, n * cin * hin * win * sizet(8)) + kk: sizet = cin * k * k + simd_on: int = 0 + smax: sizet = 0 + if stride == 1 and win + pad >= k + 3: + simd_on = 1 + smax = win + pad - (k + 3) + wb: t.CPtr = None # 整个权重张量的 4 路广播副本(cout*kk 个标量) + if simd_on != 0: + wb = stdlib.malloc(cout * kk * 32) + if wb is None: + simd_on = 0 + wbd: double | t.CPtr = wb + if simd_on != 0: + # 权重在一次调用内不变 → 整体预广播一次(注意别在 4 路循环里重复 + # vbroadcastsd: 那是 4 次 store, 会把 SIMD 的收益吃掉一半) + wjn: sizet = cout * kk + wj4: sizet = 0 + while wj4 < wjn: + wq2: double = wd[wj4] + wbd[wj4 * 4] = wq2 + wbd[wj4 * 4 + 1] = wq2 + wbd[wj4 * 4 + 2] = wq2 + wbd[wj4 * 4 + 3] = wq2 + wj4 += 1 + + bi: sizet = 0 + while bi < n: + ci: sizet = 0 + while ci < cin: + ky: sizet = 0 + while ky < k: + oy: sizet = 0 + while oy < hout: + iy: sizet = oy * stride + ky + if iy >= pad: + yy: sizet = iy - pad + if yy < hin: + dxrow: sizet = ((bi * cin + ci) * hin + yy) * win + ox: sizet = 0 + while ox < wout: + if simd_on != 0 and ox >= pad and ox <= smax and ox + 4 <= wout: + co: sizet = 0 + while co < cout: + kx: sizet = 0 + while kx < k: + goff: sizet = ((bi * cout + co) * hout + oy) * wout + ox + doff: sizet = dxrow + ox + kx - pad + vipersimd.simd_fma4d( + _dptr(dout, goff), + _dptr(wb, (co * kk + (ci * k + ky) * k + kx) * 4), + _dptr(dx, doff), _dptr(dx, doff)) + kx += 1 + co += 1 + ox += 4 + else: + co2: sizet = 0 + while co2 < cout: + kx2: sizet = 0 + while kx2 < k: + ix: sizet = ox * stride + kx2 + if ix >= pad: + xx: sizet = ix - pad + if xx < win: + xi: sizet = dxrow + xx + gi: sizet = ((bi * cout + co2) * hout + oy) * wout + ox + dxd[xi] = dxd[xi] + \ + gd[gi] * wd[co2 * kk + (ci * k + ky) * k + kx2] + kx2 += 1 + co2 += 1 + ox += 1 + oy += 1 + ky += 1 + ci += 1 + bi += 1 + if wb is not None: + stdlib.free(wb) + + +# ------------------------------------------------------------ +# MaxPool2d (k x k, stride 通常 = k) +# ------------------------------------------------------------ +def maxpool2d_fwd(x: t.CPtr, out: t.CPtr, argmax: t.CPtr, + n: sizet, c: sizet, hin: sizet, win: sizet, + k: sizet, stride: sizet): + """argmax: int 数组 [n*c*hout*wout], 记窗口内最大值在本通道内的下标(yy*win+xx)""" + if x is None or out is None or argmax is None: + return + hout: sizet = (hin - k) / stride + 1 + wout: sizet = (win - k) / stride + 1 + xd: double | t.CPtr = x + od: double | t.CPtr = out + am: int | t.CPtr = argmax + bi: sizet = 0 + while bi < n: + ch: sizet = 0 + while ch < c: + plano: sizet = (bi * c + ch) * hin * win + oy: sizet = 0 + while oy < hout: + oy0: sizet = oy * stride + ox: sizet = 0 + while ox < wout: + ox0: sizet = ox * stride + best: double = double(0.0) + bidx: int = 0 + first: int = 1 + ky: sizet = 0 + while ky < k: + yy: sizet = oy0 + ky + kx: sizet = 0 + while kx < k: + xx: sizet = ox0 + kx + v: double = xd[plano + yy * win + xx] + if first != 0 or v > best: + best = v + bidx = int(yy * win + xx) + first = 0 + kx += 1 + ky += 1 + oi: sizet = (bi * c + ch) * hout * wout + oy * wout + ox + od[oi] = best + am[oi] = bidx + ox += 1 + oy += 1 + ch += 1 + bi += 1 + + +def maxpool2d_bwd(dout: t.CPtr, argmax: t.CPtr, dx: t.CPtr, + n: sizet, c: sizet, hin: sizet, win: sizet, + k: sizet, stride: sizet): + """dx 先清零再散射累加: dx[argmax] += dout""" + if dout is None or argmax is None or dx is None: + return + hout: sizet = (hin - k) / stride + 1 + wout: sizet = (win - k) / stride + 1 + gd: double | t.CPtr = dout + am: int | t.CPtr = argmax + dxd: double | t.CPtr = dx + string.memset(dx, 0, n * c * hin * win * sizet(8)) + bi: sizet = 0 + while bi < n: + ch: sizet = 0 + while ch < c: + plano: sizet = (bi * c + ch) * hin * win + oy: sizet = 0 + while oy < hout: + ox: sizet = 0 + while ox < wout: + oi: sizet = (bi * c + ch) * hout * wout + oy * wout + ox + tgt: sizet = plano + sizet(am[oi]) + dxd[tgt] = dxd[tgt] + gd[oi] + ox += 1 + oy += 1 + ch += 1 + bi += 1 diff --git a/vipermath.vp b/vipermath.vp index b121af1..2af83ab 100644 --- a/vipermath.vp +++ b/vipermath.vp @@ -168,48 +168,48 @@ def tanh(x: t.CDouble) -> t.CDouble: def exp(x: t.CDouble) -> t.CDouble: - if x > t.CDouble(709.0): - return t.CDouble(1.7976931348623157e308) - if x < t.CDouble(-709.0): - return t.CDouble(0.0) + """e^x + + 范围归约 + 短泰勒: 直接对 x 展开 18 项泰勒在 |x| 稍大时就会发散 + (x=-10 得 54.5, 真值 4.5e-5; x=-30 得 3.7e10), 负数端更是给出巨大正值, + 会让 softmax 的分母被撑爆 → 所有概率 ≈ 0。改为先做 + x = k*ln2 + r (|r| <= ln2/2), 再算 exp(r) 后乘 2^k。 + """ + # 先用"有限且落在范围里"的正向判断过一遍: NaN 与任何数比较都为假, 会同时漏过 + # 下面两个判断, 而 t.CInt(NaN) 在 x86 上是 INT_MIN -> 后面 2^k 缩放循环要跑 + # 21 亿次, 表现为"卡死"。故 NaN/越界必须在这里挡掉。 + if x <= t.CDouble(709.0) and x >= t.CDouble(-745.0): + pass + else: + if x > t.CDouble(709.0): + return t.CDouble(1.7976931348623157e308) + if x < t.CDouble(-745.0): + return t.CDouble(0.0) + return x # NaN: 原样返回, 让 NaN 传播而不是卡死 + # 归约: 把 x 折到 [-ln2/2, ln2/2], 整数部分累积到 k (内联取整, 避免前向引用) + ln2: t.CDouble = U_M_LN2 + q: t.CDouble = x / ln2 + k: t.CInt = t.CInt(q) + frac: t.CDouble = q - t.CDouble(k) + if frac > t.CDouble(0.5): + k = k + 1 + elif frac < t.CDouble(-0.5): + k = k - 1 + r: t.CDouble = x - t.CDouble(k) * ln2 + # exp(r), |r| <= 0.3466: 12 项后截断误差 < 1e-17 result: t.CDouble = t.CDouble(1.0) term: t.CDouble = t.CDouble(1.0) - term = term * x / t.CDouble(1.0) - result = result + term - term = term * x / t.CDouble(2.0) - result = result + term - term = term * x / t.CDouble(3.0) - result = result + term - term = term * x / t.CDouble(4.0) - result = result + term - term = term * x / t.CDouble(5.0) - result = result + term - term = term * x / t.CDouble(6.0) - result = result + term - term = term * x / t.CDouble(7.0) - result = result + term - term = term * x / t.CDouble(8.0) - result = result + term - term = term * x / t.CDouble(9.0) - result = result + term - term = term * x / t.CDouble(10.0) - result = result + term - term = term * x / t.CDouble(11.0) - result = result + term - term = term * x / t.CDouble(12.0) - result = result + term - term = term * x / t.CDouble(13.0) - result = result + term - term = term * x / t.CDouble(14.0) - result = result + term - term = term * x / t.CDouble(15.0) - result = result + term - term = term * x / t.CDouble(16.0) - result = result + term - term = term * x / t.CDouble(17.0) - result = result + term - term = term * x / t.CDouble(18.0) - result = result + term + n: t.CInt = 1 + while n <= 12: + term = term * r / t.CDouble(n) + result = result + term + n += 1 + while k > 0: + result = result * t.CDouble(2.0) + k = k - 1 + while k < 0: + result = result * t.CDouble(0.5) + k = k + 1 return result diff --git a/vipersimd.vp b/vipersimd.vp index cc5fbc1..842f933 100644 --- a/vipersimd.vp +++ b/vipersimd.vp @@ -428,6 +428,109 @@ vzeroupper""", 'ymm12', 'ymm13', 'ymm14', 'ymm15', 'rcx', 'rdx', 'r8', 'r9', 'r10', 'r11']) +def scalar_fma_array(a: t.CPtr, b: t.CPtr, c_val: t.CPtr, out: t.CPtr, n: t.CSizeT) -> t.CVoid: + """标量(逐元素)融合乘加: out[i] = a[i]*b[i] + c_val[i], 一次只算 1 个 double。 + + 与 simd_fma_array 是同一数学内核的"未向量化"对照实现: + * 指令用标量 FMA `vfmadd213sd`(xmm 低位), 与 SIMD 版的 `vfmadd213pd` + 同为硬件融合乘加、同为单次舍入 -> 逐元素结果与 SIMD 版**逐位一致**, + CHECK 可直接对照; + * 每次循环 3 读 1 写、8 字节粒度, 相比 ×4 的 ymm 少 4 倍数据并行度, + 用来量化"向量化本身"带来的收益(SIMD 加速比)。 + n 须 >= 1; n == 0 直接跳过。""" + c.Asm(f"""mov rcx, {c.AsmInp(n, t.ASM_DESCR.REG_ANY)} +mov r8, {c.AsmInp(a, t.ASM_DESCR.REG_ANY)} +mov r9, {c.AsmInp(b, t.ASM_DESCR.REG_ANY)} +mov rdx, {c.AsmInp(c_val, t.ASM_DESCR.REG_ANY)} +mov r10, {c.AsmInp(out, t.ASM_DESCR.REG_ANY)} +test rcx, rcx +jz .Lsca_end +.Lsca_loop: +vmovsd xmm0, [r8] +vmovsd xmm1, [r9] +vfmadd213sd xmm0, xmm1, [rdx] +vmovsd [r10], xmm0 +add r8, 8 +add r9, 8 +add rdx, 8 +add r10, 8 +dec rcx +jnz .Lsca_loop +.Lsca_end:""", + op=[t.ASM_DESCR.CLOBBER_MEMORY, 'xmm0', 'xmm1', + 'rcx', 'rdx', 'r8', 'r9', 'r10']) + + +def scalar_add_array(a: t.CPtr, b: t.CPtr, out: t.CPtr, n: t.CSizeT) -> t.CVoid: + """标量(逐元素)加法: out[i] = a[i] + b[i], 一次 1 个 double。 + + `simd_add_array` 的未向量化对照: 标量 `vaddsd` vs SIMD `vaddpd`, + 同为 IEEE 加法 -> 结果逐位一致。""" + c.Asm(f"""mov rcx, {c.AsmInp(n, t.ASM_DESCR.REG_ANY)} +mov r8, {c.AsmInp(a, t.ASM_DESCR.REG_ANY)} +mov r9, {c.AsmInp(b, t.ASM_DESCR.REG_ANY)} +mov r10, {c.AsmInp(out, t.ASM_DESCR.REG_ANY)} +test rcx, rcx +jz .Lsca_add_end +.Lsca_add_loop: +vmovsd xmm0, [r8] +vaddsd xmm0, xmm0, [r9] +vmovsd [r10], xmm0 +add r8, 8 +add r9, 8 +add r10, 8 +dec rcx +jnz .Lsca_add_loop +.Lsca_add_end:""", + op=[t.ASM_DESCR.CLOBBER_MEMORY, 'xmm0', 'rcx', 'r8', 'r9', 'r10']) + + +def scalar_mul_array(a: t.CPtr, b: t.CPtr, out: t.CPtr, n: t.CSizeT) -> t.CVoid: + """标量(逐元素)乘法: out[i] = a[i] * b[i], 一次 1 个 double。 + + `simd_mul_array` 的未向量化对照: 标量 `vmulsd` vs SIMD `vmulpd`, + 同为 IEEE 乘法 -> 结果逐位一致。""" + c.Asm(f"""mov rcx, {c.AsmInp(n, t.ASM_DESCR.REG_ANY)} +mov r8, {c.AsmInp(a, t.ASM_DESCR.REG_ANY)} +mov r9, {c.AsmInp(b, t.ASM_DESCR.REG_ANY)} +mov r10, {c.AsmInp(out, t.ASM_DESCR.REG_ANY)} +test rcx, rcx +jz .Lsca_mul_end +.Lsca_mul_loop: +vmovsd xmm0, [r8] +vmulsd xmm0, xmm0, [r9] +vmovsd [r10], xmm0 +add r8, 8 +add r9, 8 +add r10, 8 +dec rcx +jnz .Lsca_mul_loop +.Lsca_mul_end:""", + op=[t.ASM_DESCR.CLOBBER_MEMORY, 'xmm0', 'rcx', 'r8', 'r9', 'r10']) + + +def scalar_sqrt_array(a: t.CPtr, out: t.CPtr, n: t.CSizeT) -> t.CVoid: + """标量(逐元素)平方根: out[i] = sqrt(a[i]), 一次 1 个 double。 + + `simd_sqrt_array` 的未向量化对照: 标量 `vsqrtsd` vs SIMD `vsqrtpd`, + 两者都是 IEEE 正确舍入的平方根 -> 结果逐位一致。""" + c.Asm(f"""mov rcx, {c.AsmInp(n, t.ASM_DESCR.REG_ANY)} +mov r8, {c.AsmInp(a, t.ASM_DESCR.REG_ANY)} +mov r10, {c.AsmInp(out, t.ASM_DESCR.REG_ANY)} +test rcx, rcx +jz .Lsca_sqrt_end +.Lsca_sqrt_loop: +vmovsd xmm0, [r8] +vsqrtsd xmm0, xmm0, xmm0 +vmovsd [r10], xmm0 +add r8, 8 +add r10, 8 +dec rcx +jnz .Lsca_sqrt_loop +.Lsca_sqrt_end:""", + op=[t.ASM_DESCR.CLOBBER_MEMORY, 'xmm0', 'rcx', 'r8', 'r10']) + + def simd_div_array(a: t.CPtr, b: t.CPtr, out: t.CPtr, n: t.CSizeT) -> t.CVoid: """AVX2 batch: out[i:i+4] = a[i:i+4] / b[i:i+4]""" c.Asm(f"""mov rcx, {c.AsmInp(n, t.ASM_DESCR.REG_ANY)} diff --git a/vqt6/CMakeLists.txt b/vqt6/CMakeLists.txt index 9a80def..32a5b68 100644 --- a/vqt6/CMakeLists.txt +++ b/vqt6/CMakeLists.txt @@ -27,6 +27,7 @@ find_package(Qt6 6.5 REQUIRED COMPONENTS Gui Widgets Network + Charts ) # Win32 GUI 应用不需要控制台 @@ -40,6 +41,7 @@ set(VQT6_SOURCES _bridge_widgets.cpp _bridge_gui.cpp _bridge_network.cpp + _bridge_canvas.cpp ) set(VQT6_HEADERS @@ -47,6 +49,7 @@ set(VQT6_HEADERS _bridge_widgets.h _bridge_gui.h _bridge_network.h + _bridge_canvas.h ) # Qt 模块 @@ -55,6 +58,7 @@ set(VQT6_QT_MODULES Qt6::Gui Qt6::Widgets Qt6::Network + Qt6::Charts ) # ============================================================ diff --git a/vqt6/__init__.vp b/vqt6/__init__.vp index 630510a..0ecdbab 100644 --- a/vqt6/__init__.vp +++ b/vqt6/__init__.vp @@ -23,6 +23,15 @@ from vqt6._qtnetwork import * # ============================================================ # 高级 API 类(Pythonic 包装,不透明指针) +# +# 为什么每个类都写 __new__: +# Viper 的类实例默认是"创建函数当前帧的 alloca"(见 +# CallGenCtor._translate_struct_ctor: tmp = build_alloca(struct_ty))。 +# 一旦包装对象被存进字段、或被 return 出函数,帧销毁后该指针即悬垂—— +# 表现为读 _ptr 得到垃圾、再把垃圾传给 Qt(如 QLayout::addWidget 崩溃)。 +# 编译器在类定义了 __new__ 时改用其返回值作存储指针,故这里统一 +# stdlib.malloc 堆分配包装壳(单字段 { _ptr } = 8 字节,进程期存活)。 +# 同模块内即用即弃的场景不受影响;跨帧/跨模块存取则必须靠它。 # ============================================================ # ============================================================ @@ -140,6 +149,9 @@ class QApplication: """QApplication 包装(不透明指针)""" _ptr: QtAppPtr + def __new__(self, argv: list[str] | t.CPtr) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, argv: list[str] | t.CPtr): # 把 argv 转换为 argc/argv 数组 argc: t.CInt = 1 @@ -168,12 +180,27 @@ class QApplication: def desktop_height(self) -> int: return vqt6_qapplication_desktop_height(self._ptr) + def set_font_family(self, family: str): + """设置应用级字体族(全局生效) + + 必须在创建任何控件之前调用:Qt 是在控件构造时从应用字体继承的, + 之后再设只会影响"此后新建"的控件。 + + 这里必须走应用字体而不是给主窗口挂样式表(font-family):Qt Charts 的 + 标题/图例/坐标轴标签画在 QChart(QGraphicsWidget)上,样式表够不到它们, + 只有应用字体能一并覆盖。 + """ + vqt6_qapplication_set_font_family_utf8(self._ptr, family) + @t.NoVTable class QWidget: """QWidget 包装""" _ptr: QtWidgetPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qwidget_create_with_parent(parent._ptr) @@ -236,6 +263,9 @@ class QLabel: """QLabel 包装""" _ptr: QtLabelPtr + def __new__(self, text: str = "", parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, text: str = "", parent: "QWidget | None" = None): text_qs: QtStringPtr = vqt6_qstring_create(text) if parent is not None and parent._ptr is not None: @@ -273,6 +303,9 @@ class QPushButton: """QPushButton 包装""" _ptr: QtPushButtonPtr + def __new__(self, text: str = "", parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, text: str = "", parent: "QWidget | None" = None): text_qs: QtStringPtr = vqt6_qstring_create(text) if parent is not None and parent._ptr is not None: @@ -328,13 +361,183 @@ class QPushButton: # ============================================================ -# 窗口:QMainWindow / QDialog +# 菜单:QAction / QMenu / QMenuBar / QStatusBar +# +# 按依赖顺序定义(QAction → QMenu → QMenuBar → QStatusBar), 并整体排在 +# QMainWindow 之前, 使 addMenu()/addActionWithText() 的返回注解不再前向引用。 +# +# 注意: 返回"包装类对象"(按值 struct)的方法无法跨模块解析返回类型—— +# 依赖模块 text.ll 里是 %"sha1.QStatusBar" 这类 struct 值(不含 *, +# _ParseLLVMRetTyStr 解析失败), 而 vpi 缓存的是带引号的前向注解 +# ('QStatusBar'), 同样解析失败 → 调用方 fatal(mro_ret)。 +# 故 QMainWindow.menuBar/statusBar 一律返回裸指针。 +# ============================================================ +@t.NoVTable +class QAction: + """QAction 包装""" + _ptr: QtActionPtr + + def __init__(self, text: str = ""): + if text != "": + qs: QtStringPtr = vqt6_qstring_create(text) + self._ptr = vqt6_qaction_create_with_text(qs) + vqt6_qstring_free(qs) + else: + self._ptr = vqt6_qaction_create() + if self._ptr is None: + raise RuntimeError("Failed to create QAction") + + def setText(self, text: str): + qs: QtStringPtr = vqt6_qstring_create(text) + vqt6_qaction_set_text(self._ptr, qs) + vqt6_qstring_free(qs) + + def text(self) -> str: + return _w_text_of(vqt6_qaction_text(self._ptr)) + + def setShortcut(self, key_seq: str): + qs: QtStringPtr = vqt6_qstring_create(key_seq) + vqt6_qaction_set_shortcut(self._ptr, qs) + vqt6_qstring_free(qs) + + def setCheckable(self, checkable: int): + vqt6_qaction_set_checkable(self._ptr, checkable) + + def isCheckable(self) -> int: + return vqt6_qaction_is_checkable(self._ptr) + + def setChecked(self, checked: int): + vqt6_qaction_set_checked(self._ptr, checked) + + def isChecked(self) -> int: + return vqt6_qaction_is_checked(self._ptr) + + def setEnabled(self, enabled: int): + vqt6_qaction_set_enabled(self._ptr, enabled) + + def isEnabled(self) -> int: + return vqt6_qaction_is_enabled(self._ptr) + + def trigger(self): + vqt6_qaction_trigger(self._ptr) + + def triggered(self, cb: bytes, data: bytes = 0): + vqt6_qaction_connect_triggered(self._ptr, cb, data) + + +@t.NoVTable +class QMenu: + """QMenu 包装""" + _ptr: QtMenuPtr + + def __init__(self, title: str = ""): + if title != "": + qs: QtStringPtr = vqt6_qstring_create(title) + self._ptr = vqt6_qmenu_create_with_title(qs) + vqt6_qstring_free(qs) + else: + self._ptr = vqt6_qmenu_create() + if self._ptr is None: + raise RuntimeError("Failed to create QMenu") + + def addAction(self, action: t.CPtr): + vqt6_qmenu_add_action(self._ptr, action) + + def addActionWithText(self, text: str) -> "QAction": + qs: QtStringPtr = vqt6_qstring_create(text) + ap: QtActionPtr = vqt6_qmenu_add_action_with_text(self._ptr, qs) + vqt6_qstring_free(qs) + a = QAction() + a._ptr = ap + return a + + def addMenu(self, title: str) -> "QMenu": + qs: QtStringPtr = vqt6_qstring_create(title) + mp: QtMenuPtr = vqt6_qmenu_add_menu(self._ptr, qs) + vqt6_qstring_free(qs) + m = QMenu() + m._ptr = mp + return m + + def addSeparator(self): + vqt6_qmenu_add_separator(self._ptr) + + def setTitle(self, title: str): + qs: QtStringPtr = vqt6_qstring_create(title) + vqt6_qmenu_set_title(self._ptr, qs) + vqt6_qstring_free(qs) + + def clear(self): + vqt6_qmenu_clear(self._ptr) + + +@t.NoVTable +class QMenuBar: + """QMenuBar 包装""" + _ptr: QtMenuBarPtr + + def __init__(self, parent: "QWidget | None" = None): + if parent is not None and parent._ptr is not None: + self._ptr = vqt6_qmenubar_create_with_parent(parent._ptr) + else: + self._ptr = vqt6_qmenubar_create() + if self._ptr is None: + raise RuntimeError("Failed to create QMenuBar") + + def addMenu(self, title: str) -> "QMenu": + qs: QtStringPtr = vqt6_qstring_create(title) + mp: QtMenuPtr = vqt6_qmenubar_add_menu(self._ptr, qs) + vqt6_qstring_free(qs) + m = QMenu() + m._ptr = mp + return m + + def addAction(self, action: t.CPtr): + vqt6_qmenubar_add_action(self._ptr, action) + + def clear(self): + vqt6_qmenubar_clear(self._ptr) + + +@t.NoVTable +class QStatusBar: + """QStatusBar 包装""" + _ptr: QtStatusBarPtr + + def __init__(self, parent: "QWidget | None" = None): + if parent is not None and parent._ptr is not None: + self._ptr = vqt6_qstatusbar_create_with_parent(parent._ptr) + else: + self._ptr = vqt6_qstatusbar_create() + if self._ptr is None: + raise RuntimeError("Failed to create QStatusBar") + + def showMessage(self, message: str, timeout_ms: int = 0): + qs: QtStringPtr = vqt6_qstring_create(message) + vqt6_qstatusbar_show_message(self._ptr, qs, timeout_ms) + vqt6_qstring_free(qs) + + def clearMessage(self): + vqt6_qstatusbar_clear_message(self._ptr) + + def currentMessage(self) -> str: + return _w_text_of(vqt6_qstatusbar_current_message(self._ptr)) + + def addWidget(self, w: t.CPtr, stretch: int = 0): + vqt6_qstatusbar_add_widget(self._ptr, w, stretch) + + +# ============================================================ +# 窗口:QMainWindow(依赖上面的 QMenuBar / QStatusBar, 故排在其后) # ============================================================ @t.NoVTable class QMainWindow: """QMainWindow 包装""" _ptr: QtMainWindowPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qmainwindow_create_with_parent(parent._ptr) @@ -346,20 +549,16 @@ class QMainWindow: def setCentralWidget(self, w: t.CPtr): vqt6_qmainwindow_set_central_widget(self._ptr, w) - def menuBar(self) -> "QMenuBar": - mb_ptr: QtMenuBarPtr = vqt6_qmainwindow_menu_bar(self._ptr) - mb = QMenuBar() - mb._ptr = mb_ptr - return mb + def menuBar(self) -> QtMenuBarPtr: + """返回窗口自带的菜单栏裸指针(不新建; 需要包装类请自行 new QMenuBar 后挂 _ptr)""" + return vqt6_qmainwindow_menu_bar(self._ptr) def setMenuBar(self, mb: t.CPtr): vqt6_qmainwindow_set_menu_bar(self._ptr, mb) - def statusBar(self) -> "QStatusBar": - sb_ptr: QtStatusBarPtr = vqt6_qmainwindow_status_bar(self._ptr) - sb = QStatusBar() - sb._ptr = sb_ptr - return sb + def statusBar(self) -> QtStatusBarPtr: + """返回窗口自带的状态栏裸指针(不新建; 需要包装类请自行 new QStatusBar 后挂 _ptr)""" + return vqt6_qmainwindow_status_bar(self._ptr) def addToolBar(self, tb: t.CPtr): vqt6_qmainwindow_add_tool_bar(self._ptr, tb) @@ -385,6 +584,9 @@ class QMainWindow: def setWindowTitle(self, title: str): _w_set_window_title(self._ptr, title) + def setStyleSheet(self, qss: str): + vqt6_qwidget_set_stylesheet_utf8(self._ptr, qss) + def setEnabled(self, enabled: int): _w_set_enabled(self._ptr, enabled) @@ -453,6 +655,9 @@ class QLineEdit: """QLineEdit 包装""" _ptr: QtLineEditPtr + def __new__(self, text: str = "", parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, text: str = "", parent: "QWidget | None" = None): qs: QtStringPtr = vqt6_qstring_create(text) if parent is not None and parent._ptr is not None: @@ -518,6 +723,9 @@ class QCheckBox: """QCheckBox 包装""" _ptr: QtCheckBoxPtr + def __new__(self, text: str = "", parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, text: str = "", parent: "QWidget | None" = None): qs: QtStringPtr = vqt6_qstring_create(text) if parent is not None and parent._ptr is not None: @@ -578,6 +786,9 @@ class QRadioButton: """QRadioButton 包装""" _ptr: QtRadioButtonPtr + def __new__(self, text: str = "", parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, text: str = "", parent: "QWidget | None" = None): qs: QtStringPtr = vqt6_qstring_create(text) if parent is not None and parent._ptr is not None: @@ -760,6 +971,9 @@ class QSpinBox: """QSpinBox 包装""" _ptr: QtSpinBoxPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qspinbox_create_with_parent(parent._ptr) @@ -826,6 +1040,9 @@ class QProgressBar: """QProgressBar 包装""" _ptr: QtProgressBarPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qprogressbar_create_with_parent(parent._ptr) @@ -857,6 +1074,9 @@ class QProgressBar: def setTextVisible(self, visible: int): vqt6_qprogressbar_set_text_visible(self._ptr, visible) + def setStyleSheet(self, qss: str): + vqt6_qwidget_set_stylesheet_utf8(self._ptr, qss) + def valueChanged(self, cb: bytes, data: bytes = 0): vqt6_qprogressbar_connect_value_changed(self._ptr, cb, data) @@ -946,6 +1166,9 @@ class QPlainTextEdit: """QPlainTextEdit 包装""" _ptr: QtPlainTextEditPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qplaintextedit_create_with_parent(parent._ptr) @@ -1226,6 +1449,9 @@ class QTabWidget: """QTabWidget 包装""" _ptr: QtTabWidgetPtr + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self, parent: "QWidget | None" = None): if parent is not None and parent._ptr is not None: self._ptr = vqt6_qtabwidget_create_with_parent(parent._ptr) @@ -1553,6 +1779,9 @@ class QTimer: """QTimer 包装""" _ptr: QtTimerPtr + def __new__(self) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + def __init__(self): self._ptr = vqt6_qtimer_create() if self._ptr is None: @@ -1580,164 +1809,6 @@ class QTimer: vqt6_qtimer_connect_timeout(self._ptr, cb, data) -# ============================================================ -# 菜单:QMenuBar / QMenu / QAction / QStatusBar -# ============================================================ -@t.NoVTable -class QMenuBar: - """QMenuBar 包装""" - _ptr: QtMenuBarPtr - - def __init__(self, parent: "QWidget | None" = None): - if parent is not None and parent._ptr is not None: - self._ptr = vqt6_qmenubar_create_with_parent(parent._ptr) - else: - self._ptr = vqt6_qmenubar_create() - if self._ptr is None: - raise RuntimeError("Failed to create QMenuBar") - - def addMenu(self, title: str) -> "QMenu": - qs: QtStringPtr = vqt6_qstring_create(title) - mp: QtMenuPtr = vqt6_qmenubar_add_menu(self._ptr, qs) - vqt6_qstring_free(qs) - m = QMenu() - m._ptr = mp - return m - - def addAction(self, action: t.CPtr): - vqt6_qmenubar_add_action(self._ptr, action) - - def clear(self): - vqt6_qmenubar_clear(self._ptr) - - -@t.NoVTable -class QMenu: - """QMenu 包装""" - _ptr: QtMenuPtr - - def __init__(self, title: str = ""): - if title != "": - qs: QtStringPtr = vqt6_qstring_create(title) - self._ptr = vqt6_qmenu_create_with_title(qs) - vqt6_qstring_free(qs) - else: - self._ptr = vqt6_qmenu_create() - if self._ptr is None: - raise RuntimeError("Failed to create QMenu") - - def addAction(self, action: t.CPtr): - vqt6_qmenu_add_action(self._ptr, action) - - def addActionWithText(self, text: str) -> "QAction": - qs: QtStringPtr = vqt6_qstring_create(text) - ap: QtActionPtr = vqt6_qmenu_add_action_with_text(self._ptr, qs) - vqt6_qstring_free(qs) - a = QAction() - a._ptr = ap - return a - - def addMenu(self, title: str) -> "QMenu": - qs: QtStringPtr = vqt6_qstring_create(title) - mp: QtMenuPtr = vqt6_qmenu_add_menu(self._ptr, qs) - vqt6_qstring_free(qs) - m = QMenu() - m._ptr = mp - return m - - def addSeparator(self): - vqt6_qmenu_add_separator(self._ptr) - - def setTitle(self, title: str): - qs: QtStringPtr = vqt6_qstring_create(title) - vqt6_qmenu_set_title(self._ptr, qs) - vqt6_qstring_free(qs) - - def clear(self): - vqt6_qmenu_clear(self._ptr) - - -@t.NoVTable -class QAction: - """QAction 包装""" - _ptr: QtActionPtr - - def __init__(self, text: str = ""): - if text != "": - qs: QtStringPtr = vqt6_qstring_create(text) - self._ptr = vqt6_qaction_create_with_text(qs) - vqt6_qstring_free(qs) - else: - self._ptr = vqt6_qaction_create() - if self._ptr is None: - raise RuntimeError("Failed to create QAction") - - def setText(self, text: str): - qs: QtStringPtr = vqt6_qstring_create(text) - vqt6_qaction_set_text(self._ptr, qs) - vqt6_qstring_free(qs) - - def text(self) -> str: - return _w_text_of(vqt6_qaction_text(self._ptr)) - - def setShortcut(self, key_seq: str): - qs: QtStringPtr = vqt6_qstring_create(key_seq) - vqt6_qaction_set_shortcut(self._ptr, qs) - vqt6_qstring_free(qs) - - def setCheckable(self, checkable: int): - vqt6_qaction_set_checkable(self._ptr, checkable) - - def isCheckable(self) -> int: - return vqt6_qaction_is_checkable(self._ptr) - - def setChecked(self, checked: int): - vqt6_qaction_set_checked(self._ptr, checked) - - def isChecked(self) -> int: - return vqt6_qaction_is_checked(self._ptr) - - def setEnabled(self, enabled: int): - vqt6_qaction_set_enabled(self._ptr, enabled) - - def isEnabled(self) -> int: - return vqt6_qaction_is_enabled(self._ptr) - - def trigger(self): - vqt6_qaction_trigger(self._ptr) - - def triggered(self, cb: bytes, data: bytes = 0): - vqt6_qaction_connect_triggered(self._ptr, cb, data) - - -@t.NoVTable -class QStatusBar: - """QStatusBar 包装""" - _ptr: QtStatusBarPtr - - def __init__(self, parent: "QWidget | None" = None): - if parent is not None and parent._ptr is not None: - self._ptr = vqt6_qstatusbar_create_with_parent(parent._ptr) - else: - self._ptr = vqt6_qstatusbar_create() - if self._ptr is None: - raise RuntimeError("Failed to create QStatusBar") - - def showMessage(self, message: str, timeout_ms: int = 0): - qs: QtStringPtr = vqt6_qstring_create(message) - vqt6_qstatusbar_show_message(self._ptr, qs, timeout_ms) - vqt6_qstring_free(qs) - - def clearMessage(self): - vqt6_qstatusbar_clear_message(self._ptr) - - def currentMessage(self) -> str: - return _w_text_of(vqt6_qstatusbar_current_message(self._ptr)) - - def addWidget(self, w: t.CPtr, stretch: int = 0): - vqt6_qstatusbar_add_widget(self._ptr, w, stretch) - - # ============================================================ # 绘图基础:QColor / QFont # ============================================================ @@ -1798,6 +1869,278 @@ class QFont: vqt6_qfont_set_underline(self._ptr, underline) +# ============================================================ +# 剪贴板(复制链接等) +# ============================================================ +def clipboard_set_text(text: str): + """把文本写入系统剪贴板""" + vqt6_clipboard_set_text_utf8(text) + + +def clipboard_text() -> str: + """读取系统剪贴板文本""" + buf: bytes = stdlib.malloc(4096) + vqt6_clipboard_text_utf8(buf, 4096) + s: str = buf + stdlib.free(buf) + return s + + +# ============================================================ +# 自绘控件:Canvas / Graph3D / ChartView +# ============================================================ +@t.NoVTable +class Canvas: + """手绘板 / 28x28 灰度显示控件 + + 手绘模式下鼠标左键描绘; toMnist28 把笔迹按 MNIST 预处理 + (包围盒缩放到 20x20 后质心居中)输出为 28x28 灰度字节数组。 + """ + _ptr: QtCanvasPtr + + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + + def __init__(self, parent: "QWidget | None" = None): + if parent is not None and parent._ptr is not None: + self._ptr = vqt6_canvas_create_with_parent(parent._ptr) + else: + self._ptr = vqt6_canvas_create() + if self._ptr is None: + raise RuntimeError("Failed to create Canvas") + + def clear(self): + vqt6_canvas_clear(self._ptr) + + def hasInk(self) -> int: + return vqt6_canvas_has_ink(self._ptr) + + def setMode(self, mode: int): + """0 = 手绘, 1 = 图像显示""" + vqt6_canvas_set_mode(self._ptr, mode) + + def mode(self) -> int: + return vqt6_canvas_mode(self._ptr) + + def setPen(self, r: int, g: int, b: int, width_norm: t.CDouble): + vqt6_canvas_set_pen(self._ptr, r, g, b, width_norm) + + def setGrid(self, on: int): + vqt6_canvas_set_grid(self._ptr, on) + + def setScanLine(self, y_norm: int): + """0..1000 扫描线位置, <0 关闭""" + vqt6_canvas_set_scan_line(self._ptr, y_norm) + + def setBackground(self, r: int, g: int, b: int): + vqt6_canvas_set_bg(self._ptr, r, g, b) + + def toMnist28(self, out: bytes) -> int: + """笔迹 → 28x28 灰度(写入 out); 无墨迹返回 0""" + return vqt6_canvas_to_mnist28(self._ptr, out) + + def setImage28(self, inp: bytes): + """28x28 灰度 → 显示(自动切到图像显示模式)""" + vqt6_canvas_set_image28(self._ptr, inp) + + def changed(self, cb: bytes, data: bytes = 0): + """笔迹完成(鼠标抬起)回调: def cb(slot_id: int, data: t.CPtr)""" + vqt6_canvas_connect_changed(self._ptr, cb, data) + + def show(self): + _w_show(self._ptr) + + def hide(self): + _w_hide(self._ptr) + + def setGeometry(self, x: int, y: int, w: int, h: int): + _w_set_geometry(self._ptr, x, y, w, h) + + def resize(self, w: int, h: int): + _w_resize(self._ptr, w, h) + + def move(self, x: int, y: int): + _w_move(self._ptr, x, y) + + def setEnabled(self, enabled: int): + _w_set_enabled(self._ptr, enabled) + + def setStyleSheet(self, qss: str): + vqt6_qwidget_set_stylesheet_utf8(self._ptr, qss) + + +@t.NoVTable +class Graph3D: + """软件 3D 神经网络结构图控件 + + 层次沿 Z 轴排布(输入层在最远端), 各层按近正方形网格铺开; + 相邻层前向全连接。左键拖拽旋转, 滚轮缩放。 + 渲染 API 与后端无关, 将来换 OpenGL 实现时调用代码无需改动。 + """ + _ptr: QtGraph3DPtr + + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + + def __init__(self, parent: "QWidget | None" = None): + if parent is not None and parent._ptr is not None: + self._ptr = vqt6_graph3d_create_with_parent(parent._ptr) + else: + self._ptr = vqt6_graph3d_create() + if self._ptr is None: + raise RuntimeError("Failed to create Graph3D") + + def setup(self, layer_sizes: t.CInt | t.CPtr, n_layers: int): + """layer_sizes: int 数组指针(输入层→输出层各层节点数)""" + vqt6_graph3d_setup(self._ptr, layer_sizes, n_layers) + + def clear(self): + vqt6_graph3d_clear(self._ptr) + + def nodeCount(self) -> int: + return vqt6_graph3d_node_count(self._ptr) + + def edgeCount(self) -> int: + return vqt6_graph3d_edge_count(self._ptr) + + def setLayerAlpha(self, layer: int, alpha: t.CDouble): + """逐层点亮: 0..1""" + vqt6_graph3d_set_layer_alpha(self._ptr, layer, alpha) + + def setAllLayerAlpha(self, alpha: t.CDouble): + vqt6_graph3d_set_all_layer_alpha(self._ptr, alpha) + + def setValues(self, values: t.CDouble | t.CPtr, n: int): + """每节点激活值(0..1), 影响亮度与半径""" + vqt6_graph3d_set_values(self._ptr, values, n) + + def setEdgeWeights(self, w: t.CDouble | t.CPtr, n: int): + """边权重(按层间顺序), 影响颜色/透明度并按 |w| 裁剪""" + vqt6_graph3d_set_edge_weights(self._ptr, w, n) + + def setEdgeThreshold(self, tval: t.CDouble): + vqt6_graph3d_set_edge_threshold(self._ptr, tval) + + def setEdgeBudget(self, max_edges: int): + vqt6_graph3d_set_edge_budget(self._ptr, max_edges) + + def setView(self, yaw_deg: t.CDouble, pitch_deg: t.CDouble, dist: t.CDouble): + vqt6_graph3d_set_view(self._ptr, yaw_deg, pitch_deg, dist) + + def setNodeScale(self, s: t.CDouble): + vqt6_graph3d_set_node_scale(self._ptr, s) + + def setBackground(self, r: int, g: int, b: int): + vqt6_graph3d_set_bg(self._ptr, r, g, b) + + def resetView(self): + vqt6_graph3d_reset_view(self._ptr) + + def view(self, yaw_deg: t.CDouble | t.CPtr, pitch_deg: t.CDouble | t.CPtr, + dist: t.CDouble | t.CPtr): + vqt6_graph3d_get_view(self._ptr, yaw_deg, pitch_deg, dist) + + def viewChanged(self, cb: bytes, data: bytes = 0): + vqt6_graph3d_connect_view_changed(self._ptr, cb, data) + + def show(self): + _w_show(self._ptr) + + def hide(self): + _w_hide(self._ptr) + + def setGeometry(self, x: int, y: int, w: int, h: int): + _w_set_geometry(self._ptr, x, y, w, h) + + def resize(self, w: int, h: int): + _w_resize(self._ptr, w, h) + + def move(self, x: int, y: int): + _w_move(self._ptr, x, y) + + def setEnabled(self, enabled: int): + _w_set_enabled(self._ptr, enabled) + + +@t.NoVTable +class ChartView: + """Qt Charts 折线图控件(右侧 Y 轴可选) + + 典型用法: + cv = ChartView() + loss = cv.addLine("loss", 255, 107, 107, 0) + acc = cv.addLine("acc", 77, 171, 247, 1) + cv.append(loss, step, loss_value) + cv.autoscale() + """ + _ptr: QtChartViewPtr + + def __new__(self, parent: "QWidget | None" = None) -> t.CPtr: + return stdlib.malloc(8) # 包装壳堆分配, 见本文件顶部说明 + + def __init__(self, parent: "QWidget | None" = None): + if parent is not None and parent._ptr is not None: + self._ptr = vqt6_chartview_create_with_parent(parent._ptr) + else: + self._ptr = vqt6_chartview_create() + if self._ptr is None: + raise RuntimeError("Failed to create ChartView") + + def addLine(self, name: str, r: int, g: int, b: int, right_axis: int = 0) -> QtChartSeriesPtr: + return vqt6_chartview_add_line(self._ptr, name, r, g, b, right_axis) + + def append(self, series: QtChartSeriesPtr, x: t.CDouble, y: t.CDouble): + vqt6_chartview_append(self._ptr, series, x, y) + + def clearSeries(self, series: QtChartSeriesPtr): + vqt6_chartview_clear_series(self._ptr, series) + + def clearAll(self): + vqt6_chartview_clear_all(self._ptr) + + def autoscale(self): + vqt6_chartview_autoscale(self._ptr) + + def setTitle(self, title: str): + vqt6_chartview_set_title(self._ptr, title) + + def setLegendVisible(self, visible: int): + vqt6_chartview_set_legend_visible(self._ptr, visible) + + def setBackground(self, r: int, g: int, b: int): + vqt6_chartview_set_background(self._ptr, r, g, b) + + def setMaxPoints(self, max_points: int): + """滑动窗口: 每条折线超过 max_points 就丢弃最旧的点(<=0 表示不限制)""" + vqt6_chartview_set_max_points(self._ptr, max_points) + + def openZoom(self, title: str): + """打开(或复用)大窗查看当前曲线; 关闭只是隐藏, 不销毁""" + vqt6_chartview_open_zoom(self._ptr, title) + + def syncZoom(self): + """把当前曲线与轴范围同步到大窗(大窗未打开时什么也不做)""" + vqt6_chartview_sync_zoom(self._ptr) + + def show(self): + _w_show(self._ptr) + + def hide(self): + _w_hide(self._ptr) + + def setGeometry(self, x: int, y: int, w: int, h: int): + _w_set_geometry(self._ptr, x, y, w, h) + + def resize(self, w: int, h: int): + _w_resize(self._ptr, w, h) + + def move(self, x: int, y: int): + _w_move(self._ptr, x, y) + + def setEnabled(self, enabled: int): + _w_set_enabled(self._ptr, enabled) + + # ============================================================ # 模块导出 # ============================================================ @@ -1814,11 +2157,16 @@ __all__ = [ "QMessageBox", "QFileDialog", "QTimer", "QMenuBar", "QMenu", "QAction", "QStatusBar", "QColor", "QFont", + # 自绘控件 + "Canvas", "Graph3D", "ChartView", + "clipboard_set_text", "clipboard_text", # 类型 "QtObjectPtr", "QtStringPtr", "QtByteArrayPtr", "QtWidgetPtr", "QtAppPtr", "QtLabelPtr", "QtPushButtonPtr", "QtTreeWidgetPtr", "QtTreeWidgetItemPtr", "QtColorPtr", "QtFontPtr", "QtPixmapPtr", "QtImagePtr", "QtPainterPtr", + "QtMenuBarPtr", "QtMenuPtr", "QtActionPtr", "QtStatusBarPtr", + "QtCanvasPtr", "QtGraph3DPtr", "QtChartViewPtr", "QtChartSeriesPtr", "QtTcpSocketPtr", "QtHostAddressPtr", "QtNetworkRequestPtr", "QtNetworkReplyPtr", # 常量 "QT_KEY_ESCAPE", "QT_KEY_RETURN", "QT_KEY_A", "QT_KEY_Z", diff --git a/vqt6/_bridge.h b/vqt6/_bridge.h index 211ac1e..f1169bf 100644 --- a/vqt6/_bridge.h +++ b/vqt6/_bridge.h @@ -153,6 +153,13 @@ VQT6_API void vqt6_signal_unregister(int slot_id); // ============================================================ VQT6_API void vqt6_qtimer_connect_timeout(void* timer, vqt6_slot_callback_t cb, void* user_data); +// ============================================================ +// 剪贴板(复制链接等) +// ============================================================ +VQT6_API void vqt6_clipboard_set_text_utf8(const char* utf8); +// 读取剪贴板文本到 buf(UTF-8); 返回写入字节数(不含结尾 NUL) +VQT6_API int vqt6_clipboard_text_utf8(char* buf, int buf_size); + #ifdef __cplusplus } #endif diff --git a/vqt6/_bridge_canvas.cpp b/vqt6/_bridge_canvas.cpp new file mode 100644 index 0000000..8642b4b --- /dev/null +++ b/vqt6/_bridge_canvas.cpp @@ -0,0 +1,1192 @@ +// ============================================================ +// vqt6/_bridge_canvas.cpp — 自绘控件桥接实现 +// +// * Vqt6Canvas 手绘板 / 28x28 灰度显示 +// * Vqt6Graph3D 软件 3D 神经网络结构图(透视投影 + 画家算法排序) +// * ChartView Qt Charts 折线图 +// +// 三个控件都不使用 Q_OBJECT / 信号, 回调统一经 vqt6_signal_register +// 拿到 slot_id, 事件发生时 vqt6_trigger_slot(slot_id) 回调 Viper。 +// ============================================================ + +#include "_bridge.h" +#include "_bridge_canvas.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// ============================================================ +// 公共小工具 +// ============================================================ +namespace { + +inline double ClampD(double v, double lo, double hi) { + return v < lo ? lo : (v > hi ? hi : v); +} + +// 自绘控件统一的方形绘制区(居中): 返回边长与左上角偏移 +inline double SquareSide(int w, int h, double* ox, double* oy) { + double side = (w < h) ? static_cast(w) : static_cast(h); + *ox = (static_cast(w) - side) * 0.5; + *oy = (static_cast(h) - side) * 0.5; + return side; +} + +} // namespace + +// ============================================================ +// Vqt6Canvas — 手绘板 / 28x28 灰度显示 +// ============================================================ +namespace { + +class Vqt6Canvas : public QWidget { +public: + explicit Vqt6Canvas(QWidget* parent = nullptr) : QWidget(parent) { + setMinimumSize(200, 200); + setAttribute(Qt::WA_OpaquePaintEvent, true); + } + + void Clear() { + strokes_.clear(); + mode_ = kModeDraw; + update(); + } + bool HasInk() const { return !strokes_.empty(); } + + void SetMode(int m) { mode_ = (m != 0) ? kModeShow : kModeDraw; update(); } + int Mode() const { return mode_; } + + void SetPen(const QColor& c, double width_norm) { + penColor_ = c; + penWidth_ = ClampD(width_norm, 0.005, 0.4); + update(); + } + void SetGrid(bool on) { grid_ = on; update(); } + void SetScanLine(int y_norm) { scanY_ = y_norm; update(); } + void SetBg(const QColor& c) { bg_ = c; update(); } + + // 手绘 → 28x28 灰度(MNIST 预处理); 返回是否有墨迹 + int ToMnist28(unsigned char* out) { + if (out == nullptr || strokes_.empty()) return 0; + const int kSuper = 4; // 超采样倍数 + const int kSide = kMnist * kSuper; // 112 + QImage hi(kSide, kSide, QImage::Format_Grayscale8); + hi.fill(0); + { + QPainter p(&hi); + p.setRenderHint(QPainter::Antialiasing, true); + QPen pen(QColor(255, 255, 255)); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + double pw = penWidth_ * kSide; + if (pw < kSuper) pw = kSuper; + pen.setWidthF(pw); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + for (size_t s = 0; s < strokes_.size(); ++s) { + const std::vector& st = strokes_[s]; + if (st.empty()) continue; + if (st.size() == 1) { + p.drawPoint(QPointF(st[0].x() * kSide, st[0].y() * kSide)); + continue; + } + QPainterPath path(QPointF(st[0].x() * kSide, st[0].y() * kSide)); + for (size_t i = 1; i < st.size(); ++i) { + path.lineTo(QPointF(st[i].x() * kSide, st[i].y() * kSide)); + } + p.drawPath(path); + } + } + // 4x4 盒式降采样 + double raw[kMnist][kMnist]; + double maxv = 0.0; + for (int y = 0; y < kMnist; ++y) { + for (int x = 0; x < kMnist; ++x) { + double acc = 0.0; + for (int dy = 0; dy < kSuper; ++dy) { + for (int dx = 0; dx < kSuper; ++dx) { + QRgb px = hi.pixel(x * kSuper + dx, y * kSuper + dy); + acc += static_cast(qGray(px)); + } + } + double v = acc / (kSuper * kSuper); + raw[y][x] = v; + if (v > maxv) maxv = v; + } + } + if (maxv <= 0.0) return 0; + for (int y = 0; y < kMnist; ++y) { + for (int x = 0; x < kMnist; ++x) raw[y][x] /= maxv; + } + // 包围盒 + int minX = kMnist, maxX = -1, minY = kMnist, maxY = -1; + for (int y = 0; y < kMnist; ++y) { + for (int x = 0; x < kMnist; ++x) { + if (raw[y][x] > 0.02) { + if (x < minX) minX = x; + if (x > maxX) maxX = x; + if (y < minY) minY = y; + if (y > maxY) maxY = y; + } + } + } + if (maxX < minX || maxY < minY) return 0; + // 包围盒长边缩放到 20x20(对齐 MNIST 官方预处理) + int bw = maxX - minX + 1; + int bh = maxY - minY + 1; + double scale = 20.0 / static_cast(bw > bh ? bw : bh); + double cx0 = (minX + maxX) * 0.5; + double cy0 = (minY + maxY) * 0.5; + double tmp[kMnist][kMnist]; + std::memset(tmp, 0, sizeof(tmp)); + for (int y = minY; y <= maxY; ++y) { + for (int x = minX; x <= maxX; ++x) { + double v = raw[y][x]; + if (v <= 0.0) continue; + int tx = static_cast(std::lround((x - cx0) * scale + 14.0)); + int ty = static_cast(std::lround((y - cy0) * scale + 14.0)); + if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue; + if (v > tmp[ty][tx]) tmp[ty][tx] = v; + } + } + // 质心居中 + double sum = 0.0, mx = 0.0, my = 0.0; + for (int y = 0; y < kMnist; ++y) { + for (int x = 0; x < kMnist; ++x) { + sum += tmp[y][x]; + mx += tmp[y][x] * x; + my += tmp[y][x] * y; + } + } + if (sum <= 0.0) return 0; + mx /= sum; + my /= sum; + int shiftX = static_cast(std::lround(14.0 - mx)); + int shiftY = static_cast(std::lround(14.0 - my)); + unsigned char dst[kMnist][kMnist]; + std::memset(dst, 0, sizeof(dst)); + for (int y = 0; y < kMnist; ++y) { + for (int x = 0; x < kMnist; ++x) { + double v = tmp[y][x]; + if (v <= 0.0) continue; + int tx = x + shiftX; + int ty = y + shiftY; + if (tx < 0 || tx >= kMnist || ty < 0 || ty >= kMnist) continue; + int g = static_cast(std::lround(v * 255.0)); + dst[ty][tx] = static_cast(g < 0 ? 0 : (g > 255 ? 255 : g)); + } + } + std::memcpy(out, dst, kMnist * kMnist); + return 1; + } + + void SetImage28(const unsigned char* in) { + if (in == nullptr) return; + QImage img(kMnist, kMnist, QImage::Format_Grayscale8); + std::memcpy(img.bits(), in, kMnist * kMnist); + image_ = img; + mode_ = kModeShow; + update(); + } + + int slotId = -1; + vqt6_slot_callback_t changedCb = nullptr; + void* userData = nullptr; + +protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.fillRect(rect(), bg_); + double ox = 0.0, oy = 0.0; + double side = SquareSide(width(), height(), &ox, &oy); + if (side <= 1.0) return; + + p.setRenderHint(QPainter::Antialiasing, true); + + if (mode_ == kModeShow && !image_.isNull()) { + // 图像显示: 最近邻放大, 保持像素感 + p.setRenderHint(QPainter::SmoothPixmapTransform, false); + QRectF dstRect(ox, oy, side, side); + p.drawImage(dstRect, image_); + } else { + // 手绘: 归一化坐标 → 方形绘制区 + QPen pen(penColor_); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + pen.setWidthF(penWidth_ * side); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + for (size_t s = 0; s < strokes_.size(); ++s) { + const std::vector& st = strokes_[s]; + if (st.empty()) continue; + if (st.size() == 1) { + p.drawPoint(QPointF(ox + st[0].x() * side, oy + st[0].y() * side)); + continue; + } + QPainterPath path(QPointF(ox + st[0].x() * side, oy + st[0].y() * side)); + for (size_t i = 1; i < st.size(); ++i) { + path.lineTo(QPointF(ox + st[i].x() * side, oy + st[i].y() * side)); + } + p.drawPath(path); + } + } + + // 28x28 辅助网格 + if (grid_) { + QPen gp(QColor(90, 100, 120, 90)); + gp.setWidthF(1.0); + p.setPen(gp); + for (int i = 1; i < kMnist; ++i) { + double t = static_cast(i) / kMnist; + p.drawLine(QPointF(ox + t * side, oy), QPointF(ox + t * side, oy + side)); + p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side)); + } + } + + // 扫描线(推理动画) + if (scanY_ >= 0) { + double t = ClampD(static_cast(scanY_) / 1000.0, 0.0, 1.0); + QPen sp(QColor(255, 214, 102, 220)); + sp.setWidthF(2.0); + p.setPen(sp); + p.drawLine(QPointF(ox, oy + t * side), QPointF(ox + side, oy + t * side)); + } + + // 边框 + QPen bp(QColor(70, 78, 92)); + bp.setWidthF(1.0); + p.setPen(bp); + p.setBrush(Qt::NoBrush); + p.drawRect(QRectF(ox, oy, side, side)); + } + + void mousePressEvent(QMouseEvent* e) override { + if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return; + strokes_.push_back(std::vector()); + AppendPoint(e->position()); + } + + void mouseMoveEvent(QMouseEvent* e) override { + if (mode_ != kModeDraw) return; + if (!(e->buttons() & Qt::LeftButton)) return; + AppendPoint(e->position()); + } + + void mouseReleaseEvent(QMouseEvent* e) override { + if (mode_ != kModeDraw || e->button() != Qt::LeftButton) return; + if (slotId >= 0) vqt6_trigger_slot(slotId); + } + +private: + void AppendPoint(const QPointF& pos) { + if (strokes_.empty()) return; + double ox = 0.0, oy = 0.0; + double side = SquareSide(width(), height(), &ox, &oy); + if (side <= 1.0) return; + double nx = ClampD((pos.x() - ox) / side, 0.0, 1.0); + double ny = ClampD((pos.y() - oy) / side, 0.0, 1.0); + strokes_.back().push_back(QPointF(nx, ny)); + update(); + } + + static const int kModeDraw = 0; + static const int kModeShow = 1; + static const int kMnist = 28; + + std::vector > strokes_; + QImage image_; + QColor penColor_ = QColor(255, 255, 255); + QColor bg_ = QColor(12, 14, 18); + double penWidth_ = 0.07; + bool grid_ = false; + int scanY_ = -1; + int mode_ = kModeDraw; +}; + +} // namespace + +extern "C" VQT6_API void* vqt6_canvas_create(void) { + return new Vqt6Canvas(); +} + +extern "C" VQT6_API void* vqt6_canvas_create_with_parent(void* parent) { + if (parent == nullptr) return nullptr; + return new Vqt6Canvas(static_cast(parent)); +} + +extern "C" VQT6_API void vqt6_canvas_clear(void* canvas) { + if (canvas == nullptr) return; + static_cast(canvas)->Clear(); +} + +extern "C" VQT6_API int vqt6_canvas_has_ink(void* canvas) { + if (canvas == nullptr) return 0; + return static_cast(canvas)->HasInk() ? 1 : 0; +} + +extern "C" VQT6_API void vqt6_canvas_set_mode(void* canvas, int mode) { + if (canvas == nullptr) return; + static_cast(canvas)->SetMode(mode); +} + +extern "C" VQT6_API int vqt6_canvas_mode(void* canvas) { + if (canvas == nullptr) return 0; + return static_cast(canvas)->Mode(); +} + +extern "C" VQT6_API void vqt6_canvas_set_pen(void* canvas, int r, int g, int b, double width_norm) { + if (canvas == nullptr) return; + static_cast(canvas)->SetPen(QColor(r, g, b), width_norm); +} + +extern "C" VQT6_API void vqt6_canvas_set_grid(void* canvas, int on) { + if (canvas == nullptr) return; + static_cast(canvas)->SetGrid(on != 0); +} + +extern "C" VQT6_API void vqt6_canvas_set_scan_line(void* canvas, int y_norm) { + if (canvas == nullptr) return; + static_cast(canvas)->SetScanLine(y_norm); +} + +extern "C" VQT6_API void vqt6_canvas_set_bg(void* canvas, int r, int g, int b) { + if (canvas == nullptr) return; + static_cast(canvas)->SetBg(QColor(r, g, b)); +} + +extern "C" VQT6_API int vqt6_canvas_to_mnist28(void* canvas, unsigned char* out) { + if (canvas == nullptr) return 0; + return static_cast(canvas)->ToMnist28(out); +} + +extern "C" VQT6_API void vqt6_canvas_set_image28(void* canvas, const unsigned char* in) { + if (canvas == nullptr) return; + static_cast(canvas)->SetImage28(in); +} + +extern "C" VQT6_API void vqt6_canvas_connect_changed(void* canvas, vqt6_slot_callback_t cb, void* user_data) { + if (canvas == nullptr || cb == nullptr) return; + Vqt6Canvas* c = static_cast(canvas); + c->changedCb = cb; + c->userData = user_data; + c->slotId = vqt6_signal_register(cb, user_data); +} + +// ============================================================ +// Vqt6Graph3D — 软件 3D 神经网络结构图 +// ============================================================ +namespace { + +struct GraphNode { + double x, y, z; + int layer; + double value; +}; + +struct GraphEdge { + int a, b; + double w; + double norm; // |w| / max|w| +}; + +struct GraphProj { + double sx, sy; + double depth; // 相机距离, 越大越远 + double scale; + bool ok; +}; + +class Vqt6Graph3D : public QWidget { +public: + explicit Vqt6Graph3D(QWidget* parent = nullptr) : QWidget(parent) { + setMinimumSize(320, 240); + setAttribute(Qt::WA_OpaquePaintEvent, true); + setMouseTracking(true); + } + + void Setup(const int* sizes, int n_layers) { + nodes_.clear(); + edges_.clear(); + layerStart_.clear(); + layerCount_.clear(); + layerAlpha_.clear(); + edgeDraw_.clear(); + if (sizes == nullptr || n_layers <= 0) { update(); return; } + + // 各层沿 Z 轴等距排布(输入层在最远端) + for (int li = 0; li < n_layers; ++li) { + int cnt = sizes[li]; + if (cnt < 0) cnt = 0; + layerStart_.push_back(static_cast(nodes_.size())); + layerCount_.push_back(cnt); + layerAlpha_.push_back(1.0); + double z = (n_layers > 1) + ? (-1.1 + 2.2 * static_cast(li) / static_cast(n_layers - 1)) + : 0.0; + // 近正方形网格布局 + int cols = static_cast(std::ceil(std::sqrt(static_cast(cnt > 0 ? cnt : 1)))); + if (cols < 1) cols = 1; + int rows = (cnt + cols - 1) / cols; + if (rows < 1) rows = 1; + double spacing = 1.6 / static_cast(cols > rows ? cols : rows); + for (int i = 0; i < cnt; ++i) { + int cx = i % cols; + int cy = i / cols; + GraphNode n; + n.x = (static_cast(cx) - (cols - 1) * 0.5) * spacing; + n.y = (static_cast(cy) - (rows - 1) * 0.5) * spacing; + n.z = z; + n.layer = li; + n.value = 0.0; + nodes_.push_back(n); + } + } + // 相邻层前向全连接 + for (int li = 0; li + 1 < n_layers; ++li) { + int aStart = layerStart_[li], aCnt = layerCount_[li]; + int bStart = layerStart_[li + 1], bCnt = layerCount_[li + 1]; + for (int i = 0; i < aCnt; ++i) { + for (int j = 0; j < bCnt; ++j) { + GraphEdge e; + e.a = aStart + i; + e.b = bStart + j; + e.w = 0.0; + e.norm = 0.0; + edges_.push_back(e); + } + } + } + RebuildEdgeDraw(); + update(); + } + + void Clear() { + nodes_.clear(); + edges_.clear(); + layerStart_.clear(); + layerCount_.clear(); + layerAlpha_.clear(); + edgeDraw_.clear(); + update(); + } + + int NodeCount() const { return static_cast(nodes_.size()); } + int EdgeCount() const { return static_cast(edges_.size()); } + + void SetLayerAlpha(int layer, double a) { + if (layer < 0 || layer >= static_cast(layerAlpha_.size())) return; + layerAlpha_[layer] = ClampD(a, 0.0, 1.0); + update(); + } + + void SetAllLayerAlpha(double a) { + for (size_t i = 0; i < layerAlpha_.size(); ++i) layerAlpha_[i] = ClampD(a, 0.0, 1.0); + update(); + } + + void SetValues(const double* v, int n) { + if (v == nullptr) return; + int cnt = (n < static_cast(nodes_.size())) ? n : static_cast(nodes_.size()); + for (int i = 0; i < cnt; ++i) nodes_[i].value = ClampD(v[i], 0.0, 1.0); + update(); + } + + void SetEdgeWeights(const double* w, int n) { + if (w == nullptr) return; + int cnt = (n < static_cast(edges_.size())) ? n : static_cast(edges_.size()); + double maxAbs = 0.0; + for (int i = 0; i < cnt; ++i) { + edges_[i].w = w[i]; + double a = std::fabs(w[i]); + if (a > maxAbs) maxAbs = a; + } + for (int i = 0; i < cnt; ++i) { + edges_[i].norm = (maxAbs > 0.0) ? (std::fabs(edges_[i].w) / maxAbs) : 0.0; + } + RebuildEdgeDraw(); + update(); + } + + void SetEdgeThreshold(double t) { + edgeThreshold_ = ClampD(t, 0.0, 1.0); + RebuildEdgeDraw(); + update(); + } + + void SetEdgeBudget(int k) { + edgeBudget_ = (k > 0) ? k : 0; + RebuildEdgeDraw(); + update(); + } + + void SetView(double yaw, double pitch, double dist) { + yaw_ = yaw; + pitch_ = ClampD(pitch, -85.0, 85.0); + dist_ = ClampD(dist, 0.6, 20.0); + update(); + } + + void GetView(double* yaw, double* pitch, double* dist) const { + if (yaw != nullptr) *yaw = yaw_; + if (pitch != nullptr) *pitch = pitch_; + if (dist != nullptr) *dist = dist_; + } + + void ResetView() { + yaw_ = -28.0; + pitch_ = 16.0; + dist_ = 3.4; + update(); + } + + void SetBg(const QColor& c) { bg_ = c; update(); } + void SetNodeScale(double s) { nodeScale_ = ClampD(s, 0.1, 5.0); update(); } + + int slotId = -1; + vqt6_slot_callback_t viewCb = nullptr; + void* userData = nullptr; + +protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.fillRect(rect(), bg_); + const int w = width(); + const int h = height(); + if (w < 8 || h < 8) return; + p.setRenderHint(QPainter::Antialiasing, true); + + if (nodes_.empty()) { + p.setPen(QColor(120, 130, 148)); + p.drawText(rect(), Qt::AlignCenter, QStringLiteral("尚无网络结构")); + return; + } + + const double px = (w < h ? w : h) * 0.34; // 单位长度 → 像素 + const double cx = w * 0.5; + const double cy = h * 0.5; + const double focal = 1.6; + + const double ry = yaw_ * kPi / 180.0; + const double rx = pitch_ * kPi / 180.0; + const double sy = std::sin(ry), cyw = std::cos(ry); + const double sx = std::sin(rx), cxw = std::cos(rx); + + // 投影全部节点 + std::vector proj(nodes_.size()); + for (size_t i = 0; i < nodes_.size(); ++i) { + const GraphNode& n = nodes_[i]; + double x1 = n.x * cyw + n.z * sy; + double z1 = -n.x * sy + n.z * cyw; + double y2 = n.y * cxw - z1 * sx; + double z2 = n.y * sx + z1 * cxw; + double depth = dist_ - z2; + GraphProj gp; + if (depth < 0.15) { + gp.ok = false; + gp.sx = gp.sy = 0.0; + gp.depth = 1e9; + gp.scale = 0.0; + } else { + double sc = focal / depth; + gp.ok = true; + gp.sx = cx + x1 * sc * px; + gp.sy = cy - y2 * sc * px; + gp.depth = depth; + gp.scale = sc; + } + proj[i] = gp; + } + + // ---- 边: 先画远的 ---- + std::vector eorder(edgeDraw_.begin(), edgeDraw_.end()); + std::sort(eorder.begin(), eorder.end(), [&](int a, int b) { + const GraphEdge& ea = edges_[a]; + const GraphEdge& eb = edges_[b]; + double da = proj[ea.a].depth + proj[ea.b].depth; + double db = proj[eb.a].depth + proj[eb.b].depth; + return da > db; + }); + p.setBrush(Qt::NoBrush); + for (size_t k = 0; k < eorder.size(); ++k) { + const GraphEdge& e = edges_[eorder[k]]; + const GraphProj& pa = proj[e.a]; + const GraphProj& pb = proj[e.b]; + if (!pa.ok || !pb.ok) continue; + double la = LayerAlphaOf(e.a); + double lb = LayerAlphaOf(e.b); + double la2 = (la < lb) ? la : lb; + if (la2 <= 0.01) continue; + double depthFall = ClampD(1.35 - (pa.depth + pb.depth) * 0.5 / dist_, 0.15, 1.0); + int alpha = static_cast(std::lround((0.08 + 0.42 * e.norm) * la2 * depthFall * 255.0)); + if (alpha < 4) continue; + QColor col = (e.w >= 0.0) ? QColor(255, 107, 107, alpha) : QColor(77, 171, 247, alpha); + QPen pen(col); + pen.setWidthF(1.0); + p.setPen(pen); + p.drawLine(QPointF(pa.sx, pa.sy), QPointF(pb.sx, pb.sy)); + } + + // ---- 节点: 由远及近 ---- + std::vector norder(nodes_.size()); + for (size_t i = 0; i < norder.size(); ++i) norder[i] = static_cast(i); + std::sort(norder.begin(), norder.end(), [&](int a, int b) { + return proj[a].depth > proj[b].depth; + }); + for (size_t k = 0; k < norder.size(); ++k) { + int idx = norder[k]; + const GraphNode& n = nodes_[idx]; + const GraphProj& gp = proj[idx]; + if (!gp.ok) continue; + double la = LayerAlphaOf(idx); + if (la <= 0.01) continue; + double r = 0.020 * px * nodeScale_ * (0.55 + 0.95 * n.value) * gp.scale; + if (r < 1.2) r = 1.2; + QColor base = LayerColor(n.layer); + double depthFall = ClampD(1.3 - gp.depth / dist_, 0.25, 1.0); + int a = static_cast(std::lround((0.28 + 0.72 * n.value) * la * depthFall * 255.0)); + QColor col(base.red(), base.green(), base.blue(), a < 0 ? 0 : (a > 255 ? 255 : a)); + p.setPen(Qt::NoPen); + p.setBrush(col); + p.drawEllipse(QPointF(gp.sx, gp.sy), r, r); + } + + // ---- 输出层数字标签 ---- + int last = static_cast(layerCount_.size()) - 1; + if (last >= 0 && LayerAlphaOf(layerStart_[last]) > 0.25) { + int a = static_cast(std::lround(LayerAlphaOf(layerStart_[last]) * 255.0)); + QFont f = p.font(); + f.setPointSizeF(9.0); + f.setBold(true); + p.setFont(f); + int base = layerStart_[last]; + int cnt = layerCount_[last]; + for (int i = 0; i < cnt; ++i) { + const GraphProj& gp = proj[base + i]; + if (!gp.ok) continue; + p.setPen(QColor(255, 214, 102, a)); + p.drawText(QPointF(gp.sx + 7.0, gp.sy + 4.0), QString::number(i)); + } + } + + // ---- 层标题 ---- + const char* kNames[] = { "输入", "隐藏", "输出" }; + QFont sf = p.font(); + sf.setPointSizeF(8.5); + p.setFont(sf); + for (size_t li = 0; li < layerCount_.size(); ++li) { + double z = LayerZ(static_cast(li)); + double x1 = 0.0 * cyw + z * sy; + double z1 = -0.0 * sy + z * cyw; + double y2 = (-0.95) * cxw - z1 * sx; + double z2 = (-0.95) * sx + z1 * cxw; + double depth = dist_ - z2; + if (depth < 0.15) continue; + double sc = focal / depth; + double txx = cx + x1 * sc * px; + double tyy = cy - y2 * sc * px; + int a = static_cast(std::lround(LayerAlphaOf(layerStart_[li]) * 210.0)); + if (a < 10) a = 10; + QString cap; + if (li < 3) cap = QString::fromUtf8(kNames[li]); + else cap = QStringLiteral("层"); + cap += QStringLiteral(" %1").arg(layerCount_[li]); + p.setPen(QColor(150, 160, 180, a)); + p.drawText(QPointF(txx - 22.0, tyy), cap); + } + } + + void mousePressEvent(QMouseEvent* e) override { + if (e->button() != Qt::LeftButton) return; + dragging_ = true; + lastPos_ = e->position(); + } + + void mouseMoveEvent(QMouseEvent* e) override { + if (!dragging_) return; + QPointF d = e->position() - lastPos_; + lastPos_ = e->position(); + yaw_ += d.x() * 0.45; + pitch_ = ClampD(pitch_ + d.y() * 0.35, -85.0, 85.0); + update(); + if (slotId >= 0) vqt6_trigger_slot(slotId); + } + + void mouseReleaseEvent(QMouseEvent* e) override { + if (e->button() != Qt::LeftButton) return; + dragging_ = false; + if (slotId >= 0) vqt6_trigger_slot(slotId); + } + + void wheelEvent(QWheelEvent* e) override { + double steps = e->angleDelta().y() / 120.0; + dist_ = ClampD(dist_ * std::pow(1.12, -steps), 0.6, 20.0); + update(); + if (slotId >= 0) vqt6_trigger_slot(slotId); + } + +private: + static constexpr double kPi = 3.14159265358979323846; + + double LayerZ(int li) const { + int n = static_cast(layerCount_.size()); + if (n <= 1) return 0.0; + return -1.1 + 2.2 * static_cast(li) / static_cast(n - 1); + } + + double LayerAlphaOf(int nodeIndex) const { + if (nodeIndex < 0 || nodeIndex >= static_cast(nodes_.size())) return 1.0; + int li = nodes_[nodeIndex].layer; + if (li < 0 || li >= static_cast(layerAlpha_.size())) return 1.0; + return layerAlpha_[li]; + } + + QColor LayerColor(int layer) const { + if (layer == 0) return QColor(200, 210, 225); // 输入层: 冷白 + int last = static_cast(layerCount_.size()) - 1; + if (layer == last) return QColor(255, 190, 70); // 输出层: 琥珀 + return QColor(90, 200, 190); // 隐藏层: 青 + } + + // 按阈值 + 预算重建待画边索引(|w| 大的优先) + void RebuildEdgeDraw() { + edgeDraw_.clear(); + std::vector cand; + for (size_t i = 0; i < edges_.size(); ++i) { + if (edges_[i].norm >= edgeThreshold_ && edges_[i].norm > 0.0) { + cand.push_back(static_cast(i)); + } + } + if (edgeBudget_ > 0 && static_cast(cand.size()) > edgeBudget_) { + std::partial_sort(cand.begin(), cand.begin() + edgeBudget_, cand.end(), + [&](int a, int b) { return edges_[a].norm > edges_[b].norm; }); + cand.resize(static_cast(edgeBudget_)); + } + edgeDraw_ = cand; + } + + std::vector nodes_; + std::vector edges_; + std::vector layerStart_; + std::vector layerCount_; + std::vector layerAlpha_; + std::vector edgeDraw_; + double edgeThreshold_ = 0.0; + int edgeBudget_ = 3000; + double yaw_ = -28.0; + double pitch_ = 16.0; + double dist_ = 3.4; + double nodeScale_ = 1.0; + QColor bg_ = QColor(14, 16, 22); + bool dragging_ = false; + QPointF lastPos_; +}; + +} // namespace + +extern "C" VQT6_API void* vqt6_graph3d_create(void) { + return new Vqt6Graph3D(); +} + +extern "C" VQT6_API void* vqt6_graph3d_create_with_parent(void* parent) { + if (parent == nullptr) return nullptr; + return new Vqt6Graph3D(static_cast(parent)); +} + +extern "C" VQT6_API void vqt6_graph3d_setup(void* g, const int* layer_sizes, int n_layers) { + if (g == nullptr) return; + static_cast(g)->Setup(layer_sizes, n_layers); +} + +extern "C" VQT6_API void vqt6_graph3d_clear(void* g) { + if (g == nullptr) return; + static_cast(g)->Clear(); +} + +extern "C" VQT6_API int vqt6_graph3d_node_count(void* g) { + if (g == nullptr) return 0; + return static_cast(g)->NodeCount(); +} + +extern "C" VQT6_API int vqt6_graph3d_edge_count(void* g) { + if (g == nullptr) return 0; + return static_cast(g)->EdgeCount(); +} + +extern "C" VQT6_API void vqt6_graph3d_set_layer_alpha(void* g, int layer, double alpha) { + if (g == nullptr) return; + static_cast(g)->SetLayerAlpha(layer, alpha); +} + +extern "C" VQT6_API void vqt6_graph3d_set_all_layer_alpha(void* g, double alpha) { + if (g == nullptr) return; + static_cast(g)->SetAllLayerAlpha(alpha); +} + +extern "C" VQT6_API void vqt6_graph3d_set_values(void* g, const double* values, int n) { + if (g == nullptr) return; + static_cast(g)->SetValues(values, n); +} + +extern "C" VQT6_API void vqt6_graph3d_set_edge_weights(void* g, const double* w, int n) { + if (g == nullptr) return; + static_cast(g)->SetEdgeWeights(w, n); +} + +extern "C" VQT6_API void vqt6_graph3d_set_edge_threshold(void* g, double t) { + if (g == nullptr) return; + static_cast(g)->SetEdgeThreshold(t); +} + +extern "C" VQT6_API void vqt6_graph3d_set_edge_budget(void* g, int max_edges) { + if (g == nullptr) return; + static_cast(g)->SetEdgeBudget(max_edges); +} + +extern "C" VQT6_API void vqt6_graph3d_set_view(void* g, double yaw_deg, double pitch_deg, double dist) { + if (g == nullptr) return; + static_cast(g)->SetView(yaw_deg, pitch_deg, dist); +} + +extern "C" VQT6_API void vqt6_graph3d_get_view(void* g, double* yaw_deg, double* pitch_deg, double* dist) { + if (g == nullptr) return; + static_cast(g)->GetView(yaw_deg, pitch_deg, dist); +} + +extern "C" VQT6_API void vqt6_graph3d_set_bg(void* g, int r, int gcol, int b) { + if (g == nullptr) return; + static_cast(g)->SetBg(QColor(r, gcol, b)); +} + +extern "C" VQT6_API void vqt6_graph3d_set_node_scale(void* g, double s) { + if (g == nullptr) return; + static_cast(g)->SetNodeScale(s); +} + +extern "C" VQT6_API void vqt6_graph3d_connect_view_changed(void* g, vqt6_slot_callback_t cb, void* user_data) { + if (g == nullptr || cb == nullptr) return; + Vqt6Graph3D* gp = static_cast(g); + gp->viewCb = cb; + gp->userData = user_data; + gp->slotId = vqt6_signal_register(cb, user_data); +} + +extern "C" VQT6_API void vqt6_graph3d_reset_view(void* g) { + if (g == nullptr) return; + static_cast(g)->ResetView(); +} + +// ============================================================ +// ChartView — Qt Charts 折线图 +// ============================================================ +namespace { + +struct ChartAxes { + QValueAxis* x; + QValueAxis* yLeft; + QValueAxis* yRight; +}; + +QHash& ChartAxesMap() { + static QHash m; + return m; +} + +QHash& SeriesSideMap() { + static QHash m; + return m; +} + +// 每条曲线的滑动窗口上限(0 = 不限制), 由 vqt6_chartview_set_max_points 设置 +QHash& MaxPointsMap() { + static QHash m; + return m; +} + +// 放大窗: 每个 ChartView 至多一个, 建一次后复用 +// (不设 WA_DeleteOnClose —— QChartView 被 delete 时 Qt Charts 的销毁路径很脆, +// 关闭大窗只要隐藏即可, 下次点"查看大图"直接复用) +struct ZoomView { + QMainWindow* win; + QChartView* view; + QChart* chart; + QValueAxis* x; + QValueAxis* yLeft; + QValueAxis* yRight; + QList series; // 与源图 series 顺序一一对应 + ZoomView() : win(nullptr), view(nullptr), chart(nullptr), + x(nullptr), yLeft(nullptr), yRight(nullptr) {} +}; + +QHash& ZoomMap() { + static QHash m; + return m; +} + +// 把源图当前所有曲线与轴范围整体复制到放大窗(替换式, 不是追加) +void SyncZoomView(QChartView* src, ZoomView& z) { + if (z.chart == nullptr || z.win == nullptr) return; + if (!z.win->isVisible()) return; // 关着就不做无谓的拷贝 + const QList all = src->chart()->series(); + if (z.series.size() != all.size()) return; + for (int i = 0; i < all.size(); ++i) { + QLineSeries* ls = qobject_cast(all.at(i)); + if (ls == nullptr) continue; + z.series.at(i)->replace(ls->points()); + } + ChartAxes sa = ChartAxesMap().value(src); + if (sa.x != nullptr) z.x->setRange(sa.x->min(), sa.x->max()); + if (sa.yLeft != nullptr) z.yLeft->setRange(sa.yLeft->min(), sa.yLeft->max()); + if (sa.yRight != nullptr && sa.yRight->isVisible()) { + z.yRight->setRange(sa.yRight->min(), sa.yRight->max()); + z.yRight->setVisible(true); + } +} + +} // namespace + +extern "C" VQT6_API void* vqt6_chartview_create(void) { + QChart* chart = new QChart(); + QChartView* view = new QChartView(chart); + view->setRenderHint(QPainter::Antialiasing, true); + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignBottom); + chart->setMargins(QMargins(4, 4, 4, 4)); + chart->setBackgroundRoundness(0); + + ChartAxes ax; + ax.x = new QValueAxis(); + ax.x->setTitleText(QStringLiteral("step")); + ax.yLeft = new QValueAxis(); + ax.yRight = new QValueAxis(); + ax.yRight->setVisible(false); + chart->addAxis(ax.x, Qt::AlignBottom); + chart->addAxis(ax.yLeft, Qt::AlignLeft); + chart->addAxis(ax.yRight, Qt::AlignRight); + ChartAxesMap().insert(view, ax); + return view; +} + +extern "C" VQT6_API void* vqt6_chartview_create_with_parent(void* parent) { + if (parent == nullptr) return nullptr; + void* view = vqt6_chartview_create(); + static_cast(view)->setParent(static_cast(parent)); + return view; +} + +extern "C" VQT6_API void* vqt6_chartview_add_line(void* view, const char* name, + int r, int g, int b, int right_axis) { + if (view == nullptr) return nullptr; + QChartView* cv = static_cast(view); + ChartAxes axes = ChartAxesMap().value(cv); + if (axes.x == nullptr) return nullptr; + QLineSeries* series = new QLineSeries(); + if (name != nullptr) series->setName(QString::fromUtf8(name)); + QPen pen(QColor(r, g, b)); + pen.setWidthF(2.0); + series->setPen(pen); + cv->chart()->addSeries(series); + series->attachAxis(axes.x); + if (right_axis != 0) { + series->attachAxis(axes.yRight); + axes.yRight->setVisible(true); + } else { + series->attachAxis(axes.yLeft); + } + SeriesSideMap().insert(series, right_axis != 0 ? 1 : 0); + return series; +} + +extern "C" VQT6_API void vqt6_chartview_append(void* view, void* series, double x, double y) { + if (view == nullptr || series == nullptr) return; + QLineSeries* ls = static_cast(series); + ls->append(x, y); + // 滑动窗口: 超出上限就丢掉最旧的点, 避免长时间训练后曲线挤成一团 + const int maxp = MaxPointsMap().value(static_cast(view), 0); + if (maxp > 0) { + const int extra = ls->count() - maxp; + if (extra > 0) ls->removePoints(0, extra); + } +} + +extern "C" VQT6_API void vqt6_chartview_clear_series(void* view, void* series) { + if (series == nullptr) return; + static_cast(series)->clear(); +} + +extern "C" VQT6_API void vqt6_chartview_clear_all(void* view) { + if (view == nullptr) return; + QChartView* cv = static_cast(view); + const QList all = cv->chart()->series(); + for (int i = 0; i < all.size(); ++i) { + QLineSeries* ls = qobject_cast(all.at(i)); + if (ls != nullptr) ls->clear(); + } +} + +extern "C" VQT6_API void vqt6_chartview_autoscale(void* view) { + if (view == nullptr) return; + QChartView* cv = static_cast(view); + ChartAxes axes = ChartAxesMap().value(cv); + if (axes.x == nullptr) return; + const QList all = cv->chart()->series(); + double xmin = 1e300, xmax = -1e300; + double lmin = 1e300, lmax = -1e300; + double rmin = 1e300, rmax = -1e300; + bool hasRight = false; + for (int i = 0; i < all.size(); ++i) { + QLineSeries* ls = qobject_cast(all.at(i)); + if (ls == nullptr) continue; + int side = SeriesSideMap().value(ls, 0); + QVector pts = ls->points(); + for (int k = 0; k < pts.size(); ++k) { + double x = pts.at(k).x(); + double y = pts.at(k).y(); + if (x < xmin) xmin = x; + if (x > xmax) xmax = x; + if (side != 0) { + hasRight = true; + if (y < rmin) rmin = y; + if (y > rmax) rmax = y; + } else { + if (y < lmin) lmin = y; + if (y > lmax) lmax = y; + } + } + } + if (xmax < xmin) { xmin = 0.0; xmax = 1.0; } + if (xmax - xmin < 1e-9) xmax = xmin + 1.0; + axes.x->setRange(xmin, xmax); + if (lmax >= lmin) { + if (lmax - lmin < 1e-9) { lmin -= 0.5; lmax += 0.5; } + axes.yLeft->setRange(lmin, lmax); + } + if (hasRight && rmax >= rmin) { + if (rmax - rmin < 1e-9) { rmin -= 0.5; rmax += 0.5; } + axes.yRight->setRange(rmin, rmax); + axes.yRight->setVisible(true); + } +} + +extern "C" VQT6_API void vqt6_chartview_set_title(void* view, const char* utf8) { + if (view == nullptr) return; + QChartView* cv = static_cast(view); + cv->chart()->setTitle(utf8 != nullptr ? QString::fromUtf8(utf8) : QString()); +} + +extern "C" VQT6_API void vqt6_chartview_set_legend_visible(void* view, int visible) { + if (view == nullptr) return; + QChartView* cv = static_cast(view); + cv->chart()->legend()->setVisible(visible != 0); +} + +extern "C" VQT6_API void vqt6_chartview_set_background(void* view, int r, int g, int b) { + if (view == nullptr) return; + QChartView* cv = static_cast(view); + QColor c(r, g, b); + cv->chart()->setBackgroundVisible(true); + cv->chart()->setBackgroundBrush(c); + cv->chart()->setPlotAreaBackgroundVisible(true); + cv->chart()->setPlotAreaBackgroundBrush(c.darker(115)); +} + +extern "C" VQT6_API void vqt6_chartview_set_max_points(void* view, int max_points) { + if (view == nullptr) return; + MaxPointsMap().insert(static_cast(view), max_points); +} + +extern "C" VQT6_API void* vqt6_chartview_open_zoom(void* view, const char* title) { + if (view == nullptr) return nullptr; + QChartView* src = static_cast(view); + ZoomView& z = ZoomMap()[src]; + + if (z.win == nullptr) { + QChart* chart = new QChart(); + chart->legend()->setVisible(true); + chart->legend()->setAlignment(Qt::AlignBottom); + chart->setMargins(QMargins(6, 6, 6, 6)); + chart->setBackgroundRoundness(0); + chart->setBackgroundVisible(true); + chart->setBackgroundBrush(src->chart()->backgroundBrush()); + chart->setPlotAreaBackgroundVisible(true); + chart->setPlotAreaBackgroundBrush(src->chart()->plotAreaBackgroundBrush()); + + QValueAxis* ax = new QValueAxis(); + ax->setTitleText(QStringLiteral("step")); + QValueAxis* yLeft = new QValueAxis(); + QValueAxis* yRight = new QValueAxis(); + yRight->setVisible(false); + chart->addAxis(ax, Qt::AlignBottom); + chart->addAxis(yLeft, Qt::AlignLeft); + chart->addAxis(yRight, Qt::AlignRight); + + // 按源图的曲线条数建空曲线(同名/同色/同左右轴), 数据靠 SyncZoomView 拷贝 + const QList all = src->chart()->series(); + for (int i = 0; i < all.size(); ++i) { + QLineSeries* ls = qobject_cast(all.at(i)); + if (ls == nullptr) continue; + QLineSeries* cp = new QLineSeries(); + cp->setName(ls->name()); + cp->setPen(ls->pen()); + chart->addSeries(cp); + cp->attachAxis(ax); + if (SeriesSideMap().value(ls, 0) != 0) { + cp->attachAxis(yRight); + yRight->setVisible(true); + } else { + cp->attachAxis(yLeft); + } + z.series.append(cp); + } + + QChartView* zv = new QChartView(chart); + zv->setRenderHint(QPainter::Antialiasing, true); + + QMainWindow* win = new QMainWindow(); + win->setCentralWidget(zv); + win->resize(1100, 700); + + z.win = win; + z.view = zv; + z.chart = chart; + z.x = ax; + z.yLeft = yLeft; + z.yRight = yRight; + } + + z.win->setWindowTitle(title != nullptr ? QString::fromUtf8(title) + : QStringLiteral("训练曲线")); + // 先显示再同步: SyncZoomView 只在窗口可见时才拷贝, 首次打开也要有数据 + z.win->show(); + z.win->raise(); + z.win->activateWindow(); + SyncZoomView(src, z); + return z.win; +} + +extern "C" VQT6_API void vqt6_chartview_sync_zoom(void* view) { + if (view == nullptr) return; + QChartView* src = static_cast(view); + if (!ZoomMap().contains(src)) return; + SyncZoomView(src, ZoomMap()[src]); +} diff --git a/vqt6/_bridge_canvas.h b/vqt6/_bridge_canvas.h new file mode 100644 index 0000000..4de0e70 --- /dev/null +++ b/vqt6/_bridge_canvas.h @@ -0,0 +1,99 @@ +// ============================================================ +// vqt6/_bridge_canvas.h — 自绘控件 C 桥接声明 +// +// 三个自绘控件(不依赖 Q_OBJECT/moc, 回调直接走 vqt6_trigger_slot): +// * Canvas — 手绘板 / 28x28 灰度图显示(带扫描线、网格) +// * Graph3D — 软件 3D 神经网络结构图(QPainter 透视投影) +// * ChartView — Qt Charts 折线图(loss/acc 曲线) +// +// 设计约定: Graph3D 的 API 与渲染后端无关(只暴露 +// setup/set_values/set_edge_weights/set_layer_alpha/set_view), +// 将来若换成 OpenGL 后端, Viper 侧调用代码无需改动。 +// ============================================================ + +#ifndef VQT6_BRIDGE_CANVAS_H +#define VQT6_BRIDGE_CANVAS_H + +#include "_bridge.h" + +#ifdef __cplusplus +extern "C" { +#endif + +// ============================================================ +// Canvas — 手绘板 / 28x28 灰度显示 +// 坐标系约定: 内部统一用 0..1 归一化坐标, 与控件像素尺寸解耦 +// ============================================================ +VQT6_API void* vqt6_canvas_create(void); +VQT6_API void* vqt6_canvas_create_with_parent(void* parent); +VQT6_API void vqt6_canvas_clear(void* canvas); +VQT6_API int vqt6_canvas_has_ink(void* canvas); +VQT6_API void vqt6_canvas_set_mode(void* canvas, int mode); // 0=手绘 1=图像显示 +VQT6_API int vqt6_canvas_mode(void* canvas); +VQT6_API void vqt6_canvas_set_pen(void* canvas, int r, int g, int b, double width_norm); +VQT6_API void vqt6_canvas_set_grid(void* canvas, int on); // 28x28 辅助网格 +VQT6_API void vqt6_canvas_set_scan_line(void* canvas, int y_norm); // 0..1000, <0=关 +VQT6_API void vqt6_canvas_set_bg(void* canvas, int r, int g, int b); +// 手绘内容 → 28x28 灰度(MNIST 预处理: 包围盒缩放到 20x20 后按质心居中); 无墨迹返回 0 +VQT6_API int vqt6_canvas_to_mnist28(void* canvas, unsigned char* out); +// 28x28 灰度 → 显示(自动切到图像显示模式) +VQT6_API void vqt6_canvas_set_image28(void* canvas, const unsigned char* in); +// 笔迹完成(鼠标抬起)回调 +VQT6_API void vqt6_canvas_connect_changed(void* canvas, vqt6_slot_callback_t cb, void* user_data); + +// ============================================================ +// Graph3D — 软件 3D 神经网络结构图 +// 层布局: 每层按近正方形网格排在 XY 平面, 各层沿 Z 轴依次排列 +// ============================================================ +VQT6_API void* vqt6_graph3d_create(void); +VQT6_API void* vqt6_graph3d_create_with_parent(void* parent); +// layer_sizes: 自输入层到输出层各层节点数; 建立节点与前向全连接边 +VQT6_API void vqt6_graph3d_setup(void* g, const int* layer_sizes, int n_layers); +VQT6_API void vqt6_graph3d_clear(void* g); +VQT6_API int vqt6_graph3d_node_count(void* g); +VQT6_API int vqt6_graph3d_edge_count(void* g); +// 逐层点亮: alpha 0..1(节点与相邻边同时生效) +VQT6_API void vqt6_graph3d_set_layer_alpha(void* g, int layer, double alpha); +VQT6_API void vqt6_graph3d_set_all_layer_alpha(void* g, double alpha); +// 每节点激活值(长度 = node_count), 取值 0..1, 影响节点亮度与半径 +VQT6_API void vqt6_graph3d_set_values(void* g, const double* values, int n); +// 边权重(长度 = edge_count, 按层间顺序), 影响颜色/透明度, 并按 |w| 裁剪 +VQT6_API void vqt6_graph3d_set_edge_weights(void* g, const double* w, int n); +VQT6_API void vqt6_graph3d_set_edge_threshold(void* g, double t); // |w|归一后低于此值不画 +VQT6_API void vqt6_graph3d_set_edge_budget(void* g, int max_edges); // 最多画多少条(取 |w| 最大) +VQT6_API void vqt6_graph3d_set_view(void* g, double yaw_deg, double pitch_deg, double dist); +VQT6_API void vqt6_graph3d_get_view(void* g, double* yaw_deg, double* pitch_deg, double* dist); +VQT6_API void vqt6_graph3d_set_bg(void* g, int r, int gcol, int b); +VQT6_API void vqt6_graph3d_set_node_scale(void* g, double s); +VQT6_API void vqt6_graph3d_connect_view_changed(void* g, vqt6_slot_callback_t cb, void* user_data); +// 原点复位 +VQT6_API void vqt6_graph3d_reset_view(void* g); + +// ============================================================ +// ChartView — Qt Charts 折线图(loss/acc) +// ============================================================ +VQT6_API void* vqt6_chartview_create(void); +VQT6_API void* vqt6_chartview_create_with_parent(void* parent); +// 新增一条折线, 返回 series 句柄; right_axis!=0 时挂到右侧 Y 轴 +VQT6_API void* vqt6_chartview_add_line(void* view, const char* name, + int r, int g, int b, int right_axis); +VQT6_API void vqt6_chartview_append(void* view, void* series, double x, double y); +VQT6_API void vqt6_chartview_clear_series(void* view, void* series); +VQT6_API void vqt6_chartview_clear_all(void* view); +// 按当前数据重算坐标轴范围(左轴/右轴各自独立) +VQT6_API void vqt6_chartview_autoscale(void* view); +VQT6_API void vqt6_chartview_set_title(void* view, const char* utf8); +VQT6_API void vqt6_chartview_set_legend_visible(void* view, int visible); +VQT6_API void vqt6_chartview_set_background(void* view, int r, int g, int b); +// 滑动窗口: 每条折线超过 max_points 时丢弃最旧的点(max_points<=0 表示不限制) +VQT6_API void vqt6_chartview_set_max_points(void* view, int max_points); +// 打开(或复用)只读大窗; 同一 view 只建一次, 关闭仅隐藏不销毁 +VQT6_API void* vqt6_chartview_open_zoom(void* view, const char* title); +// 把源图当前曲线与轴范围同步到大窗(仅在大窗可见时生效) +VQT6_API void vqt6_chartview_sync_zoom(void* view); + +#ifdef __cplusplus +} +#endif + +#endif // VQT6_BRIDGE_CANVAS_H diff --git a/vqt6/_bridge_core.cpp b/vqt6/_bridge_core.cpp index 557060b..5f2a4be 100644 --- a/vqt6/_bridge_core.cpp +++ b/vqt6/_bridge_core.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -477,3 +479,27 @@ extern "C" VQT6_API void vqt6_qtimer_connect_timeout(void* timer, vqt6_slot_call vqt6_trigger_slot(slot_id); }); } + +// ============================================================ +// 剪贴板 +// ============================================================ +extern "C" VQT6_API void vqt6_clipboard_set_text_utf8(const char* utf8) { + QClipboard* cb = QGuiApplication::clipboard(); + if (cb == nullptr || utf8 == nullptr) return; + cb->setText(QString::fromUtf8(utf8)); +} + +extern "C" VQT6_API int vqt6_clipboard_text_utf8(char* buf, int buf_size) { + if (buf == nullptr || buf_size <= 0) return 0; + QClipboard* cb = QGuiApplication::clipboard(); + if (cb == nullptr) { + buf[0] = '\0'; + return 0; + } + QByteArray u = cb->text().toUtf8(); + int n = static_cast(u.size()); + if (n > buf_size - 1) n = buf_size - 1; + std::memcpy(buf, u.constData(), static_cast(n)); + buf[n] = '\0'; + return n; +} diff --git a/vqt6/_bridge_widgets.cpp b/vqt6/_bridge_widgets.cpp index 52e61ac..8a6b417 100644 --- a/vqt6/_bridge_widgets.cpp +++ b/vqt6/_bridge_widgets.cpp @@ -171,6 +171,19 @@ extern "C" VQT6_API int vqt6_qapplication_quit_on_last_window_closed(void* app) return QApplication::quitOnLastWindowClosed() ? 1 : 0; } +extern "C" VQT6_API void vqt6_qapplication_set_font_family_utf8(void* app, const char* family) { + if (family == nullptr) return; + QApplication* a = (app != nullptr) ? static_cast(app) + : static_cast(QApplication::instance()); + if (a == nullptr) return; + // 只换字体族:从当前应用字体克隆,字号/字重/斜体等全部保持不变。 + // 不能直接 QFont(family) —— 那个构造把 pointSize 置为 -1,实际字号会被 + // 平台默认值改写,整个界面的字号会跟着变。 + QFont f = QApplication::font(); + f.setFamily(QString::fromUtf8(family)); + QApplication::setFont(f); +} + // ============================================================ // QWidget // ============================================================ @@ -1167,18 +1180,135 @@ extern "C" VQT6_API void vqt6_enable_custom_item_checks(void* app) { a->setStyle(new vqt6_CheckProxyStyle()); } +// ------------------------------------------------------------------ +// 自绘 QRadioButton:圆形指示器(圆环 + 内点) +// 与自绘 QCheckBox 同源:同样的 QPalette 取色、同样的 16ms 指数衰减过渡 +// (参考 ViperQtProject 里 vqt6_CheckBox / vqt6_CheckProxyStyle 的做法)。 +// ------------------------------------------------------------------ +// 圆形的绘制区域:在 r 内取居中的正方形(半径一致才是个正圆) +static QRectF vqt6_radio_box(const QRect& r) { + qreal side = qMin(r.width(), r.height()) - 2.0; + if (side < 10.0) side = 14.0; + QRectF box(0, 0, side, side); + box.moveCenter(QRectF(r).center()); + return box; +} + +// 统一的单选指示器绘制:圆环 + 随进度长大的内点 +// box —— 绘制区域(正方形) +// pal —— 控件调色板(决定深浅主题配色) +// enabled —— 是否可用 +// progress —— 选中过渡进度(0=未选 1=选中) +static void vqt6_paint_radio_indicator(QPainter* p, const QRectF& box, const QPalette& pal, + bool enabled, qreal progress) { + p->setRenderHint(QPainter::Antialiasing, true); + const qreal t = progress < 0.0 ? 0.0 : (progress > 1.0 ? 1.0 : progress); + const QColor cBase = enabled ? pal.base().color() : pal.window().color(); + const QColor cOn = enabled ? pal.highlight().color() : pal.button().color(); + const QColor cB0 = enabled ? pal.text().color() : pal.placeholderText().color(); + const QColor cB1 = enabled ? pal.highlight().color() : pal.buttonText().color(); + const QColor fill = vqt6_mix_color(cBase, cOn, t); + const QColor border = vqt6_mix_color(cB0, cB1, t); + + QPainterPath path; + path.addEllipse(box); + p->fillPath(path, fill); + QPen bpen(border, 1.4); + p->setPen(bpen); + p->drawEllipse(box); + + if (!enabled || t <= 0.003) return; + // 内点:半径从 0 涨到 0.32*边长,颜色取 highlightedText 并按进度淡入 + QColor gc = pal.highlightedText().color(); + gc.setAlphaF(gc.alphaF() * t); + const qreal side = qMin(box.width(), box.height()); + const qreal rd = side * 0.32 * t; + p->setPen(Qt::NoPen); + p->setBrush(gc); + p->drawEllipse(box.center(), rd, rd); +} + +class vqt6_RadioButton : public QRadioButton { +public: + vqt6_RadioButton(QWidget* parent = nullptr) : QRadioButton(parent) { + initAnim(); + } + vqt6_RadioButton(const QString& text, QWidget* parent = nullptr) + : QRadioButton(text, parent) { + initAnim(); + } + +protected: + void paintEvent(QPaintEvent*) override { + QPainter p(this); + p.setRenderHint(QPainter::Antialiasing, true); + const QPalette& pal = palette(); + QStyleOptionButton opt; + initStyleOption(&opt); + QRect r = style()->subElementRect(QStyle::SE_RadioButtonIndicator, &opt, this); + if (r.isEmpty() || r.isNull()) { + r = QRect(0, 0, 16, 16); + r.moveCenter(QPoint(rect().left() + 12, rect().center().y())); + } + // 圆环 + 内点:与自绘复选框共用配色与动画进度 + vqt6_paint_radio_indicator(&p, vqt6_radio_box(r), pal, isEnabled(), m_progress); + // 文本 + QRect lr = style()->subElementRect(QStyle::SE_RadioButtonContents, &opt, this); + if (!lr.isEmpty() && !lr.isNull()) { + p.setPen(QPen(isEnabled() ? pal.text() : pal.placeholderText(), 1.0)); + p.drawText(lr, Qt::AlignLeft | Qt::AlignVCenter, text()); + } + } + + void showEvent(QShowEvent* e) override { + snapToState(); + QRadioButton::showEvent(e); + } + +private: + QTimer m_timer; + qreal m_progress = 0.0; + qreal m_target = 0.0; + + void initAnim() { + m_timer.setInterval(16); + QObject::connect(&m_timer, &QTimer::timeout, this, [this]() { + m_progress += (m_target - m_progress) * 0.25; + if (qAbs(m_target - m_progress) < 0.005) { + m_progress = m_target; + m_timer.stop(); + } + update(); + }); + QObject::connect(this, &QRadioButton::toggled, this, [this](bool on) { + m_target = on ? 1.0 : 0.0; + if (!isVisible()) { + m_progress = m_target; + } else if (!m_timer.isActive()) { + m_timer.start(); + } + }); + } + + void snapToState() { + m_target = isChecked() ? 1.0 : 0.0; + m_progress = m_target; + m_timer.stop(); + } +}; + extern "C" VQT6_API void* vqt6_qradiobutton_create(void) { - return new QRadioButton(); + return new vqt6_RadioButton(); } extern "C" VQT6_API void* vqt6_qradiobutton_create_with_text(void* text) { if (text == nullptr) return nullptr; - return new QRadioButton(*static_cast(text)); + return new vqt6_RadioButton(*static_cast(text)); } extern "C" VQT6_API void* vqt6_qradiobutton_create_with_text_parent(void* text, void* parent) { if (text == nullptr || parent == nullptr) return nullptr; - return new QRadioButton(*static_cast(text), static_cast(parent)); + return new vqt6_RadioButton(*static_cast(text), static_cast(parent)); } extern "C" VQT6_API void vqt6_qradiobutton_set_checked(void* rb, int checked) { diff --git a/vqt6/_bridge_widgets.h b/vqt6/_bridge_widgets.h index 50cd948..7cf0e4c 100644 --- a/vqt6/_bridge_widgets.h +++ b/vqt6/_bridge_widgets.h @@ -25,6 +25,7 @@ VQT6_API int vqt6_qapplication_desktop_height(void* app); VQT6_API void vqt6_qapplication_set_quit_on_last_window_closed(void* app, int quit); VQT6_API int vqt6_qapplication_quit_on_last_window_closed(void* app); VQT6_API void vqt6_qapplication_set_theme_palette(void* app, int theme); +VQT6_API void vqt6_qapplication_set_font_family_utf8(void* app, const char* family); VQT6_API void vqt6_enable_custom_item_checks(void* app); // ============================================================ diff --git a/vqt6/_qtcore.vp b/vqt6/_qtcore.vp index e06fb66..f95816c 100644 --- a/vqt6/_qtcore.vp +++ b/vqt6/_qtcore.vp @@ -158,3 +158,10 @@ def vqt6_qbytearraylist_free(list: QtByteArrayListPtr) -> t.State: pass def vqt6_register_slot(slot_id: t.CInt, user_data: t.CVoid | t.CPtr) -> t.State: pass def vqt6_unregister_slot(slot_id: t.CInt) -> t.State: pass def vqt6_trigger_slot(slot_id: t.CInt) -> t.State: pass # 测试用 + + +# ============================================================ +# 剪贴板 — 复制链接等 +# ============================================================ +def vqt6_clipboard_set_text_utf8(text: t.CChar | t.CPtr) -> t.State: pass +def vqt6_clipboard_text_utf8(buf: t.CChar | t.CPtr, buf_size: t.CInt) -> t.CInt | t.State: pass diff --git a/vqt6/_qtwidgets.vp b/vqt6/_qtwidgets.vp index 1631594..5252f4a 100644 --- a/vqt6/_qtwidgets.vp +++ b/vqt6/_qtwidgets.vp @@ -21,6 +21,7 @@ def vqt6_qapplication_desktop_height(app: QtAppPtr) -> t.CInt | t.State: pass def vqt6_qapplication_set_quit_on_last_window_closed(app: QtAppPtr, quit: t.CInt) -> t.State: pass def vqt6_qapplication_quit_on_last_window_closed(app: QtAppPtr) -> t.CInt | t.State: pass def vqt6_qapplication_set_theme_palette(app: QtAppPtr, theme: t.CInt) -> t.State: pass +def vqt6_qapplication_set_font_family_utf8(app: QtAppPtr, family: t.CChar | t.CPtr) -> t.State: pass def vqt6_enable_custom_item_checks(app: QtAppPtr) -> t.State: pass @@ -546,3 +547,64 @@ def vqt6_qtabwidget_connect_current_changed(tw: QtTabWidgetPtr, cb: bytes, user_ def vqt6_qaction_connect_triggered(action: QtActionPtr, cb: bytes, user_data: bytes) -> t.State: pass def vqt6_qtreewidget_connect_item_changed(tw: QtTreeWidgetPtr, cb: bytes, user_data: bytes) -> t.State: pass def vqt6_qtreewidget_connect_item_clicked(tw: QtTreeWidgetPtr, cb: bytes, user_data: bytes) -> t.State: pass + + +# ============================================================ +# Canvas — 手绘板 / 28x28 灰度显示(自绘控件) +# 坐标约定: 内部 0..1 归一化, 与控件像素尺寸解耦 +# ============================================================ +def vqt6_canvas_create() -> QtCanvasPtr | t.State: pass +def vqt6_canvas_create_with_parent(parent: QtWidgetPtr) -> QtCanvasPtr | t.State: pass +def vqt6_canvas_clear(canvas: QtCanvasPtr) -> t.State: pass +def vqt6_canvas_has_ink(canvas: QtCanvasPtr) -> t.CInt | t.State: pass +def vqt6_canvas_set_mode(canvas: QtCanvasPtr, mode: t.CInt) -> t.State: pass +def vqt6_canvas_mode(canvas: QtCanvasPtr) -> t.CInt | t.State: pass +def vqt6_canvas_set_pen(canvas: QtCanvasPtr, r: t.CInt, g: t.CInt, b: t.CInt, width_norm: t.CDouble) -> t.State: pass +def vqt6_canvas_set_grid(canvas: QtCanvasPtr, on: t.CInt) -> t.State: pass +def vqt6_canvas_set_scan_line(canvas: QtCanvasPtr, y_norm: t.CInt) -> t.State: pass +def vqt6_canvas_set_bg(canvas: QtCanvasPtr, r: t.CInt, g: t.CInt, b: t.CInt) -> t.State: pass +def vqt6_canvas_to_mnist28(canvas: QtCanvasPtr, out: t.CUnsignedChar | t.CPtr) -> t.CInt | t.State: pass +def vqt6_canvas_set_image28(canvas: QtCanvasPtr, inp: t.CUnsignedChar | t.CPtr) -> t.State: pass +def vqt6_canvas_connect_changed(canvas: QtCanvasPtr, cb: bytes, user_data: bytes) -> t.State: pass + + +# ============================================================ +# Graph3D — 软件 3D 神经网络结构图(自绘控件) +# API 与渲染后端无关, 后续可替换为 OpenGL 实现 +# ============================================================ +def vqt6_graph3d_create() -> QtGraph3DPtr | t.State: pass +def vqt6_graph3d_create_with_parent(parent: QtWidgetPtr) -> QtGraph3DPtr | t.State: pass +def vqt6_graph3d_setup(g: QtGraph3DPtr, layer_sizes: t.CInt | t.CPtr, n_layers: t.CInt) -> t.State: pass +def vqt6_graph3d_clear(g: QtGraph3DPtr) -> t.State: pass +def vqt6_graph3d_node_count(g: QtGraph3DPtr) -> t.CInt | t.State: pass +def vqt6_graph3d_edge_count(g: QtGraph3DPtr) -> t.CInt | t.State: pass +def vqt6_graph3d_set_layer_alpha(g: QtGraph3DPtr, layer: t.CInt, alpha: t.CDouble) -> t.State: pass +def vqt6_graph3d_set_all_layer_alpha(g: QtGraph3DPtr, alpha: t.CDouble) -> t.State: pass +def vqt6_graph3d_set_values(g: QtGraph3DPtr, values: t.CDouble | t.CPtr, n: t.CInt) -> t.State: pass +def vqt6_graph3d_set_edge_weights(g: QtGraph3DPtr, w: t.CDouble | t.CPtr, n: t.CInt) -> t.State: pass +def vqt6_graph3d_set_edge_threshold(g: QtGraph3DPtr, tval: t.CDouble) -> t.State: pass +def vqt6_graph3d_set_edge_budget(g: QtGraph3DPtr, max_edges: t.CInt) -> t.State: pass +def vqt6_graph3d_set_view(g: QtGraph3DPtr, yaw_deg: t.CDouble, pitch_deg: t.CDouble, dist: t.CDouble) -> t.State: pass +def vqt6_graph3d_get_view(g: QtGraph3DPtr, yaw_deg: t.CDouble | t.CPtr, pitch_deg: t.CDouble | t.CPtr, dist: t.CDouble | t.CPtr) -> t.State: pass +def vqt6_graph3d_set_bg(g: QtGraph3DPtr, r: t.CInt, gcol: t.CInt, b: t.CInt) -> t.State: pass +def vqt6_graph3d_set_node_scale(g: QtGraph3DPtr, s: t.CDouble) -> t.State: pass +def vqt6_graph3d_connect_view_changed(g: QtGraph3DPtr, cb: bytes, user_data: bytes) -> t.State: pass +def vqt6_graph3d_reset_view(g: QtGraph3DPtr) -> t.State: pass + + +# ============================================================ +# ChartView — Qt Charts 折线图(loss/acc 曲线) +# ============================================================ +def vqt6_chartview_create() -> QtChartViewPtr | t.State: pass +def vqt6_chartview_create_with_parent(parent: QtWidgetPtr) -> QtChartViewPtr | t.State: pass +def vqt6_chartview_add_line(view: QtChartViewPtr, name: t.CChar | t.CPtr, r: t.CInt, g: t.CInt, b: t.CInt, right_axis: t.CInt) -> QtChartSeriesPtr | t.State: pass +def vqt6_chartview_append(view: QtChartViewPtr, series: QtChartSeriesPtr, x: t.CDouble, y: t.CDouble) -> t.State: pass +def vqt6_chartview_clear_series(view: QtChartViewPtr, series: QtChartSeriesPtr) -> t.State: pass +def vqt6_chartview_clear_all(view: QtChartViewPtr) -> t.State: pass +def vqt6_chartview_autoscale(view: QtChartViewPtr) -> t.State: pass +def vqt6_chartview_set_title(view: QtChartViewPtr, title: t.CChar | t.CPtr) -> t.State: pass +def vqt6_chartview_set_legend_visible(view: QtChartViewPtr, visible: t.CInt) -> t.State: pass +def vqt6_chartview_set_background(view: QtChartViewPtr, r: t.CInt, g: t.CInt, b: t.CInt) -> t.State: pass +def vqt6_chartview_set_max_points(view: QtChartViewPtr, max_points: t.CInt) -> t.State: pass +def vqt6_chartview_open_zoom(view: QtChartViewPtr, title: t.CChar | t.CPtr) -> QtWidgetPtr | t.State: pass +def vqt6_chartview_sync_zoom(view: QtChartViewPtr) -> t.State: pass diff --git a/vqt6/_types.vp b/vqt6/_types.vp index 20beb31..57e5177 100644 --- a/vqt6/_types.vp +++ b/vqt6/_types.vp @@ -80,6 +80,12 @@ QtColorDialogPtr: t.CTypedef = t.CVoid | t.CPtr # QColorDialog* QtFontDialogPtr: t.CTypedef = t.CVoid | t.CPtr # QFontDialog* QtInputDialogPtr: t.CTypedef = t.CVoid | t.CPtr # QInputDialog* +# vqt6 自绘控件(_bridge_canvas) +QtCanvasPtr: t.CTypedef = t.CVoid | t.CPtr # Vqt6Canvas*(QWidget* 兼容) +QtGraph3DPtr: t.CTypedef = t.CVoid | t.CPtr # Vqt6Graph3D*(QWidget* 兼容) +QtChartViewPtr: t.CTypedef = t.CVoid | t.CPtr # QChartView* +QtChartSeriesPtr: t.CTypedef = t.CVoid | t.CPtr # QLineSeries* + # QtNetwork QtTcpSocketPtr: t.CTypedef = t.CVoid | t.CPtr # QTcpSocket* QtUdpSocketPtr: t.CTypedef = t.CVoid | t.CPtr # QUdpSocket* diff --git a/w32/fileio.vp b/w32/fileio.vp index 759811f..54677d2 100644 --- a/w32/fileio.vp +++ b/w32/fileio.vp @@ -1,6 +1,7 @@ import t, c from stdint import * import stdio +import stdlib import w32.win32base import w32.win32file @@ -36,6 +37,26 @@ SHARE_WRITE: t.CDefine = 0x00000002 SHARE_DELETE: t.CDefine = 0x00000004 +def create_file_utf8(path: str, access: ULONG, share: ULONG, disposition: ULONG, + flags: ULONG) -> w32.win32base.HANDLE: + """按路径编码打开/创建文件。 + 合法 UTF-8(源码字面量、Qt 返回的中文目录) → 转 UTF-16 走 CreateFileW; + 非法 UTF-8(如命令行 argv 的 ANSI 串) → 退回 CreateFileA。详见 win32base""" + if w32.win32base.utf8_is_valid(path) == 0: + return w32.win32file.CreateFileA(path, access, share, None, disposition, + flags, None) + wp: w32.win32base.WCHARPTR = w32.win32base.utf8_to_utf16_dup(path) + if wp is None: + # 宽字符缓冲分配失败: 走 A 路径, 非 ASCII 路径在此自然失败并返回 + # INVALID_HANDLE_VALUE(不掩盖错误, 只是复用同一失败出口) + return w32.win32file.CreateFileA(path, access, share, None, disposition, + flags, None) + h: w32.win32base.HANDLE = w32.win32file.CreateFileW( + t.CPtr(wp), access, share, None, disposition, flags, None) + stdlib.free(wp) + return h + + class File: handle: w32.win32base.HANDLE closed: bool @@ -97,9 +118,9 @@ class File: disposition = w32.win32file.OPEN_EXISTING self.can_read = True - self.handle = w32.win32file.CreateFileA( - filename, access, self._share_mode, - None, disposition, w32.win32file.FILE_ATTRIBUTE_NORMAL, None + self.handle = create_file_utf8( + filename, access, self._share_mode, disposition, + w32.win32file.FILE_ATTRIBUTE_NORMAL ) if self.handle == w32.win32base.INVALID_HANDLE_VALUE: @@ -189,22 +210,30 @@ class File: buf[total] = 0 return LONG(total) - def read_all(self, buf: bytes, max_count: ULONG) -> LONG: + def read_all(self, buf: bytes, buf_size: ULONG) -> LONG: + """把整个文件读进 buf 并写成 C 字符串(内部先 seek 到 0) + + 契约(注意与 write 的 count 口径不同): + buf_size 是缓冲区**容量, 含终止符** —— 最多读 buf_size-1 字节, + 再在 buf[返回值] 写一个 NUL 字节, 故返回值 <= buf_size-1。 + 文件比 buf_size-1 更长时**静默截断**, 不报错; 要区分"读完了"还是 + "被截断了", 请在调用前自己比一次 f.size()。 + 返回实际读到的字节数, 负数为错误码。""" if self.closed: return FRESULT.ERR_CLOSED if not self.can_read: return FRESULT.ERR_PERM - if max_count < 2: return FRESULT.ERR + if buf_size < 2: return FRESULT.ERR file_size: LONGLONG = self.size() if file_size < 0: return FRESULT.ERR_IO to_read: ULONG = 0 - if file_size < max_count - 1: + if file_size < buf_size - 1: to_read = ULONG(file_size) else: - to_read = max_count - 1 + to_read = buf_size - 1 self.seek(0, SEEK_SET) bytes_read: ULONG = 0 result: BOOL = w32.win32file.ReadFile(self.handle, buf, to_read, t.CPtr(c.Addr(bytes_read)), None) if result == 0: return FRESULT.ERR_IO - if bytes_read < max_count: + if bytes_read < buf_size: buf[bytes_read] = 0 return LONG(bytes_read) @@ -362,22 +391,30 @@ class FileW: buf[total] = 0 return LONG(total) - def read_all(self, buf: bytes, max_count: ULONG) -> LONG: + def read_all(self, buf: bytes, buf_size: ULONG) -> LONG: + """把整个文件读进 buf 并写成 C 字符串(内部先 seek 到 0) + + 契约(注意与 write 的 count 口径不同): + buf_size 是缓冲区**容量, 含终止符** —— 最多读 buf_size-1 字节, + 再在 buf[返回值] 写一个 NUL 字节, 故返回值 <= buf_size-1。 + 文件比 buf_size-1 更长时**静默截断**, 不报错; 要区分"读完了"还是 + "被截断了", 请在调用前自己比一次 f.size()。 + 返回实际读到的字节数, 负数为错误码。""" if self.closed: return FRESULT.ERR_CLOSED if not self.can_read: return FRESULT.ERR_PERM - if max_count < 2: return FRESULT.ERR + if buf_size < 2: return FRESULT.ERR file_size: LONGLONG = self.size() if file_size < 0: return FRESULT.ERR_IO to_read: ULONG = 0 - if file_size < max_count - 1: + if file_size < buf_size - 1: to_read = ULONG(file_size) else: - to_read = max_count - 1 + to_read = buf_size - 1 self.seek(0, SEEK_SET) bytes_read: ULONG = 0 result: BOOL = w32.win32file.ReadFile(self.handle, buf, to_read, t.CPtr(c.Addr(bytes_read)), None) if result == 0: return FRESULT.ERR_IO - if bytes_read < max_count: + if bytes_read < buf_size: buf[bytes_read] = 0 return LONG(bytes_read) diff --git a/w32/win32base.vp b/w32/win32base.vp index aba48d3..d1b09d9 100644 --- a/w32/win32base.vp +++ b/w32/win32base.vp @@ -1,5 +1,6 @@ import t from stdint import * +import stdlib HANDLE: t.CTypedef = VOIDPTR LPCSTR: t.CTypedef = t.CConst | t.CChar | t.CPtr @@ -78,3 +79,44 @@ extern def SleepEx(dwMilliseconds: ULONG, bAlertable: BOOL) -> ULONG extern def GetTickCount() -> ULONG extern def GetTickCount64() -> ULONGLONG extern def GetCommandLineA() -> CHARPTR + + +# ============================================================================= +# 路径编码: Viper 字符串统一 UTF-8, 而 Windows 的 A 系列 API 走系统 ANSI 代码页 +# (中文机器上为 GBK)。直接拿 UTF-8 路径调 CreateFileA / CreateDirectoryA, +# 遇到中文目录名会被按 GBK 误读 → 误报"文件/目录不存在"。故路径统一转 UTF-16 +# 走 W 系列 API; 仅当字节串不是合法 UTF-8(如命令行 argv 传来的 ANSI 串)时才 +# 退回 A 系列, 保持既有行为。 +# ============================================================================= +UTF8_CODEPAGE: t.CDefine = 65001 +MB_ERR_INVALID_CHARS: t.CDefine = 0x00000008 + +extern def MultiByteToWideChar(CodePage: UINT, dwFlags: DWORD, lpMultiByteStr: LPCSTR, + cbMultiByte: int, lpWideCharStr: WCHARPTR, + cchWideChar: int) -> int + + +def utf8_is_valid(s: str) -> int: + """整串(含结尾 NUL)是否为合法 UTF-8。1=是(按 UTF-8 处理), 0=否(按 ANSI)""" + if s is None: + return 0 + n: int = MultiByteToWideChar(UTF8_CODEPAGE, MB_ERR_INVALID_CHARS, s, -1, + t.CPtr(0), 0) + if n <= 0: + return 0 + return 1 + + +def utf8_to_utf16_dup(s: str) -> WCHARPTR: + """UTF-8 → 新分配的 UTF-16 缓冲(含结尾 NUL), 调用方负责 stdlib.free; + 失败(含非法 UTF-8)返回 None""" + if s is None: + return None + n: int = MultiByteToWideChar(UTF8_CODEPAGE, 0, s, -1, t.CPtr(0), 0) + if n <= 0: + return None + buf: WCHARPTR = stdlib.malloc(t.CSizeT(n) * 2) + if buf is None: + return None + MultiByteToWideChar(UTF8_CODEPAGE, 0, s, -1, buf, n) + return buf