Flexible Data Placement (FDP)
How FDP lets the host steer writes into physically isolated regions of NAND, so that when data dies it dies together — driving device write amplification toward 1.0 without the software upheaval of ZNS.
!!! info "Terminology and scope"
**FDP = Flexible Data Placement** (not "Flexible Die Placement"). It was standardized as NVMe **TP4146**, ratified 2022-11-30, and folded into the NVMe 2.0/2.1 family (NVM Command Set Specification).
FDP keeps the **standard LBA namespace model completely intact**. No zone state machine, no zone append, no zone reset, no sequential-write requirement. A write command simply carries a *placement hint* alongside the LBA. Reads are entirely unchanged. This is the whole design thesis: get most of the garbage-collection benefit for a fraction of the host-software cost.
1. The Problem FDP Solves
!!! abstract "Core problem"
In a conventional SSD the FTL decides where every write lands. The host has no say. When the FTL's garbage collector must reclaim a block, it reads the block, filters out the still-valid pages, rewrites them elsewhere, and only then erases. That copying is **write amplification** — and it happens at times the host cannot predict, producing **tail-latency spikes** and consuming P/E cycles the host never asked for.
Why mixing is the enemy
Host writes two independent streams (different lifetimes):
A = short-lived cache entries B = long-lived index data
CONVENTIONAL SSD — one append point, everything interleaves:
┌──────────┬──────────┬──────────┐
│ Block 0 │ Block 1 │ Block 2 │
│[A][A][B] │[A][B][A] │[B][A][A] │ ← A and B physically intermixed
└──────────┴──────────┴──────────┘
│
All the A data expires (host overwrites / deletes it)
│
▼
Every block is now mostly-invalid but NOT fully invalid —
each still holds live B data.
GC must: read block → identify live B pages → rewrite them
elsewhere → erase block
Result: WAF = 2.5x. Host wrote 1 GB; NAND saw 2.5 GB.
FDP — separate append points, lifetimes don't mix:
┌──────────┬──────────┐ ┌──────────┬──────────┐
│ RU (A) │ RU (A) │ │ RU (B) │ RU (B) │
│[A][A][A] │[A][A][A] │ │[B][B][B] │[B][B][B] │
└──────────┴──────────┘ └──────────┴──────────┘
│
All the A data expires; host deallocates those LBAs
│
▼
The A Reclaim Units are now 100% invalid.
GC reads nothing, copies nothing, just erases.
Result: WAF → 1.0x for that stream.
How it shows up at scale
-
Tail-latency spikes
A GC cycle's read-copy-erase competes with host I/O. P99.9 latency can jump an order of magnitude for milliseconds at a time, with no warning the host can act on.
-
Noisy neighbor
Tenant A hammers the drive with append-only logs. Tenant B runs a latency-sensitive database on the same SSD. A's GC churn degrades B's tail latency, and B has no recourse.
-
Reduced endurance
Write amplification burns P/E cycles the host never issued. A drive rated 1 DWPD may effectively deliver 0.6 DWPD under a mixed workload.
-
Over-provisioning tax
The conventional answer to high WAF is more spare area. CacheLib ran up to 50% host over-provisioning to keep WAF tolerable — half the drive, bought and not used.
!!! tip "The FDP insight"
**The host usually knows which data is garbage before the SSD does.** A log-structured store, a KV engine, or an object store already tracks valid vs. stale in its own metadata. FDP's bet: if the host can say *"these writes belong together,"* the drive can put them in the same physical region — and when the host later deallocates that whole region's worth of LBAs, the drive erases it with zero copying.
Note what FDP does **not** do: it does not hand the host raw NAND addresses (that's closer to ZNS). The FTL still does LBA→physical mapping, wear leveling, ECC, and bad-block management. The host gets *isolation*, not *control*.
2. The Object Model
This is where most secondhand FDP explanations go wrong. There are five objects, not three, and the one people omit — the Reclaim Unit Handle — is the one that actually does the work.
| Object | What it is |
|---|---|
| Reclaim Unit (RU) | A set of physical NAND blocks that can be erased/reused without disturbing any other RU. Purely a physical construct — the host never addresses it directly. Often a superblock or sub-superblock; commonly ~GB-scale in shipping drives. |
| Reclaim Group (RG) | A collection of RUs, typically aligned to a die or set of dies. Lets the host isolate at die granularity. Many enterprise drives ship with a single RG. |
| Reclaim Unit Handle (RUH) | A controller resource that references one RU in each Reclaim Group — i.e. an append point. A conventional SSD has exactly one append point; an FDP drive exposes several. This is the thing writes are steered into. |
| Placement Handle (PHNDL) | A namespace-scoped index into that namespace's list of RUHs. Namespaces get their own handle list, so a handle is meaningful per-namespace. |
| Placement Identifier (PID) | What actually rides in the write command: a 16-bit value encoding {Reclaim Group ID, Placement Handle}. The RG:PH split within those 16 bits is set by the RGIF (Reclaim Group Identifier Format) field. |
HOST WRITE
┌────────────────────────────────┐
│ opcode=Write, SLBA, NLB │
│ CDW12: DTYPE = 2 (Data Placement)│
│ CDW13: DSPEC = PID ────────────┼──┐
└────────────────────────────────┘ │
│ PID decodes to {RG, Placement Handle}
▼
NAMESPACE ┌───────────────────┐
placement handle list ──────►│ Placement Handle 2│
└─────────┬─────────┘
│ maps to
▼
CONTROLLER ┌──────────────────────────┐
│ Reclaim Unit Handle 2 │ ← an APPEND POINT
└──────────┬───────────────┘
│ references one RU per RG
┌─────────────────────┴──────────────────┐
▼ ▼
Reclaim Group 0 Reclaim Group 1
┌──────┬──────┬──────┐ ┌──────┬──────┬──────┐
│ RU 0 │ RU 1 │ RU 2 │ │ RU 8 │ RU 9 │ RU10 │
│ │ ◄── │ │ RUH2 currently │ │ ◄── │ │
└──────┴──────┴──────┘ appending here └──────┴──────┴──────┘
When the current RU fills, the controller transparently moves the
RUH to a fresh empty RU. The host does not manage this.
!!! note "An FDP drive is a conventional drive with more append points"
That's the cleanest one-sentence model. A normal SSD streams all host writes into one head superblock. An FDP drive exposes N head points (RUHs) and lets the host choose which one each write goes to. Everything else about the drive — mapping, wear leveling, ECC — is unchanged.
The design tension: more RUHs means finer host control, but each RUH needs its own write buffer / stripe resources, and small RUs (sub-superblock) reduce the parallelism a full superblock write would get. `<small RU, many RUH>` vs. `<big RU, few RUH>` is a real SSD architecture tradeoff.
RUH types
Two flavors, reported per-RUH in the FDP Configurations log page:
- Initially Isolated — the RU referenced by this RUH contains data written only through this RUH.
- Persistently Isolated — the isolation guarantee persists across the RU's life, including through controller-internal movement.
Most current drives report all RUHs as one type; check the log page rather than assuming.
3. How a Write Carries Placement Intent
FDP rides on the existing NVMe Directives framework — it does not add new I/O opcodes.
NVMe Directives taxonomy:
00h Identify (must be supported if directives are supported)
01h Streams (the predecessor; largely superseded)
02h Data Placement ← FDP uses this one
FFh Vendor Specific
Transport: Directive Send (host → device: configuration)
Directive Receive (device → host: status/parameters)
In the Write command SQE:
CDW12, bits 23:20 → DTYPE = 2h (Data Placement Directive)
CDW13, bits 31:16 → DSPEC = PID (Placement Identifier)
!!! warning "The two fields people get backwards"
`DTYPE` lives in **CDW12**. `DSPEC` — the field that actually carries the Placement Identifier — lives in **CDW13**, bits 31:16. A write that sets one without the other does nothing useful.
Backward compatibility falls out of this design. A write command that does not specify the Data Placement Directive is treated as Placement Handle 0, and the controller picks the Reclaim Group itself. So:
- An FDP-agnostic host on an FDP drive → works, no overhead, no benefit.
- An FDP-aware host on a non-FDP drive → works, hints ignored.
This is why FDP is described as an opt-in hint rather than a contract.
Reads carry nothing
!!! info
**Read commands have no placement field.** The FTL owns the LBA→physical mapping exactly as it always did. Read LBA 1000 and the drive finds it, regardless of which RUH it was written through. **Placement is a write-path concept only.**
4. The Command & Log-Page Surface
There is no "FDP Configure" or "FDP Reclaim" opcode. The interface is assembled from existing NVMe machinery.
=== "Features"
| Feature ID | Name | Purpose |
|---|---|---|
| `1Dh` | Flexible Data Placement | Enable/disable FDP for an **Endurance Group** |
| `1Eh` | FDP Events | Control which FDP events the controller generates for a given RUH |
```bash
# Is FDP on?
nvme get-feature /dev/nvme0 -f 0x1D -H
# Which event types are enabled?
nvme get-feature /dev/nvme0n1 -f 0x1E -s 0 -c 0 -H
```
=== "Commands"
| Command | Direction | Role in FDP |
|---|---|---|
| **Directive Send / Receive** | Both | Enable the Data Placement directive (type `02h`); exchange configuration |
| **I/O Management Receive** | Device → Host | **Reclaim Unit Handle Status** — the per-RUH view of the currently-referenced RU (how full it is, etc.) |
| **I/O Management Send** | Host → Device | **Reclaim Unit Handle Update** — force an RUH to abandon its current RU and move to a fresh one |
| **Dataset Management (Deallocate)** | Host → Device | How the host actually tells the drive data is dead. **This is the reclaim path.** |
| **Get Log Page** | Device → Host | All four FDP log pages (below) |
=== "Log pages"
All four are **Endurance Group scoped** — you must pass the Endurance Group Identifier in the Log Specific Identifier (LSI) field, not a namespace ID.
| LID | Log page | Contains |
|---|---|---|
| `20h` | **FDP Configurations** | The capability descriptor: `nrg`, `nruh`, `maxpids`, `nns`, `runs`, `rgif`, plus per-RUH type |
| `21h` | **Reclaim Unit Handle Usage** | Per-RUH usage descriptors |
| `22h` | **FDP Statistics** | Host bytes with metadata written (`hbmw`), media bytes written (`mbmw`), media bytes erased (`mbe`) — **this is how you compute device WAF** |
| `23h` | **FDP Events** | Event history (see below) |
```bash
# Real output from an FDP drive (xNVMe):
# nrg: 32 nruh: 8 maxpids: 127 nns: 256 runs: 40960
# ruht[0..7]: 1 ← all eight RUHs are type 1
```
!!! tip "WAF straight from the drive"
`mbmw / hbmw` from the FDP Statistics log page gives you device write amplification directly. No estimation, no SMART-attribute reverse-engineering. For a handbook this is the single most practically useful thing on the page.
FDP events
Reported in log page 23h, gated by Feature 1Eh:
| Type | Event | What it means |
|---|---|---|
0 |
Reclaim Unit Not Fully Written | The host forced an RUH to a new RU (via RUH Update) before the current RU was full. Capacity was left on the floor — usually intentional, sometimes a bug. |
3 |
Invalid Placement Identifier | A write specified a PID that doesn't exist or isn't permitted for that namespace. Your placement hint was silently discarded. |
Additional event types exist (media/controller-initiated RU transitions). The two above are the ones a host developer will actually trip over.
!!! danger "Invalid PID does not fail the write"
A write with a bad Placement Identifier still **succeeds** — it just gets placed wherever the controller likes. Your isolation is gone and your WAF quietly climbs, with no error return. Enable event `3` and watch log page `23h`, or you will debug this the hard way.
5. Enabling FDP
!!! danger "Enabling FDP is destructive"
FDP is configured at **Endurance Group** scope, and the Set Feature (`1Dh`) that enables it requires that **all namespaces in that Endurance Group be deleted first.** This is not a drop-in runtime toggle — it is a provisioning-time decision.
(This is the opposite of what a lot of secondhand FDP writeups claim.)
# 1. Delete every namespace in the Endurance Group
nvme delete-ns /dev/nvme0 -n 1
# 2. Enable FDP on the Endurance Group (-s = endurance group id)
nvme set-feature /dev/nvme0 -f 0x1D -c 1 -s 1
# 3. Verify
nvme get-feature /dev/nvme0 -f 0x1D -H
# 4. Recreate the namespace, specifying its placement handle list (-p)
# Here: this namespace may use RUHs 0,1,2,3 → 4 placement handles
nvme create-ns /dev/nvme0 -b 4096 --nsze=<blocks> --ncap=<blocks> \
-p 0,1,2,3 -n 4
# 5. Attach it
nvme attach-ns /dev/nvme0 --namespace-id=1 --controllers=0x7
# 6. Read back the capability descriptor
nvme get-log /dev/nvme0 --log-id=0x20 --lsi=1 --log-len=512
Note step 4: the placement handle list is bound to the namespace at creation time. That's the moment you decide how many isolated streams this namespace gets.
6. The Reclaim Model
This is the part most often described incorrectly, so it's worth stating plainly.
!!! abstract "There is no host-issued erase"
**The host never erases anything.** There is no command that says "wipe Reclaim Unit N." The host's only lever is **Dataset Management (Deallocate)** on LBA ranges — exactly the same TRIM/discard it already issues today.
What FDP changes is not *how* the host frees data, but *where the data was put*, such that a deallocate can invalidate an entire RU at once.
THE FDP RECLAIM LOOP
1. Host writes stream X through Placement Handle 2 (→ RUH 2 → RU 5).
Host records: "these LBAs went to the RU behind PH 2."
2. RU 5 fills. Controller silently moves RUH 2 to a fresh RU. Host
may observe the transition via Reclaim Unit Handle Status.
3. Later, stream X's data dies (compaction done, cache entries evicted,
object deleted). Host issues:
nvme dsm --ad (deallocate) over that LBA range
4. Because every LBA in RU 5 came from stream X, RU 5 is now 100%
invalid. Nothing live remains in it.
5. Controller's GC picks up RU 5, finds zero valid pages to relocate,
and erases it.
Pages copied: 0. Device WAF for this RU: 1.0.
Contrast with a ZNS Zone Reset, which genuinely is a host-issued destructive operation with a defined state machine. FDP has no equivalent, and no way to lose live data by "reclaiming too early." If the host deallocates LBAs it still needs, that's just a normal TRIM bug — the same one it could always have had.
Reclaim Unit Handle Update
The one host-initiated placement operation: I/O Management Send → Reclaim Unit Handle Update tells the controller "stop appending to the RU currently behind this RUH; start a fresh one."
Use it when you've finished a logical unit of work (a compaction output, a cache segment) and you don't want the next logical unit sharing the same RU. The cost: the abandoned RU is not fully written, so you waste some of its capacity — which is exactly what FDP event type 0 reports.
The LBA-tracking problem
!!! warning "This is the real cost of adopting FDP"
To get WAF ≈ 1, the host must be able to say *"every LBA in that RU is now dead."* But FDP deliberately hides physical addresses. So the host has to **track which LBAs it wrote through which RUH, in fill order**, and reason about RU boundaries itself.
This is why FDP lands so easily in **log-structured** systems and so poorly in general-purpose ones. An LSM tree or a log-structured cache already writes sequentially and already knows when a segment is dead — the tracking is free. A random-update workload has to build that bookkeeping from scratch, and may not be able to at all.
Practical strategies, easiest first:
- Segment-aligned streams. Size your host-side segment (SSTable, cache region, vLog chunk) to the RU size and write it through one RUH. When the segment dies, deallocate the whole thing. Cleanest possible mapping.
- Lifetime bucketing. Don't try to match RUs exactly — just route data by expected lifetime (hot/warm/cold, or metadata/data) into different RUHs. You get most of the WAF win from separation alone, without precise tracking. This is what CacheLib does with BigHash vs. BlockCache.
- RUH status polling. Use I/O Management Receive to watch how full the current RU behind each RUH is, and align your segment boundaries to it.
- Copy-forward fallback. If some LBAs in an RU outlive the rest, the host must relocate them itself. Device WAF stays 1.0, but you've moved the amplification into your application — sometimes still a win, sometimes not.
7. FDP vs. ZNS vs. Streams
| Dimension | FDP | ZNS | Streams (legacy) |
|---|---|---|---|
| Namespace model | Standard LBA. Unchanged. | Zoned. LBA space divided into zones. | Standard LBA. |
| Write model | Any LBA, any order. PID is an optional hint in DSPEC. | Sequential-only within a zone. Zone Append command. | Any LBA. Stream ID hint. |
| Read model | Completely unchanged. | Unchanged (reads unrestricted). | Unchanged. |
| State machine | None. | Empty → Open → Closed → Full → Empty. Host manages it. | None. |
| Host-issued erase | No. Host deallocates; drive reclaims. | Yes. Zone Reset. | No. |
| FTL | Full FTL retained: mapping, wear leveling, ECC, bad blocks. | Dramatically simplified. Zone offset ≈ physical. | Full FTL. |
| Wear leveling | Drive's job. Host unaware. | Largely the host's problem. | Drive's job. |
| Host software cost | Add DTYPE/DSPEC to writes + lifetime bucketing. No FS change. | ZNS-aware filesystem (f2fs-zns, zonefs) or app-level zone management. | Minimal. |
| Isolation guarantee | Physical, at RU/RG granularity. | Physical, at zone granularity. | Weak — no way for the host to learn the current alignment. |
| Backward compatible | Yes, both directions. | No. | Yes. |
| Status | NVMe TP4146, in 2.0/2.1. Shipping. | NVMe ZNS command set. Niche. | Deprecated in practice. |
!!! tip "Why FDP won where Streams didn't"
Streams had the same basic idea — tag writes, hope the drive separates them — but gave the host **no visibility into RU alignment or capacity**. You couldn't tell when a stream's region was full, so you couldn't align your segments to it, so you couldn't reliably drive WAF to 1. FDP fixes exactly that gap with the Reclaim Unit Handle Status and the FDP log pages.
!!! tip "Choosing"
- **FDP** when you want most of the GC benefit with a bounded software change, and you want to keep your filesystem, driver, and read path exactly as they are.
- **ZNS** when you want maximum control and a cheaper drive (simplified FTL, less DRAM), and you're prepared to rewrite the storage stack to get it.
- For nearly every workload that isn't already zone-native, FDP is the pragmatic answer. That's why it's the one getting adopted.
8. Real-World Results
-
CacheLib (Meta)
BigHash produces random writes; BlockCache produces sequential writes. In a conventional SSD they intermix.
Giving each its own placement handle dropped device WAF from ~3 to ~1 — at 0% host over-provisioning, replacing the ~50% OP they had been using to control it.
Notably: on CDN workloads where BigHash isn't configured, FDP made no difference. The gain comes from separating dissimilar lifetimes — if there's nothing to separate, there's nothing to win.
-
LSM-tree stores (RocksDB, Cassandra)
Compaction output is written as sorted runs; after the next compaction the old run is entirely garbage. A natural fit: one RUH per level or per compaction output.
The block layer below the engine can often do this without modifying the engine itself.
-
Multi-tenant isolation
Each tenant gets its own RUH (and, with multiple RGs, its own dies). Tenant A's write churn can no longer force GC that stalls Tenant B.
Also enables honest per-tenant WAF attribution from the FDP Statistics log page.
-
F2FS / filesystem integration
Research work plumbs a PID through F2FS via a directory xattr, so per-path placement becomes per-write DSPEC stamping automatically — journal traffic separated from table/index updates with no application change.
!!! note "Where FDP does nothing for you"
- **Truly random, uniform-lifetime workloads.** Nothing to separate → no benefit.
- **Read-heavy workloads.** FDP is write-path only.
- **Individual I/O latency.** FDP removes GC-induced *spikes*; it does not make any single read or write faster.
- **Read disturb, bad blocks, wear leveling, power-loss protection.** All still the drive's job.
- **Security.** Deallocate is not a secure erase. Use Sanitize / Crypto Erase.
9. Pitfalls
-
Silent PID rejection
An invalid Placement Identifier does not fail the write. It's placed wherever the drive chooses, isolation is lost, and nothing tells you.
Mitigation: enable FDP event
3(Invalid Placement Identifier) and poll log page23h. Validate your PID encoding againstrgiffrom log page20h— the RG:PH bit split is not fixed. -
Assuming a namespace can use every RUH
The placement handle list is bound at namespace creation. A namespace can only target the RUHs it was created with.
Mitigation: plan the handle list up front. Changing it means recreating the namespace.
-
Forgetting FDP is Endurance-Group scoped
The log pages take an Endurance Group Identifier in LSI, not an NSID. Passing an NSID returns nothing useful, and people conclude the drive doesn't support FDP.
Mitigation:
--lsi=<endurance group id>. And remember enabling FDP requires deleting namespaces. -
Expecting a runtime enable
You cannot flip FDP on for a populated drive. Set Feature
1Dhrequires all namespaces in the Endurance Group deleted.Mitigation: treat FDP as a provisioning decision, not a tuning knob.
-
Bypassing the block layer, unexpectedly
The Linux block layer does not carry FDP directives. FDP I/O goes through the character device (
/dev/ng0n1) via io_uring passthru, or a userspace driver.Mitigation: target the Generic device node from
nvme list, not the block node. This surprises everyone once. -
Over-writing an RUH's RU
Forcing an RUH Update before its RU is full wastes the remainder of that RU's capacity.
Mitigation: watch event type
0(Reclaim Unit Not Fully Written) and Reclaim Unit Handle Status. Align host segment size to RU size where you can. -
RU size mismatch
RUs are commonly ~GB-scale. If your host segments are 64 MB, you'll be mixing many segments per RU and lose most of the isolation benefit.
Mitigation: read
runsand the RU size from log page20hbefore designing your segment layout. Don't assume. -
Too many RUHs
More append points sounds strictly better. It isn't — each RUH consumes write-buffer/stripe resources, and sub-superblock RUs sacrifice channel/die parallelism.
Mitigation: use the number of RUHs the drive actually exposes (
nruh), and treat lifetime classes, not tenants, as the unit of separation.
10. Tooling
| Tool | FDP support |
|---|---|
| nvme-cli | nvme fdp-status, fdp-configs, fdp-stats, fdp-events, fdp-update, fdp-set-events; plus get-log/set-feature directly |
| libnvme | nvme_fdp_reclaim_unit_handle_status() and friends |
| xNVMe | Full FDP support: xnvme log-fdp-config, log-fdp-stats, log-fdp-events, RUH status |
| SPDK | Native FDP support since 23.05. Canonical example: test/nvme/fdp/fdp.c |
| fio | Via the xNVMe ioengine since 3.35: fdp=1, fdp_pli=, fdp_pli_select={random,roundrobin}. Character device only. |
| Linux kernel | I/O passthru (io_uring_cmd) on the NVMe generic char device. Not the block layer. |
11. Study Checklist
Fundamentals
- [ ] Explain why intermixing lifetimes in one RU is what causes write amplification
- [ ] State FDP's design thesis: hints, not addresses — the FTL is fully retained
- [ ] Explain why FDP is backward compatible in both directions
Object model
- [ ] Name all five objects: RU, RG, RUH, Placement Handle, Placement Identifier
- [ ] Explain what a Reclaim Unit Handle actually is (an append point) and why a conventional SSD has exactly one
- [ ] Explain how a PID decodes to {RG, Placement Handle} and what
rgifcontrols - [ ] Explain the
<small RU, many RUH>vs.<big RU, few RUH>architecture tradeoff
Interface
- [ ] State where DTYPE and DSPEC live (CDW12 vs. CDW13) — and that FDP adds no new I/O opcodes
- [ ] Name the four FDP log pages and their LIDs, and know they're Endurance Group scoped
- [ ] Compute device WAF from
mbmw / hbmwin log page22h - [ ] Explain what Feature
1Dhand1Ehdo - [ ] Explain why enabling FDP requires deleting namespaces
Reclaim
- [ ] Explain that the host never erases — it deallocates, and the drive reclaims
- [ ] Trace the full reclaim loop from write → fill → deallocate → zero-copy erase
- [ ] Explain what Reclaim Unit Handle Update does and what it costs
- [ ] Explain the LBA-tracking problem and why log-structured systems get it for free
- [ ] Explain why an invalid PID is a silent failure
Comparison & use
- [ ] Contrast FDP / ZNS / Streams on write model, host-issued erase, FTL role, and software cost
- [ ] Explain why Streams failed where FDP succeeds (no RU alignment visibility)
- [ ] Explain the CacheLib result and why the CDN workload saw no benefit
- [ ] State what FDP does not solve
References
- NVMe TP4146 — Flexible Data Placement (ratified 2022-11-30)
- NVM Express Base Specification 2.1; NVM Command Set Specification 1.1/1.2
- Samsung — Introduction to Flexible Data Placement whitepaper; NVMe FDP: A Promising New SSD Data Placement Approach
- CacheLib — FDP-enabled cache documentation (the WAF 3→1 result)
- xNVMe FDP tutorial; SPDK
test/nvme/fdp/fdp.c