Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4cac32a
0.4.1 릴리스 뒤 main 을 dev 에 병합한다
cmir79 Sep 26, 2026
396caf7
dev 의 판 번호를 0.4.2-dev 로 — 0.4.1 은 나갔다
cmir79 Sep 26, 2026
9af786b
정지 순간의 블록에 대한 하류 실측을 evaluation.md 에 적는다
cmir79 Sep 26, 2026
8a230e0
정지 순간의 블록 절을 정정한다 — 이 Basler 는 간헐적으로 블록을 끊는다
cmir79 Sep 26, 2026
d599e0c
정지 뒤 그랩의 시한 초과가 수신기 회귀가 아님을 대조 실측으로 닫는다
cmir79 Sep 26, 2026
0361ad6
GVCP 재시도 횟수와 PENDING_ACK 자동 상한의 int 셈이 감기지 않게 한다
cmir79 Sep 26, 2026
c194d48
DeviceHeartbeatTimeoutMs 를 음수로 내보내지 않게 포화한다
cmir79 Sep 26, 2026
7c05158
MaxPendingAckWaitMs = 0 의 뜻을 옵션 문서에 적고 시험으로 못 박는다
cmir79 Sep 26, 2026
e30f465
GevTimeoutException 문서에 이 예외가 나는 세 경우를 모두 적는다
cmir79 Sep 26, 2026
241f1fe
IGevPort 의 32비트 초과 주소 문서를 실제 동작(하위 32비트로 좁힘)에 맞춘다
cmir79 Sep 26, 2026
88c71e2
탐색 소켓의 바인드가 실패하면 그 자리에서 닫는다
cmir79 Sep 26, 2026
91f6c2e
탐색 반복을 창 안에 가두고 1 미만의 Repeat 를 거절한다
cmir79 Sep 26, 2026
986949d
탐색이 아무것도 보내지 못한 빈 결과를 까닭과 함께 경고한다
cmir79 Sep 26, 2026
39800e1
ProbeAsync 가 null 을 돌려주는 까닭과 던지는 예외를 문서에 모두 적는다
cmir79 Sep 26, 2026
a1702fa
XML 적재가 장치 상실을 일반 GevException 으로 감싸지 않고 원래 형으로 던진다
cmir79 Sep 26, 2026
45e217a
무효화가 수식 노드의 pVariable 입력까지 내려가 래치 뒤 레지스터를 다시 읽게 한다
cmir79 Sep 26, 2026
18fd901
IsDone 되읽기가 명령의 접근 모드를 따르고, 완료 규칙과 PollingTime 의 쓰임을 문서에 정확히 적는다
cmir79 Sep 26, 2026
74feb71
SetTlParamsLockedAsync 가 false 를 돌려주는 두 경우를 로그와 문서에서 가른다
cmir79 Sep 26, 2026
5853cb9
제어 채널이 세션보다 먼저 닫히면 그 자리에서 제어권 상실로 넘기고, ObjectDisposedException 을 오류 계…
cmir79 Sep 26, 2026
849489f
시뮬레이터 타임스탬프 제어를 1 = reset, 2 = latch 로 바로잡고 전송 계층 이름(GevTimestamp*)을 싣는다
cmir79 Sep 26, 2026
5ea0d1b
시뮬레이터가 획득 중 AcquisitionMode 와 이미지 형식 피처를 잠근다
cmir79 Sep 26, 2026
79b0d8c
시뮬레이터에 장치 재부팅을 흉내 내는 Reboot() 를 더한다
cmir79 Sep 26, 2026
7161052
시뮬레이터 문서에 재전송 명령의 재실행과 시험용 호스트 타이밍을 적는다
cmir79 Sep 26, 2026
0563192
스트림 정지가 호출자의 취소로 장치 전송 끄기와 로컬 정리를 건너뛰지 않게 한다
cmir79 Sep 26, 2026
98992ac
스트림 소켓 생성이 실패해도 시작이 정지 상태로 끝나게 한다
cmir79 Sep 26, 2026
4eb0cfa
수신 스레드가 스스로 끝나면 IsStarted 가 거짓이 되게 한다
cmir79 Sep 26, 2026
abc4693
첫 페이로드 전에 끊긴 블록을 보존 시간까지 붙들지 않고 곧바로 불완전으로 닫는다
cmir79 Sep 26, 2026
3cbf71d
이미 실은 뒤에 패킷 간격이 바뀐 프레임을 완성으로 내보내지 않고 오류로 버린다
cmir79 Sep 26, 2026
45454f5
리더만 받은 프레임 위로 같은 블록 번호의 새 촬영이 오면 옛 리더로 조립하지 않고 슬롯을 다시 연다
cmir79 Sep 26, 2026
b1abad8
트레일러를 잃은 채 바이트가 모자란 프레임이 보존 시간까지 기다리는 까닭을 적는다
cmir79 Sep 26, 2026
33bfca1
리센드가 꺼지면 보존 시간이 쓰이지 않는다는 것과 수신 루프의 실제 점검 간격을 문서와 시작 로그에 적는다
cmir79 Sep 26, 2026
e18e1ea
GevDevice.OpenAsync(IPEndPoint) 를 공개해 표준 포트가 아닌 장치를 열 수 있게 한다
cmir79 Sep 26, 2026
78619b0
PENDING_ACK 연장을 다 쓴 시한 초과를 XML 적재가 장치 상실로 읽지 않게 표식으로 가른다
cmir79 Sep 26, 2026
e7ffd9d
XML 적재가 장치 상실이 아닌 시한 초과를 맨 GevTimeoutException 으로 내지 않게 한다
cmir79 Sep 26, 2026
49096fb
XML 캐시 키를 읽다가 장치를 잃으면 삼키지 않고 그 자리에서 던진다
cmir79 Sep 26, 2026
93c0d28
GvcpChannel 이 호출자의 IPEndPoint 대신 자기 사본을 쥐어, 연 뒤에 그 객체를 바꿔도 응답 대조가 깨지지…
cmir79 Sep 26, 2026
41641e5
시뮬레이터 Reboot 가 제어권 해제(null) 알림을 명령 잠금 안에서 올려, 새 보유자 알림보다 늦게 닿지 않게 한다
cmir79 Sep 26, 2026
884644b
시뮬레이터의 타임스탬프 reset 이 카운터를 실제로 되돌리는지 가르는 단정을 더한다
cmir79 Sep 26, 2026
a29bdd1
CLI 의 InternalsVisibleTo 주석과 DeviceTarget.ProbeAsync 의 null 설명을 현재 계약…
cmir79 Sep 26, 2026
b8e1d97
GevDevice.OpenAsync 세 오버로드에 던지는 예외 전부를 적는다
cmir79 Sep 26, 2026
9459a51
스트림 정지의 SCP·SCDA 끄기에 호출자 토큰·재시도와 무관한 고정 예산을 준다
cmir79 Sep 26, 2026
3badaa6
리센드가 꺼져 있으면 트레일러 없는 버린 프레임도 재요청 간격에 닫는다
cmir79 Sep 26, 2026
f314a5c
CompleteFramesAreDeliveredInOrder 가 수신기가 보낸 패킷을 다 센 뒤에 계수기를 보게 한다
cmir79 Sep 26, 2026
f388840
스트림 시험 셋이 수신기 쪽 사건을 기다린 뒤 계수기를 보게 한다
cmir79 Sep 26, 2026
006c146
SlowSenderDoesNotTriggerSpuriousResends 의 실패가 수신 쪽 유실과 군더더기 요청을 가르게 한다
cmir79 Sep 26, 2026
2b7e807
수신 대기를 소켓 수신 시한 대신 Poll 로 해 윈도우에서 데이터그램을 잃지 않게 한다
cmir79 Sep 26, 2026
bab22df
제어 채널이 응답 대조용 끝점을 밖으로 내주지 않고, 정지·닫기 예산을 채널이 쥔 시한에서 정하게 한다
cmir79 Sep 26, 2026
34a874d
Poll 수신 대기의 뒷정리 — 대기 간격 상한, 닫힌 소켓의 종료 사유, .NET Framework 할당 기록, 옛 대기를…
cmir79 Sep 26, 2026
92594c7
0.5.0 으로 판 번호를 확정한다
cmir79 Sep 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
내지 않는다 — 다음 판에 같이 나간다). 올린 번호는
다시 쓰지 않는다 — NuGet 은 지울 수도 덮어쓸 수도 없다. publish.yml 이 태그와 이 값이 같은지, 태그 커밋이
main 에 있는지 검사하고, 태그가 아닌 ref 에서는 올리지 않는다. -->
<Version>0.4.1</Version>
<Version>0.5.0</Version>

<Authors>kintaein</Authors>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
Expand Down
130 changes: 104 additions & 26 deletions docs/architecture.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/design-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ holds today and nothing would notice a regression. `partial` = some cases guarde
| R9 | met | `FormulaParser.cs:270,277` (depth 200); `NodeBinder.cs:179-239` — **iterative** DFS, so cyclic XML cannot overflow the stack | `NodeMapBindTests.ReferenceCycle_IsDetectedAtBind`, `FormulaTests.DeepParenthesesAreRejectedWithoutStackOverflow` |
| R10 | met | `GenApi/Runtime/IntegerNodes.cs:365-380` (`FieldOf` flips LSB/MSB for BigEndian) | `IntegerNodeTests.MaskedIntReg_BigEndian_Bit0_IsMostSignificantBit`, `MaskedIntReg_LittleEndian_Lsb0Msb7_IsLowByte`, `MaskedIntReg_BitBeyondRegister_FailsAtBind` |
| R11 | met | `Gvsp/GevStream.PacketSize.cs:26-65,78,157`; SCPD from the option only | `PacketSizeNegotiationTests.*`, `StreamingScenarioTests.Start_AccessesChannelRegistersInTheDocumentedOrder_AndStopReversesIt` |
| R12 | met | `Gvsp/GevStream.cs` (granted socket buffer read back and logged, dedicated receiver thread), `Receiver.cs` (one reusable scratch buffer, pooled slots and frame buffers) | `ReceiverAllocationTests` — the receiver's per-datagram work is called on the test thread (`FeedPacketForTest`), so `GC.GetAllocatedBytesForCurrentThread` can see it: 0 bytes across 700+ packets, 0 bytes for late/duplicate packets of a closed block, and completing a frame allocates only the `GevFrame` object (≤ 256 bytes, not the 64 KiB image). Mutation-checked: one unguarded interpolated `GevLog.Debug` on the packet path fails both. `GvcpChannelTests.PacketResendDoesNotAllocateOnTheHotPath` guards the GVCP side |
| R12 | met (one runtime exception, 2026-09-26) | **Exception:** on .NET Framework (the netstandard2.0 asset) the receiver's wait allocates — `Socket.Poll` there allocates a small array per call (about 40 B, measured; 0 B on net6/net8), and the receiver polls whenever the socket is empty, about once per packet at full rate. Accepted: the previous zero-allocation wait (blocking receive with a socket receive timeout) lost datagrams on Windows (`docs/evaluation.md`, "Receive wait on Windows"). `ReceiverAllocationTests` feed packets past the socket, so they do not see the wait. Otherwise: `Gvsp/GevStream.cs` (granted socket buffer read back and logged, dedicated receiver thread), `Receiver.cs` (one reusable scratch buffer, pooled slots and frame buffers) | `ReceiverAllocationTests` — the receiver's per-datagram work is called on the test thread (`FeedPacketForTest`), so `GC.GetAllocatedBytesForCurrentThread` can see it: 0 bytes across 700+ packets, 0 bytes for late/duplicate packets of a closed block, and completing a frame allocates only the `GevFrame` object (≤ 256 bytes, not the 64 KiB image). Mutation-checked: one unguarded interpolated `GevLog.Debug` on the packet path fails both. `GvcpChannelTests.PacketResendDoesNotAllocateOnTheHotPath` guards the GVCP side |
| R13 | met | `Gvcp/GevDiscovery.cs` — `SelectInterfaces` enumerates every up IPv4 interface, `BuildTargets` forms the limited and directed broadcasts; `GevNet.cs` (`GetIpv4Interfaces`, `DirectedBroadcast`) | `GevDiscoveryTests.Probe*`, `DiscoverCollectsRepliesFromEveryTargetAndDedupesByMac`, and `DiscoveryBroadcastTests` (11 cases: per-mask directed address, unknown mask, /0 collapsing onto the limited one, unicast appended not substituted, loopback opt-in enumeration, and both broadcasts observed leaving the socket) — mutation-checked: forcing `DirectedBroadcast` off fails 7 |
| R14 | met | `GevDiscovery.cs:214-218,284-288`; `GevDeviceInfo.cs:52` | `GevDiscoveryTests.DiscoverSkipsTruncatedRepliesInsteadOfCreatingGhosts`, `ProbeSkipsTruncatedDiscoveryAck` |
| R15 | met | `Gvcp/GvbsAddr.cs` (the offset table), `GevDeviceInfo.cs:55-101`, `GevDevice.cs:113-116` — the only name lookup in the library is `TLParamsLocked`, which is not a bootstrap register | `GevDeviceInfoReadTests.EveryFieldIsReadAtItsOwnAddress`, `OpenIsNotFooledByADeviceWhoseBulkReadSkipsUnimplementedWords` |
Expand Down
57 changes: 55 additions & 2 deletions docs/evaluation.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,39 @@ resumes later, would otherwise see the stream go permanently silent with no erro
Three defects were found here that no simulator run had shown, each now fixed and guarded by a test:
the transport-layer lock (`TLParamsLocked`) that gates the acquisition commands, the host firewall that
silently swallowed every GVSP packet, and GenApi addresses above 32 bits that made the whole File Access
category unreadable. A fourth was cosmetic but real: a blocking receive can return `IOPending` on Windows,
which the receiver logged as an error and answered with a sleep.
category unreadable. A fourth looked cosmetic at the time: a blocking receive can return `IOPending` on Windows,
which the receiver logged as an error and answered with a sleep. It was not cosmetic — see the next section.

### Receive wait on Windows: a timed-out blocking receive loses datagrams (2026-09-26)

*Bench measurement with the CLI harness (`samples/GevSharp.Cli`): protocol layer only, no consumer application in the path.*

Up to 0.4.1 the receiver waited for packets with a blocking receive and a socket receive timeout
(`SO_RCVTIMEO`), 2 ms while a frame is being assembled and 200 ms when idle, and treated `IOPending` like a
timeout. Windows documents the socket state after a timed-out blocking receive as indeterminate, and in
practice a datagram that arrives at the moment the timeout expires can be lost. A loopback probe under load
lost exactly as many datagrams as it saw `IOPending` returns.

On hardware the condition is packets spaced wider than the wait interval, so that every packet arrives just
after a wait ends: slow senders, a large inter-packet delay (SCPD), bandwidth shared between cameras, or a
device that sends the leader long before the payload. Basler acA2500-14gm, Mono8 2592x1944, SCPD 300000 ticks
(about 2.4 ms between packets), resend off so a loss is not hidden, three concurrent test runs as CPU load,
60 s per run:

| Receiver | Run | Blocks | Incomplete | Missing packets |
|---|---|---|---|---|
| 0.3.0 CLI (`SO_RCVTIMEO` wait) | 1 | 46 | 9 | 10 |
| 0.3.0 CLI (`SO_RCVTIMEO` wait) | 2 | 39 | 8 | 9 |
| this tree (non-blocking receive + `Poll`) | 1 | 39 | 0 | 0 |
| this tree (non-blocking receive + `Poll`) | 2 | 39 | 0 | 0 |

With resend on, each such loss costs a resend request instead of a frame, which is why the full-rate runs
above never showed it: back-to-back packets do not leave the 2 ms gaps. At full rate the new wait changes
nothing measurable: 120 s, 1752 frames, 989,880 packets, 14.59 fps, 0 resend requests, the same as before.
The receiver now receives non-blocking while data is queued and waits with `Poll` (which does not consume
data) only when the socket is empty. `GevSharp.Tests` has an opt-in load test for the loopback case
(`ReceiveWaitLossTests`, `GEVSHARP_STRESS=1`); it did not reproduce the loss in 2,800 frames on the old code,
so the hardware table is the evidence.

### Odd-width GVSP Packed line rule — settled by measurement, and we had it wrong

Expand Down Expand Up @@ -273,6 +304,28 @@ clamped at the expected size. This matters since frame completion also requires
frame closed as incomplete, with the one-time warning naming both sizes. Not observed on this camera; the rule
is left as is until a device shows it, and the warning is what would surface it.

**Blocks at acquisition stop (2026-09-26, reported through the CvInspect adapter — not a CLI harness run).** The
same R29 check closes a block that a device cuts short with an early trailer. The consumer ran GevSharp 0.4.1
through its adapter on this camera with single grabs, live bursts, 1 ms-timeout grabs that stop acquisition while
a frame is in flight, and cancellations 0–3 ms after the call. The first run ended with 0 incomplete. The second
run of the same procedure had **one cut block**: during the cancellation series, block 55 ended with a trailer
after 431 payload packets — 3,856,896 of the 5,038,848 bytes the leader announced — and was closed as
incomplete with the one-time warning. This is the first hardware observation of the case R29 guards: 0.4.0 would
have delivered that frame as complete with the previous frame's pixels in its missing part. So this Basler
**sometimes** cuts the block it is sending when acquisition is stopped; one clean run did not show it, and one
such run is not evidence that a model never cuts. In the same run the next single grab timed out once (2 s).

The consumer then ran a 0.4.0 control with the same adapter source and procedure (cancel 0–3 ms after the call ×20,
then five normal grabs; eight rounds). Both versions saw the device cut blocks with an early trailer ("trailer sets
packet count 563 -> 552" and similar), and some of the cut blocks were the frame of the **normal grab right after
a cancellation round** — no packet was missing and no resend was asked; the device sent the trailer early.
0.4.1 closed those frames as incomplete, so that grab timed out (packets arrived in the window, 0 completed,
1 incomplete). 0.4.0 returned the cut frame as that grab's answer, marked complete, with its last 12–14 packets'
worth of bytes still holding the previous frame. So the timeout is not a receiver regression: it is the same
device behaviour, now reported instead of delivered as a wrong image. Scope: this one camera, grabs right after a
cancelled grab, intermittent (2 of 8 rounds per version). Why the device cuts the next grab's block was not
measured. Figures and raw logs are the consumer's (its `cvinspect-0290-cutloop` run); see its records.

`PixelFormatInfo.FrameBytes` is the single definition of that and `GvspImageLeader.ImageBytes` routes
through it, so the receiver sizes a frame the way the device does. Where a line is not a whole number of
bytes and there is no line padding there is no stride at all, and saying so is part of the fix:
Expand Down
2 changes: 1 addition & 1 deletion docs/genapi-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ all four float kinds → `Float`, `Node`/`Unknown` → `Unknown`).
| `IsStreamable` | bool | `Streamable` | default false |
| `PErrors` | IReadOnlyList\<string\> | `pError`* | |
| `IsDeprecated` | bool | `IsDeprecated` | default false |
| `PollingTimeMs` | long? | `PollingTime` | register nodes: treat reads as NoCache; Command: completion polling |
| `PollingTimeMs` | long? | `PollingTime` | only its presence is used, never the interval: register nodes treat reads as NoCache; a Command's `IsDone` re-reads `pValue` (GevSharp's own rule). The caller picks the polling interval |
| `PSelected` | IReadOnlyList\<string\> | `pSelected`* | accepted on any kind; meaningful on Integer kinds, Enumeration, Boolean. `pSelecting` is derived by the runtime |

`MergePriority`/`ExposeStatic` attributes and `<Extension>` are ignored.
Expand Down
9 changes: 7 additions & 2 deletions docs/protocol-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ See `GvbsAddr`. Highlights:
- `0x0200` first URL (512), `0x0400` second URL (512) — camera XML location.
- `0x0934` GVCP capability bits (bit2 packet resend, bit5 pending ack, bit29 heartbeat disable, …),
`0x0938` heartbeat timeout (ms), `0x093C/0x0940` timestamp tick frequency (Hz, 64-bit),
`0x0944` timestamp control (write 2 = reset, 1 = latch), `0x0948/0x094C` latched timestamp.
`0x0944` timestamp control (write 1 = reset, 2 = latch — the values real device descriptions put in
`GevTimestampControlReset`/`GevTimestampControlLatch`; an earlier revision of this line had them swapped),
`0x0948/0x094C` latched timestamp.
- `0x0A00` CCP: 1 = exclusive, 2 = control, 4 = control switchover enable; 0 = open. Writing CCP requires
no privilege when the register is 0; writes from a non-controlling host return `ACCESS_DENIED (0x8006)`.
`0x0A04`/`0x0A14` primary application port/IP — the socket of whoever holds control, which answers
Expand Down Expand Up @@ -243,7 +245,10 @@ into a node map is a later milestone.
`<pSelected>` on selector features (the selected features are those listed).
- Guards: `<pIsImplemented>`, `<pIsAvailable>`, `<pIsLocked>` (Integer/Boolean/SwissKnife nodes: non-zero = true),
`<ImposedAccessMode>`, `<pInvalidator>` (nodes whose write invalidates this node's cache), `<Streamable>`.
- Commands: `<CommandValue>` / `<pCommandValue>` written to `<pValue>`; `<PollingTime>` marks self-clearing bits.
- Commands: `<CommandValue>` / `<pCommandValue>` written to `<pValue>`. `<PollingTime>` on a Command is *read by
GevSharp* as marking a self-clearing bit that `IsDone` may poll — GevSharp's own reading, not a rule taken from a
primary source (none checked). Descriptions often leave it out, also on commands that take time to finish
(a user-set load), and others put it on exactly those commands.
- Booleans: `<OnValue>` / `<OffValue>` (default 1 / 0).
- Strings: `<StringReg>` fixed `Length`, `<String>` literal or `pValue`.
- Floats: `<FloatReg>` Length 4/8 IEEE; `<Converter>`; `<Float>` with `Min/Max/Inc/Unit/Representation/DisplayNotation/DisplayPrecision`.
Loading
Loading