
Купил себе столитровый прибабах, дабы отдохнуть от этой вашей радиофизики. Чуть не оглох на обкатке, правда
​
Купил за 24600 вместо 35000 что он ща стоит, новехонький, гайковёрт на 1700нм тянет

​
Купил за 24600 вместо 35000 что он ща стоит, новехонький, гайковёрт на 1700нм тянет
Собстна... Пару месяцев назад, может полтора, я делал пост, который тут на удивление залетел и до сих пор в закрепе, насчет своего мода в Minecraft. Думал, начну делать апдейты быстрее, но...
Но чем дальше я это пишу, тем яснее становится неприятная вещь: можно сделать хороший DSP-тракт, нормальный шумовой бюджет и красивые приборы, но если сама модель распространения неправильная, всё остальное начинает обслуживать очень убедительную ложь.
В чем суть: проблема проявилась на довольно простом тесте.
Передатчик и приёмник стоят в 200 метрах друг от друга. Частота 7 МГц, длина волны около 43 метров. Между ними на линии визирования ставится один железный блок размером 1 кубический метр.
Текущая модель способна посчитать такой блок практически абсолютной преградой и закрыть канал на весь бюджет, вплоть до сотен децибел. Физически это ерунда. Радиус первой зоны Френеля в середине такого пути около 46 метров. Метровый блок закрывает крошечную долю рабочего фронта. Он не обязан быть прозрачным как материал, волна просто обходит его. На 7 МГц размер объекта слишком мал относительно длины волны, чтобы он работал как сплошная стена.
Масштаб трагедии на графике. Огромный пузырь - это первая зона Френеля (ее радиус в центре - 46 метров). Черная линия - ось LOS (прямая видимость). Крошечная, едва заметная точка на отметке 100 метров - тот самый кубический метр железа. Лучевая модель считает, что эта точка намертво перекрывает весь пузырь.
Причина оказалась не в DDA и не в таблице проводимости. Причина в самой абстракции. Сейчас транспорт в основном лучевой, и у него нашлось несколько критических проблем:
В итоге я пришёл к выводу, что бесконечно чинить этот набор поправок нет смысла. Можно убрать самые грубые артефакты, но луч не станет волной от того, что вокруг него появилось ещё пять коэффициентов.
Ближайшая цель не "полный Максвелл на весь Minecraft-мир". Это было бы красиво только в тексте, а на практике закончилось бы сеткой из миллиардов неизвестных и дымом из сервера. Идея в иерархии моделей.
Сначала стабилизирую текущий движок. Уберу сломанный мультипуть, заменю точечное перекрытие зоны на устойчивую оценку площади и закрою ошибки в FFI, Vulkan и состоянии движущихся радиостанций.
Следующий большой шаг уже полевой: маршинг-солвер для комплексного электромагнитного поля. Изначально я думал начать с обычного 2D-параболического уравнения. Но в 2D одиночный куб автоматически превращается в бесконечную стену, уходящую поперёк среза.
Поэтому основной вариант теперь 3D PE:
du/dx = [ i / (2*k_0) ] * (d^2 u / dy^2 + d^2 u / dz^2) + [ (i*k_0) / 2 ] * (n^2 - 1) * u
Поле маршируется по дальности, а поперечная дифракция считается спектрально. Препятствие локально подавляет часть фронта, после чего поле естественно заполняет тень с боков.
Поле хочется считать один раз на передатчик и кэшировать. При этом хранить нужно комплексную передаточную функцию (амплитуду и фазу), чтобы получить когерентную интерференцию и корректную стыковку с локальным полноволновым решением.
Расчет интерференции прямого и отраженного от земли лучей. Вот для получения этих "лепестков" затухания и нужны комплексные числа (амплитуда и фаза). Без фазы волны просто сложились бы по мощности, выдав плоскую скучную линию без интерференционных нулей.
PE (Parabolic Equation) отлично считает рельеф и большие дистанции, но не умеет в обратное рассеяние, щели и металлические комнаты. Для таких мест планируются локальные полноволновые "острова".
Чтобы это работало, пространство придется разделить на два уровня:
| Физика | Глобальный решатель (3D PE) | Локальный остров (FDTD / FDFD) |
|---|---|---|
| Что считает | Дальнобойное распространение, кривизна земли, огибание рельефа | Электромагнитное поле внутри сложной геометрии |
| Где применяется | Открытый мир, расстояния в сотни блоков | Комнаты, туннели, металлические коридоры |
| Как работает | Волна марширует только вперед (односторонняя модель) | Векторный Максвелл, считает отражения во все стороны |
Отдельная проблема локальных островов - это металл. Просто занулить поле внутри металлического вокселя удобно численно, но это поглощающая маска, а не идеальный проводник (PEC). Настоящий металл должен отражать поле и соблюдать граничные условия.
Также придётся аккуратно определить знак комплексной проницаемости. Достаточно один раз смешать соглашения exp(-i wt) и exp(+ikx), чтобы пассивная среда начала усиливать волну вместо поглощения.
Граница между физикой и вычислительным бюджетом тут довольно забавная. Полностью разрешить такой мир по (λ/10) невозможно. Поэтому сейчас думаю над несколькими вопросами:
В общем, проект постепенно переходит из состояния "мод с довольно серьёзным радио" в состояние "небольшой радиофизический движок, к которому зачем-то прикручен Minecraft 1.7.10".
Мне это нравится, хотя иногда открываешь формулу, потом код, потом ещё одну статью, а через несколько часов обнаруживаешь, что пытался физически корректно объяснить, почему рация Стива не ловит другую рацию за кубом железа.
Если кому-то интересны PE, FDTD/FDFD, Vulkan, антенны или просто странные технические решения, буду рад обсудить.
количество съеденной бумаги бтв
A while back I posted the thing that replaced (2+3)*2 with a UB bingo card. People kept commenting, so I kept going. This is v2. It got worse. That was the point.
The old version was honest about one thing: it broke runtime semantics. Raw pointers, transmute, FFI setjmp/longjmp across stack frames, the whole circus. But the borrow checker was still technically happy with me. I was sneaking lies past it through channels it doesn't guard. v2 stops sneaking. Now I lie to its face on its own terms and it just nods.
What's actually new this time:
&mut to the same i32, both used across each other, neither derived from the other so there's no reborrow to track. A &i32 the compiler thinks is frozen, mutated through underneath while it's still "frozen." A reference to a local returned after its scope ends, accepted as 'static. A self-referential struct, the one thing the model literally can't describe.There's a little Anarchy<T> handle that hands out references of any lifetime and any mutability to the same slot, as many at once as you want - and the call sites that use it contain zero unsafe. Hold one and there's nothing left for the checker to verify.
unsafe at the crime scene. Declared false covariance via PhantomData<fn() -> &'a i32> on a type that controls a writable slot, so lifetimes coerce shorter with no cast and no unsafe block. The compiler trusts the marker as blindly as LLVM trusted noalias.Also threw in autoref specialization and Deref polymorphism so a Mirror isn't an i32 but answers to i32's methods anyway. Type identity dissolves without a single transmute.
(2+3)*2 is now also computed at the type level. Peano numbers + recursive trait impls. <Ten as Peano>::N is the answer produced entirely by trait resolution chewing through the recursion, and the runtime result gets checked against it. There's also a const-eval gate that's just an out-of-bounds array index - the build dies if the invariant breaks. No assert!, no panic!, just [()][(n != 10) as usize].addr_of! btw is gone too.println! -> snprintf + write(1, …). assert_eq! -> if … { _exit(1) }. vec! -> alloc + from_raw_parts. Every ptr::addr_of! replaced with the raw-ref operator &raw const / &raw mut. A macro generates text that isn't in your source, so it's a different program than the one you're reading - felt like cheating. asm! stays, because it isn't generation: the assembler string goes through the compiler as-is, it's an intrinsic. The ! is a syntactic accident there.
fork -> vforkLet me be honest: someone with a great nickname asked me, paraphrasing,
>"I need (2+3)x2 but I have no MMU, can you do this with vfork instead of fork?"
So I did. No MMU means no page table to copy, which is the entire reason vfork still exists, so I leaned all the way in: the child borrows the parent's address space, writes the answer into a global, and leaves through a raw _exit so nothing unwinds. No pipe, no copy. The parent is frozen until the child is gone, so the read always sees the write. Standing on the edge of the vfork contract on purpose.
movnti store straight past every cache level, sfence to drain the write-combining buffer, clflush to evict the line by hand, mfence, then read it back from DRAM. The bits never change. The whole memory hierarchy does a round trip it was specifically built to let you skip.#[repr(C, packed)] record, value parked one byte in so it straddles its own alignment. Never form a reference to a packed field (instant UB), only raw pointers.panic_any -> catch_unwind -> downcast), a calling-convention violation (calling a 3-arg extern "C" fn through a pointer typed with one arg), and niche-layout abuse (smuggling the value inside Option<NonNull>).Everything from mu v1 is still in there - the /proc/self/mem round trip, process_vm_readv on itself, the GS-segment and thread-name storage, the hand-decomposed vtable dispatch, the memcpy over an AtomicI32, the self-modifying JIT blob on an RWX page. v2 just stacks the type-system stuff on top.
It's one translation unit, Linux-only, x86-64-only, and it prints 10. Every new function is an identity, so the chain 2 -> +3 -> ×2 never actually moves.
Credits: u/torsten_dev for the public-function-pointer idea - a function's address is a relocation, a symbolic constant LLVM can't reason about as an integer, so addr.saturating_add(x).wrapping_sub(addr) survives to run as an actual barrier (the floated (x ^ a) ^ a version doesn't work, that folds to x no matter what a is and the compiler proves it without ever knowing a). And u/Cats_and_Shit (lmao) for the vfork suggestion.
Mod that actually tries to simulate a radio signal. Why? Because I wanted MIKU FM stations in Minecraft. That's all. That's what I'm showing here lmao.
NOTE: I'll upload it someday. After some code refactors, tho. Maybe in two weeks. Or one. No clue.
NOTE 2: Models and textures are belong to HBM Nuclear Tech Mod. These are temp. placeholders, but whatever. Thanks to bobcat.
This is for 1.7.10 (GTNH, needs lwjgl3ify). Instead of "are you within X blocks, yes/no", the whole signal path is simulated. The DSP and propagation math live in a Rust crate talking to Vulkan, the Forge mod drives it through Java 21's FFM.
Real modulation modes: AM, narrow/wide FM, SSB, CW, plus a noise jammer (I didn't show it because the jamming model sucks duh). When you transmit, the audio is modulated to baseband (FM integrates the audio into phase, AM rides it on the envelope, SSB builds the analytic signal with a Hilbert transform, etc.). Every transmitter in range gets frequency-shifted into your receiver's passband by a per-pair oscillator, they're all summed, a noise floor is added, then it's demodulated back. So two stations close on the dial genuinely interfere and a jammer actually buries you.
Propagation is a link budget, btw. Free-space loss is the real Friis equation, 20*log(d) + 20*log(f) - 147.55 dB. Obstacles are a voxel DDA raycast, but through the first Fresnel zone rather than a single line: a bundle of rays that pinch together at both endpoints and bulge in the middle (the zone radius is roughly 0.5*sqrt(lambda*d), tens of metres on VHF over a long path). So a wall beside the line only blocks part of the zone and costs a few dB while the signal diffracts around it, but seal a transmitter inside iron and every ray passes through the metal and the path dies completely. Materials attenuate by their loss regime: conductors scale as sqrt(f), dielectrics roughly linearly, iron is about 200 dB/m (a wall), glass a few dB/m.
Noise is physical, starts at the real thermal floor, kT0 of about -174 dBm/Hz, scaled by your receiver bandwidth and noise figure, then the man-made noise of your environment is added on top (ITU-R P.372, which falls with frequency, so a city is tens of dB noisier than open country on HF). That produces a kinda real result: on HF the external noise dominates and a better receiver barely moves the floor, while on VHF the receiver's own figure sets it. The S-meter reads the actual S/N from the signal power over the noise power, before the AGC flattens it.
The world is flat, so there'd be no horizon and a ground wave would run forever. So Earth curvature is modelled... probably. It works in core, but not in Minecraft somewhy. But what is has is the 4/3-effective-Earth bulge between two antennas, d1*d2/(2k*Re), rises into the path past the radio horizon (sqrt(2k*Re*h), the familiar 4.12*sqrt(h) km) and the diffraction over it kills the link. Antenna height drives the horizon directly, so stacking antenna blocks raises your effective height and you reach further, literally through blocks. Earth radius is a config knob if you want a t i g h t e r world (why?)
HF is the exception. It bounces off the ionosphere: a foF2 lookup over the day sets the maximum usable frequency by the secant law, go above it and the signal escapes to space; below it the D-layer absorption follows 1/(f+fL)^2 and the solar zenith angle, so daytime eats the low bands and night opens them up. Skywave ignores the curvature entirely, which is exactly why shortwave reaches over the horizon and VHF can't.
Other bits: a transmitter has a Station mode that plays .qoa music files and embeds the station name in the signal as an inaudible 16/18 kHz FSK subcarrier, so a receiver or the analyzer pulls the name straight off the air (and loses it in the noise when the signal gets weak, like real RDS). There's an SDR-style spectrum analyzer with a waterfall and click-to-tune, a relay block, and the receive audio is band-limited (300-3000 Hz on voice, with FM de-emphasis) so it sounds like a radio instead of a clean speaker with hiss.
Уже несколько недель ковыряю довольно странный pet-проект и захотелось поделиться, потому что стек получился нестандартный, и проблемы по дороге попадаются интересные.
Идея простая до неприличия. Беру Minecraft 1.7.10 (да, тот самый, GTNH-style модпаки), и пишу для него мод, где радиосвязь работает по-настоящему. Не "поставил блок передатчика, поставил блок приёмника, услышал звук". А вот так: у каждого передатчика есть несущая частота, полоса, мощность в ваттах, тип модуляции (AM, NFM, WFM, USB, LSB, CW), микрофон захватывает голос игрока через OpenAL, модулирует на несущую, дальше сигнал летит через воксельный мир с поглощением по материалам (камень глушит сильнее дерева, железо вообще закрывает путь), огибает препятствия через дифракцию Буллингтона по knife edge, на HF отражается от ионосферы по таблице foF2 с учётом времени суток и солнечной активности, ловит доплер от движения, ну и затухание Рэлея сверху для атмосферы. Приёмник всё это демодулирует обратно в PCM и через позиционный звук в OpenAL выдаёт игроку.
Спектроанализатор тоже сделал отдельным блоком. Открываешь GUI, видишь честный SDR-style водопад с центральной частотой, спаном от 1 кГц до 20 кГц, можешь зумиться колесом, кликом перенастраивать, видеть огибающие FM, боковые SSB, шумовой пол со своей текстурой. Получилось очень похоже на gqrx или SDR# по ощущениям.
Стек такой. Сам мод на Java 21 (через lwjgl3ify, который патчит 1.7.10 до современной JVM). Вся тяжёлая DSP и физика распространения волн вынесена в отдельный Rust воркспейс, rf-core называется. Там Vulkan-компьют для FFT, Cooley-Tukey radix-2 на 4096 точек в шейред мемори, остальное пока на CPU потому что прибыли от GPU на коротких операциях нет. Связь Java с Rust через FFM (Project Panama, который в 21-й Java preview), без всякого JNI. Маршалинг структур через MemoryLayout, downcall handles в статических final полях для агрессивного инлайна джитом.
Пайплайн железный. Сервер каждый тик собирает живые излучения, для каждого приёмника зовёт mgr.mix(...), который для всех emission прогоняет солвер распространения, складывает baseband, добавляет тепловой шум, прогоняет AGC, отдаёт IQ-чанк. Демодулятор тут же сворачивает IQ в int16 PCM, пакет летит на клиент, клиент скармливает буфер OpenAL источнику с правильной 3D-позицией. От нажатия PTT до звука в ухе у второго игрока проходит около 100 миллисекунд.
Один и тот же передатчик слушают одновременно три потребителя. Сам блок-приёмник, скрытый приёмник спектроанализатора, плюс handheld радио у игрока в руке. Первая версия mix'a у каждого из них честно дергала produce_baseband, что внутри Rust дренировало audioring и продвигало фазу модулятора. Результат: первый получал нормальный звук, остальные получали зиропаддед аудио с прыгнувшей фазой, и водопад на анализаторе показывал симметричный диагональный муар, который пропадал стоило удалить приёмник. Исправил кешем baseband на тик внутри ActiveEmission. Модулятор теперь продвигается строго один раз за серверный тик, на максимум из запросов всех потребителей.
OpenAL сурс-пул на Windows ограничен 32 источниками. Когда игрок ходил с ручным радио, клиент создавал новый источник AL на каждый блок перемещения, потому что ключ голоса в моей системе включал координаты. Через минуту ходьбы драйвер начинал отдавать AL_OUT_OF_MEMORY, после чего любой вызов с нулевым handle лил в лог AL_INVALID_NAME сотнями строк в секунду. Перевёл voicekey на стабильный идентификатор от сервера (UUID для handheld, координата только для блоков), добавил LRU-эвикшн и таймаут, развязал обновление позиции от создания источника.
FM-демодулятор на пустой несущей выдавал адский белый шум. Это математически правильно, atan2 на чистом гауссовом шуме равномерно распределён в [-pi, pi], после умножения на fmGain и clamp'а получается оглушительный нойзфлор. Реальные радио решают это сквелчом. Сделал классический squelch по коэффициенту вариации огибающей. У чистого FM-карьера CV около нуля, у Rayleigh шума около 0.52, между ними линейный рамп гейна, со сглаживанием один пол. На паузе между нотами в тест-тона (music) режиме сквелч теперь закрывается за 20 миллисекунд и не оглушает.
Что ещё хочу. Уже думаю про мост с проигрывателем (поставил проигрыватель рядом с передатчиком, заиграла пластинка в эфир), про чесночный режим для шифрования голоса между двумя радиостанциями с общим ключом, и про моделирование Sporadic-E и Auroral Backscatter для HF аномалий. Возможно, добавлю реалистичные ограничения по току на батарейку handheld'а, чтобы 100-ваттная передача быстро её сажала.
Кому интересно ковыряться в такой странной комбинации технологий, или у кого есть опыт с FFM или Vulkan compute и хочется обсудить подходы, буду рад, кину репо. Особенно интересен фидбек от тех, кто занимается реальной DSP и видит, где я грубо упрощаю физику. Знаю что foF2 LUT это позорно грубо, но игре больше и не надо, иначе никто не услышит свой первый QSO за разумное время.
Продолжаю, в общем-то, писать свой отладчик и крейт на Rust под Vulkan, в текущий момент отладчик может визуализировать почти все, что добавляет крейт.
Были добавлены (имплементированы в GUI из родного крейта, с которым GUI общается по IPC):
cmdFillBuffer. После зависания читаю его и вижу последний выполнившийся маркер и первый неотработавший. Это и есть точка зависания, плюс-минус один диспатч. В отладчике отдельная вкладка со списком зависаний, по клику открывается трейл маркеров со статусами OK/HUNG/PENDING.ERROR_DEVICE_LOST, дампится в краш-репорт. Реально полезно когда зависание произошло три минуты назад и непонятно вообще какой вообще сабмит виноват и почему.Allocator::allocate, делает force_capture бэктрейса, фильтрует фреймы стд и моей либы, и первый внешний фрейм считает за место вызова. Накапливает статистику по местам - сколько байт активно, сколько было пиково, сколько раз вызывали. Раз в секунду делает топ-N снапшот и шлет во вьювер. Открыл вкладку, увидел что какой-то один тупой хелпер выделяет 4гб - дальше уже моя проблема.subtle несовместимости которые валидационный слой иногда пропускает.VK_EXT_debug_printf. Мессенджер валидационного слоя парсит DEBUG-PRINTF сообщения, классифицирует по стадиям (вершинный/фрагментный/компьют/трассировка/мэш), и шлет в отдельную вкладку с фильтрами по стадии и дедупом по локации и префиксу формата. Дедюп нужен потому что принтф в горячем цикле спамит тысячи строк в секунду, без него вкладка превращается в кашу. Вместо одной строки на вызов получаешь "это сообщение x4823" - сразу видно что принтф внутри тайтового цикла.VK_EXT_device_fault, VK_NV_device_diagnostic_checkpoints, VK_AMD_buffer_marker. Каждое есть только на части железа, но любое из них лучше чем ничего. В обработчике потери устройства вызываю collect_all и получаю вендор-специфичное описание плюс последние NV чекпоинты плюс статус AMD маркеров. Прикручено к репортеру крашей, в отчете отдельная markdown-секция.track_caller на регистрации каждого Vulkan-объекта, в репорте на закрытии показывает все недоудаленное с указанием файла, строки и колонки создания. Плюс счетчик использований - сколько раз объект реально биндился. Если счетчик равен нулю значит объект создали и забыли, классика. В отдельной вкладке табличка живые/уничтоженные/осиротевшие с сортировкой и группировкой по типам.И обновил интерфейс:
rusttype или fontdue, поэтому написал сам (по факту, вытянул из своей же игры) - cmap format 4, glyf+loca, простые и составные глифы, аналитический cканлайновый растеризатор с правилом ненулевой обмотки и 5x суперсэмплингом по вертикали. Кэш глифов с ключом (glyph_id, целочисленный px размер). Если TTF не лежит в assets, откат на встроенный 5x8 битмап, как раньше.Это все. Если кому интересно - скоро уже выложу на гх, ссылку под постом оставлю. Все так же ноль зависимостей кроме ash, все так же Win32 FFI напрямую, все так же мазохизм. Заодно хуй че сломается.
So, three months ago I started writing a Vulkan wrapper alongside my Super Hexagon clone because every existing one made me angry for different reasons. ash is too low level (although i love it), vulkano makes choices for you, wgpu is a different abstraction entirely. Fine, i'll do it myself, how hard can it be?
Turns out hard. But that's not the point... and I made it zero-dependency. It relies on ash only. Without egui, winit or etc. Pure FFI, pure ash.
The point is two weeks ago I was debugging a missing barrier between a compute write and a fragment read, and I sat in renderdoc for forty minutes scrolling through a frame capture trying to figure out which of my 60 dispatches was the one. and I thought, you know what, I have all this state in my own library already, I just need to render it somewhere.
So now I have an ipc (im tired pressing shift sorry) ring buffer between my lib and a separate viewer process, and the viewer draws:
here's the thing though!
It's coupled to my own vulkan wrapper. The ipc protocol is mine, the event kinds are mine. someone on vulkano (why tho lmao) would need to write a bridge, someone on wgpu wouldn't benefit because wgpu hides most of what this shows. It's also windows only - rendering is portable but the window code is win32. and the viewer is one binary where every view shares an Rc<RefCell<World>>, which works fine but probably wouldn't survive a code review.
if you've shipped a niche tool like this, was the maintenance worth it?
btw yes i know about gpuopen rdna profiler. yes i tried it. yes it's better at what it does. mine does different things.
Стоило давно это сделать, просто как-то не задумывался. Но в принципе, теперь у моего крейта есть отдельный движок, где можно все отлаживать. Зачем вам это нужно? Понятия не имею.
TL;DR: написал визуализатор для своего Vulkan-движка чтобы не сдохнуть от рендердока, потому что я конченный дебил.
Короче. Каждый раз когда у меня что-то ломалось в графике, я открывал рендердок, ждал пока он прожуёт 38^(10000мб) дамп, искал в нём один забытый барьер и закрывал рендердок. Потом это повторялось через час. В какой-то момент я просто сел и написал свой инструмент потому что всё равно быстрее.
Что есть сейчас.
Архитектура примитивная до неприличия. Кольцевая память между моей либой и вьювером, 256 байт на запись, лоззи при переполнении. Продюсер пишет, вьювер читает. Падает один - другой ждёт.
Чего нет.
Хз, надо ли публиковать вообще, но если вдруг кому тут понадобится и кто точно такой же мазохист как и я - вылью на гх. А, нуида, все снова zero-dependency, кроме ash, разумеется, иначе с вулканом я там вообще не поработаю. А так... Ну, Win FFI)))
Если будете говорить что я переизобрёл <ээээээээнструмент> - я знаю, мне быстрее было написать чем учиться <эээээээээээнструменту>. Суп вилкой есть тоже можно.
*Да, Tracy я видел. Мне нужен был вулканоспецифичный аудит барьеров и трекинг дескрипторов, а не зоны таймлайна.